导出产物保存收敛成共享 exportArtifactDownload
- 新增 exportArtifactDownload.ts,承载保存对话框 + save_local_project_asset_file + 文件管理器定位 - 删掉逐字重复的 taonierDownload.ts 与 xhsMinitoolDownload.ts - 两个 ArtifactsPane 改用通用的 saveExportArtifactToDisk / revealExportArtifactInManager
This commit is contained in:
+10
-9
@@ -1,14 +1,15 @@
|
||||
/**
|
||||
* 把陶泥儿导出产物经原生保存对话框落到用户选定的位置。
|
||||
* 把导出 tab 的产物(zip / icon / 代码包)经原生保存对话框落到用户选定的位置。
|
||||
*
|
||||
* 复用素材保存的既有链路 `save_local_project_asset_file`:路径门禁、敏感文件拒绝、自我覆盖保护
|
||||
* 都在宿主那一侧,前端不自己读文件、不拼绝对路径、也不写第二份保存实现。
|
||||
* 都在宿主那一侧,前端不自己读文件、不拼绝对路径、也不写第二份保存实现。陶泥儿与小红书小工具
|
||||
* 的下载行为完全一致,这里收敛成一份,避免两边各改各的。
|
||||
*
|
||||
* 返回值不只说明这次存没存下来,失败还带一句原因:`saved`(宿主写完)、`cancelled`(用户关掉
|
||||
* 对话框)、`failed`(对话框打不开 / 宿主拒绝写)。成功不必在面板上复述——落点是用户亲手选的,
|
||||
* 原生对话框就是回执;失败则由调用方决定怎么告知。
|
||||
*
|
||||
* 同一条保存链路上还有 [`revealTaonierFileInManager`]:保存失败时用系统文件管理器打开原件
|
||||
* 同一条保存链路上还有 [`revealExportArtifactInManager`]:保存失败时用系统文件管理器打开原件
|
||||
* 所在位置,作为用户自己把文件拿出去的兜底出口。
|
||||
*/
|
||||
import { save as saveNativeFileDialog } from '@tauri-apps/plugin-dialog';
|
||||
@@ -17,24 +18,24 @@ import { resolveTauriInvoke } from '../../../../app/tauri';
|
||||
import { resourceFileNameFromPath } from '../../../../features/resource-canvas/resourceCanvasAssetTransferModel';
|
||||
|
||||
/** 一次导出产物保存的结局。失败带一句可展示的原因(宿主给的是中文人话)。 */
|
||||
export type TaonierDownloadResult =
|
||||
export type ExportArtifactDownloadResult =
|
||||
| { status: 'saved' }
|
||||
| { status: 'cancelled' }
|
||||
| { status: 'failed'; reason: string };
|
||||
|
||||
/** 在系统文件管理器里定位一份导出产物的结局。 */
|
||||
export type TaonierRevealResult =
|
||||
export type ExportArtifactRevealResult =
|
||||
| { status: 'revealed' }
|
||||
| { status: 'failed'; reason: string };
|
||||
|
||||
export async function saveTaonierFileToDisk(input: {
|
||||
export async function saveExportArtifactToDisk(input: {
|
||||
projectPath: string;
|
||||
/** 项目内相对路径,交给宿主的既有门禁复核。 */
|
||||
relativePath: string;
|
||||
/** 路径里取不出文件名时用的兜底名。 */
|
||||
fallbackFileName: string;
|
||||
title: string;
|
||||
}): Promise<TaonierDownloadResult> {
|
||||
}): Promise<ExportArtifactDownloadResult> {
|
||||
const invoke = resolveTauriInvoke();
|
||||
if (!invoke) {
|
||||
return { status: 'failed', reason: '需要在应用内运行才能保存文件' };
|
||||
@@ -75,10 +76,10 @@ export async function saveTaonierFileToDisk(input: {
|
||||
* 保存失败后的兜底出口:不去替用户挑落点,只把原件所在位置打开,让他自己复制。与保存共用同一条
|
||||
* 宿主路径门禁——能保存的原件才能被定位,打开的一定是项目里真实存在的那份文件。
|
||||
*/
|
||||
export async function revealTaonierFileInManager(input: {
|
||||
export async function revealExportArtifactInManager(input: {
|
||||
projectPath: string;
|
||||
relativePath: string;
|
||||
}): Promise<TaonierRevealResult> {
|
||||
}): Promise<ExportArtifactRevealResult> {
|
||||
const invoke = resolveTauriInvoke();
|
||||
if (!invoke) {
|
||||
return { status: 'failed', reason: '需要在应用内运行才能打开文件管理器' };
|
||||
-113
@@ -1,113 +0,0 @@
|
||||
/**
|
||||
* 把导出产物(icon / zip)经原生保存对话框落到用户选定的位置。
|
||||
*
|
||||
* 复用素材保存的既有链路 `save_local_project_asset_file`:路径门禁、敏感文件拒绝、自我覆盖保护
|
||||
* 都在宿主那一侧,前端不自己读文件、不拼绝对路径、也不写第二份保存实现。
|
||||
*
|
||||
* 返回值不只说明这次存没存下来,失败还带一句原因:`saved`(宿主写完)、`cancelled`(用户关掉
|
||||
* 对话框)、`failed`(对话框打不开 / 宿主拒绝写)。成功不必在面板上复述——落点是用户亲手选的,
|
||||
* 原生对话框就是回执;失败则由调用方决定怎么告知,不能像以前那样只留一条控制台痕迹,也不能只给
|
||||
* 一句没有下文的「请重试」。
|
||||
*
|
||||
* 同一条保存链路上还有 [`revealXhsMinitoolFileInManager`]:保存失败时用系统文件管理器打开原件
|
||||
* 所在位置,作为用户自己把文件拿出去的兜底出口。
|
||||
*/
|
||||
import { save as saveNativeFileDialog } from '@tauri-apps/plugin-dialog';
|
||||
|
||||
import { resolveTauriInvoke } from '../../../../app/tauri';
|
||||
import { resourceFileNameFromPath } from '../../../../features/resource-canvas/resourceCanvasAssetTransferModel';
|
||||
|
||||
/** 一次导出产物保存的结局。失败带一句可展示的原因(宿主给的是中文人话)。 */
|
||||
export type XhsMinitoolDownloadResult =
|
||||
| { status: 'saved' }
|
||||
| { status: 'cancelled' }
|
||||
| { status: 'failed'; reason: string };
|
||||
|
||||
/** 在系统文件管理器里定位一份导出产物的结局。 */
|
||||
export type XhsMinitoolRevealResult =
|
||||
| { status: 'revealed' }
|
||||
| { status: 'failed'; reason: string };
|
||||
|
||||
export async function saveXhsMinitoolFileToDisk(input: {
|
||||
projectPath: string;
|
||||
/** 项目内相对路径,交给宿主的既有门禁复核。 */
|
||||
relativePath: string;
|
||||
/** 路径里取不出文件名时用的兜底名。 */
|
||||
fallbackFileName: string;
|
||||
title: string;
|
||||
}): Promise<XhsMinitoolDownloadResult> {
|
||||
const invoke = resolveTauriInvoke();
|
||||
if (!invoke) {
|
||||
return { status: 'failed', reason: '需要在应用内运行才能保存文件' };
|
||||
}
|
||||
let destination: string | null = null;
|
||||
try {
|
||||
destination = await saveNativeFileDialog({
|
||||
// 默认文件名取路径最后一段(`/` 与 `\` 都认),取不到就用调用方给的兜底名。
|
||||
defaultPath:
|
||||
resourceFileNameFromPath(input.relativePath.trim()) ||
|
||||
input.fallbackFileName,
|
||||
title: input.title,
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('导出产物保存对话框没能打开', error);
|
||||
return { status: 'failed', reason: '保存对话框没能打开' };
|
||||
}
|
||||
// 用户取消对话框:什么都没发生,也没有话要说。
|
||||
if (!destination) return { status: 'cancelled' };
|
||||
try {
|
||||
await invoke('save_local_project_asset_file', {
|
||||
input: {
|
||||
projectPath: input.projectPath,
|
||||
relativePath: input.relativePath,
|
||||
destinationPath: destination,
|
||||
},
|
||||
});
|
||||
return { status: 'saved' };
|
||||
} catch (error) {
|
||||
console.warn('导出产物没能写到你选的位置', error);
|
||||
return { status: 'failed', reason: exportFailureReason(error) };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在系统文件管理器里定位这份导出产物。
|
||||
*
|
||||
* 保存失败后的兜底出口:不去替用户挑落点,只把原件所在位置打开,让他自己复制。与保存共用同一条
|
||||
* 宿主路径门禁——能保存的原件才能被定位,打开的一定是项目里真实存在的那份文件。
|
||||
*/
|
||||
export async function revealXhsMinitoolFileInManager(input: {
|
||||
projectPath: string;
|
||||
relativePath: string;
|
||||
}): Promise<XhsMinitoolRevealResult> {
|
||||
const invoke = resolveTauriInvoke();
|
||||
if (!invoke) {
|
||||
return { status: 'failed', reason: '需要在应用内运行才能打开文件管理器' };
|
||||
}
|
||||
try {
|
||||
await invoke('reveal_local_project_asset_file', {
|
||||
projectPath: input.projectPath,
|
||||
relativePath: input.relativePath,
|
||||
});
|
||||
return { status: 'revealed' };
|
||||
} catch (error) {
|
||||
console.warn('导出产物没能在文件管理器里打开', error);
|
||||
return { status: 'failed', reason: exportFailureReason(error) };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 把宿主或对话框抛出的原因收成一句能给用户看的话。
|
||||
*
|
||||
* 宿主这条链路的拒绝都是中文人话(`Result<_, String>`),原样带出去;认不出形状时退回一句通用
|
||||
* 说明,不把 `[object Object]` 之类的内部形状糊到界面上。
|
||||
*/
|
||||
function exportFailureReason(error: unknown): string {
|
||||
if (typeof error === 'string') {
|
||||
return error.trim() || '写入所选位置失败';
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message.trim() || '写入所选位置失败';
|
||||
}
|
||||
return '写入所选位置失败';
|
||||
}
|
||||
+5
-5
@@ -2,9 +2,9 @@ import { type ReactNode, useState } from 'react';
|
||||
|
||||
import type { GameDistributionPublishResult } from '../../../../../services/gameDistributionPublish';
|
||||
import {
|
||||
revealTaonierFileInManager,
|
||||
saveTaonierFileToDisk,
|
||||
} from '../../state/taonierDownload';
|
||||
revealExportArtifactInManager,
|
||||
saveExportArtifactToDisk,
|
||||
} from '../../state/exportArtifactDownload';
|
||||
import { TAONIER_ARTIFACT_RELATIVE_PATH } from '../../state/taonierInstruction';
|
||||
import type { TaonierExportState } from '../../state/useTaonierExport';
|
||||
import { PaneButton } from '../common/PaneButton';
|
||||
@@ -77,7 +77,7 @@ export function ArtifactsPane({
|
||||
const downloadArtifact = (): void => {
|
||||
if (!projectPath) return;
|
||||
const relativePath = TAONIER_ARTIFACT_RELATIVE_PATH;
|
||||
void saveTaonierFileToDisk({
|
||||
void saveExportArtifactToDisk({
|
||||
projectPath,
|
||||
relativePath,
|
||||
fallbackFileName: 'taonier.zip',
|
||||
@@ -96,7 +96,7 @@ export function ArtifactsPane({
|
||||
/** 兜底出口:让系统文件管理器定位原件;打不开就把原因留在弹窗里。 */
|
||||
const revealDownloadedFile = (): void => {
|
||||
if (!projectPath || !downloadFailure) return;
|
||||
void revealTaonierFileInManager({
|
||||
void revealExportArtifactInManager({
|
||||
projectPath,
|
||||
relativePath: downloadFailure.relativePath,
|
||||
}).then((result) => {
|
||||
|
||||
+7
-7
@@ -1,11 +1,11 @@
|
||||
import { openUrl } from '@tauri-apps/plugin-opener';
|
||||
import { type ReactNode, useState } from 'react';
|
||||
|
||||
import type { XhsMinitoolExportState } from '../../state/useXhsMinitoolExport';
|
||||
import {
|
||||
revealXhsMinitoolFileInManager,
|
||||
saveXhsMinitoolFileToDisk,
|
||||
} from '../../state/xhsMinitoolDownload';
|
||||
revealExportArtifactInManager,
|
||||
saveExportArtifactToDisk,
|
||||
} from '../../state/exportArtifactDownload';
|
||||
import type { XhsMinitoolExportState } from '../../state/useXhsMinitoolExport';
|
||||
import { XHS_MINITOOL_ARTIFACT_RELATIVE_PATH } from '../../state/xhsMinitoolInstruction';
|
||||
import { PaneButton } from '../common/PaneButton';
|
||||
import { AdaptPromptDialog } from './AdaptPromptDialog';
|
||||
@@ -73,7 +73,7 @@ export function ArtifactsPane({
|
||||
const downloadIcon = (): void => {
|
||||
if (!projectPath || !form) return;
|
||||
const relativePath = form.iconPath.trim();
|
||||
void saveXhsMinitoolFileToDisk({
|
||||
void saveExportArtifactToDisk({
|
||||
projectPath,
|
||||
relativePath,
|
||||
fallbackFileName: 'icon.png',
|
||||
@@ -88,7 +88,7 @@ export function ArtifactsPane({
|
||||
const downloadArtifact = (): void => {
|
||||
if (!projectPath) return;
|
||||
const relativePath = XHS_MINITOOL_ARTIFACT_RELATIVE_PATH;
|
||||
void saveXhsMinitoolFileToDisk({
|
||||
void saveExportArtifactToDisk({
|
||||
projectPath,
|
||||
relativePath,
|
||||
fallbackFileName: 'xhs-minitool.zip',
|
||||
@@ -107,7 +107,7 @@ export function ArtifactsPane({
|
||||
/** 兜底出口:让系统文件管理器定位原件;打不开就把原因留在弹窗里。 */
|
||||
const revealDownloadedFile = (): void => {
|
||||
if (!projectPath || !downloadFailure) return;
|
||||
void revealXhsMinitoolFileInManager({
|
||||
void revealExportArtifactInManager({
|
||||
projectPath,
|
||||
relativePath: downloadFailure.relativePath,
|
||||
}).then((result) => {
|
||||
|
||||
Reference in New Issue
Block a user