From c660aaba3db6df6e6b5edfa135341da07c176446 Mon Sep 17 00:00:00 2001 From: beyondkmp Date: Sun, 22 Jun 2025 10:32:23 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E7=9B=AE=E5=BD=95=E8=BF=81=E7=A7=BB=E7=9A=84bug=20(#7386)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: move initAppDataDir function inline and remove export from utils/file.ts * fix some bugs * fix shouldcopy error * fix: handle appDataPath initialization and update logic in file.ts; update defaultChecked in DataSettings component * fix: improve appDataPath handling and migration logic in file.ts * fix: add error message for selecting the same app data path in DataSettings component and update localization files * fix: ensure migration confirmation modal is shown correctly in DataSettings component * feat: add new IPC channel for retrieving data path from arguments and update related components for migration handling * fix: update app data path validation to check for prefix match in DataSettings component * refactor: simplify data migration logic in DataSettings component by removing unnecessary flag * fix: update initAppDataDir invocation to check for app packaging status in bootstrap.ts --- packages/shared/IpcChannel.ts | 3 + src/main/bootstrap.ts | 5 + src/main/index.ts | 7 +- src/main/ipc.ts | 26 +++- src/main/utils/file.ts | 84 ++++++++---- src/preload/index.ts | 5 +- src/renderer/src/hooks/useAppInit.ts | 8 ++ src/renderer/src/i18n/locales/en-us.json | 9 +- src/renderer/src/i18n/locales/ja-jp.json | 11 +- src/renderer/src/i18n/locales/ru-ru.json | 9 +- src/renderer/src/i18n/locales/zh-cn.json | 9 +- src/renderer/src/i18n/locales/zh-tw.json | 9 +- .../settings/DataSettings/DataSettings.tsx | 129 ++++++++++++++---- 13 files changed, 240 insertions(+), 74 deletions(-) create mode 100644 src/main/bootstrap.ts diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index d02cd15be8..4c6988cf6e 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -20,6 +20,9 @@ export enum IpcChannel { App_Copy = 'app:copy', App_SetStopQuitApp = 'app:set-stop-quit-app', App_SetAppDataPath = 'app:set-app-data-path', + App_GetDataPathFromArgs = 'app:get-data-path-from-args', + App_FlushAppData = 'app:flush-app-data', + App_IsNotEmptyDir = 'app:is-not-empty-dir', App_RelaunchApp = 'app:relaunch-app', App_IsBinaryExist = 'app:is-binary-exist', App_GetBinaryPath = 'app:get-binary-path', diff --git a/src/main/bootstrap.ts b/src/main/bootstrap.ts new file mode 100644 index 0000000000..f682c06fcd --- /dev/null +++ b/src/main/bootstrap.ts @@ -0,0 +1,5 @@ +import { app } from 'electron' + +import { initAppDataDir } from './utils/file' + +app.isPackaged && initAppDataDir() diff --git a/src/main/index.ts b/src/main/index.ts index 102264317a..3699335a90 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,7 +1,11 @@ +// don't reorder this file, it's used to initialize the app data dir and +// other which should be run before the main process is ready +// eslint-disable-next-line +import './bootstrap' + import '@main/config' import { electronApp, optimizer } from '@electron-toolkit/utils' -import { initAppDataDir } from '@main/utils/file' import { replaceDevtoolsFont } from '@main/utils/windowUtil' import { app } from 'electron' import installExtension, { REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS } from 'electron-devtools-installer' @@ -22,7 +26,6 @@ import { registerShortcuts } from './services/ShortcutService' import { TrayService } from './services/TrayService' import { windowService } from './services/WindowService' -initAppDataDir() Logger.initialize() /** diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 1fc398b8c6..5e1ac819a9 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -34,7 +34,7 @@ import { setOpenLinkExternal } from './services/WebviewService' import { windowService } from './services/WindowService' import { calculateDirectorySize, getResourcePath } from './utils' import { decrypt, encrypt } from './utils/aes' -import { getCacheDir, getConfigDir, getFilesDir, hasWritePermission, updateConfig } from './utils/file' +import { getCacheDir, getConfigDir, getFilesDir, hasWritePermission, updateAppDataConfig } from './utils/file' import { compress, decompress } from './utils/zip' const fileManager = new FileStorage() @@ -218,10 +218,28 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { // Set app data path ipcMain.handle(IpcChannel.App_SetAppDataPath, async (_, filePath: string) => { - updateConfig(filePath) + updateAppDataConfig(filePath) app.setPath('userData', filePath) }) + ipcMain.handle(IpcChannel.App_GetDataPathFromArgs, () => { + return process.argv + .slice(1) + .find((arg) => arg.startsWith('--new-data-path=')) + ?.split('--new-data-path=')[1] + }) + + ipcMain.handle(IpcChannel.App_FlushAppData, () => { + BrowserWindow.getAllWindows().forEach((w) => { + w.webContents.session.flushStorageData() + w.webContents.session.cookies.flushStore() + }) + }) + + ipcMain.handle(IpcChannel.App_IsNotEmptyDir, async (_, path: string) => { + return fs.readdirSync(path).length > 0 + }) + // Copy user data to new location ipcMain.handle(IpcChannel.App_Copy, async (_, oldPath: string, newPath: string) => { try { @@ -234,8 +252,8 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { }) // Relaunch app - ipcMain.handle(IpcChannel.App_RelaunchApp, () => { - app.relaunch() + ipcMain.handle(IpcChannel.App_RelaunchApp, (_, options?: Electron.RelaunchOptions) => { + app.relaunch(options) app.exit(0) }) diff --git a/src/main/utils/file.ts b/src/main/utils/file.ts index 737dcee0ba..177a28a90f 100644 --- a/src/main/utils/file.ts +++ b/src/main/utils/file.ts @@ -8,6 +8,20 @@ import { FileType, FileTypes } from '@types' import { app } from 'electron' import { v4 as uuidv4 } from 'uuid' +export function initAppDataDir() { + const appDataPath = getAppDataPathFromConfig() + if (appDataPath) { + app.setPath('userData', appDataPath) + return + } + + if (isPortable) { + const portableDir = process.env.PORTABLE_EXECUTABLE_DIR + app.setPath('userData', path.join(portableDir || app.getPath('exe'), 'data')) + return + } +} + // 创建文件类型映射表,提高查找效率 const fileTypeMap = new Map() @@ -35,46 +49,70 @@ export function hasWritePermission(path: string) { function getAppDataPathFromConfig() { try { const configPath = path.join(getConfigDir(), 'config.json') - if (fs.existsSync(configPath)) { - const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')) - if (config.appDataPath && fs.existsSync(config.appDataPath) && hasWritePermission(config.appDataPath)) { - return config.appDataPath - } + if (!fs.existsSync(configPath)) { + return null } + + const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')) + + if (!config.appDataPath) { + return null + } + + let appDataPath = null + // 兼容旧版本 + if (config.appDataPath && typeof config.appDataPath === 'string') { + appDataPath = config.appDataPath + // 将旧版本数据迁移到新版本 + appDataPath && updateAppDataConfig(appDataPath) + } else { + appDataPath = config.appDataPath.find( + (item: { executablePath: string }) => item.executablePath === app.getPath('exe') + )?.dataPath + } + + if (appDataPath && fs.existsSync(appDataPath) && hasWritePermission(appDataPath)) { + return appDataPath + } + + return null } catch (error) { return null } - return null } -export function initAppDataDir() { - const appDataPath = getAppDataPathFromConfig() - if (appDataPath) { - app.setPath('userData', appDataPath) - return - } - - if (isPortable) { - const portableDir = process.env.PORTABLE_EXECUTABLE_DIR - app.setPath('userData', path.join(portableDir || app.getPath('exe'), 'data')) - return - } -} - -export function updateConfig(appDataPath: string) { +export function updateAppDataConfig(appDataPath: string) { const configDir = getConfigDir() if (!fs.existsSync(configDir)) { fs.mkdirSync(configDir, { recursive: true }) } + // config.json + // appDataPath: [{ executablePath: string, dataPath: string }] const configPath = path.join(getConfigDir(), 'config.json') if (!fs.existsSync(configPath)) { - fs.writeFileSync(configPath, JSON.stringify({ appDataPath }, null, 2)) + fs.writeFileSync( + configPath, + JSON.stringify({ appDataPath: [{ executablePath: app.getPath('exe'), dataPath: appDataPath }] }, null, 2) + ) return } const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')) - config.appDataPath = appDataPath + if (!config.appDataPath || (config.appDataPath && typeof config.appDataPath !== 'object')) { + config.appDataPath = [] + } + + const existingPath = config.appDataPath.find( + (item: { executablePath: string }) => item.executablePath === app.getPath('exe') + ) + + if (existingPath) { + existingPath.dataPath = appDataPath + } else { + config.appDataPath.push({ executablePath: app.getPath('exe'), dataPath: appDataPath }) + } + fs.writeFileSync(configPath, JSON.stringify(config, null, 2)) } diff --git a/src/preload/index.ts b/src/preload/index.ts index 2a8ac3df89..5138d4e4de 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -29,9 +29,12 @@ const api = { select: (options: Electron.OpenDialogOptions) => ipcRenderer.invoke(IpcChannel.App_Select, options), hasWritePermission: (path: string) => ipcRenderer.invoke(IpcChannel.App_HasWritePermission, path), setAppDataPath: (path: string) => ipcRenderer.invoke(IpcChannel.App_SetAppDataPath, path), + getDataPathFromArgs: () => ipcRenderer.invoke(IpcChannel.App_GetDataPathFromArgs), copy: (oldPath: string, newPath: string) => ipcRenderer.invoke(IpcChannel.App_Copy, oldPath, newPath), setStopQuitApp: (stop: boolean, reason: string) => ipcRenderer.invoke(IpcChannel.App_SetStopQuitApp, stop, reason), - relaunchApp: () => ipcRenderer.invoke(IpcChannel.App_RelaunchApp), + flushAppData: () => ipcRenderer.invoke(IpcChannel.App_FlushAppData), + isNotEmptyDir: (path: string) => ipcRenderer.invoke(IpcChannel.App_IsNotEmptyDir, path), + relaunchApp: (options?: Electron.RelaunchOptions) => ipcRenderer.invoke(IpcChannel.App_RelaunchApp, options), openWebsite: (url: string) => ipcRenderer.invoke(IpcChannel.Open_Website, url), getCacheSize: () => ipcRenderer.invoke(IpcChannel.App_GetCacheSize), clearCache: () => ipcRenderer.invoke(IpcChannel.App_ClearCache), diff --git a/src/renderer/src/hooks/useAppInit.ts b/src/renderer/src/hooks/useAppInit.ts index 531582708d..8b5fe0ade6 100644 --- a/src/renderer/src/hooks/useAppInit.ts +++ b/src/renderer/src/hooks/useAppInit.ts @@ -30,6 +30,14 @@ export function useAppInit() { console.timeEnd('init') }, []) + useEffect(() => { + window.api.getDataPathFromArgs().then((dataPath) => { + if (dataPath) { + window.navigate('/settings/data', { replace: true }) + } + }) + }, []) + useUpdateHandler() useFullScreenNotice() diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index b62c708825..b8bcfb66b2 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -1087,10 +1087,10 @@ "app_data": "App Data", "app_data.select": "Modify Directory", "app_data.select_title": "Change App Data Directory", - "app_data.restart_notice": "The app will need to restart to apply the changes", - "app_data.copy_data_option": "Copy data from original directory to new directory", + "app_data.restart_notice": "The app may need to restart multiple times to apply the changes", + "app_data.copy_data_option": "Copy data, will automatically restart after copying the original directory data to the new directory", "app_data.copy_time_notice": "Copying data may take a while, do not force quit app", - "app_data.path_changed_without_copy": "Path changed successfully, but data not copied", + "app_data.path_changed_without_copy": "Path changed successfully", "app_data.copying_warning": "Data copying, do not force quit app", "app_data.copying": "Copying data to new location...", "app_data.copy_success": "Successfully copied data to new location", @@ -1103,6 +1103,9 @@ "app_data.select_error_root_path": "New path cannot be the root path", "app_data.select_error_write_permission": "New path does not have write permission", "app_data.stop_quit_app_reason": "The app is currently migrating data and cannot be exited", + "app_data.select_not_empty_dir": "New path is not empty", + "app_data.select_not_empty_dir_content": "New path is not empty, if you select copy, it will overwrite the data in the new path, there is a risk of data loss, continue?", + "app_data.select_error_same_path": "New path is the same as the old path, please select another path", "app_knowledge": "Knowledge Base Files", "app_knowledge.button.delete": "Delete File", "app_knowledge.remove_all": "Remove Knowledge Base Files", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index bc363a0e91..430cba8351 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -1085,10 +1085,10 @@ "app_data": "アプリデータ", "app_data.select": "ディレクトリを変更", "app_data.select_title": "アプリデータディレクトリの変更", - "app_data.restart_notice": "変更を適用するには、アプリを再起動する必要があります", - "app_data.copy_data_option": "データをコピーする, 開くと元のディレクトリのデータが新しいディレクトリにコピーされます", - "app_data.copy_time_notice": "データコピーには時間がかかります。アプリを強制終了しないでください", - "app_data.path_changed_without_copy": "パスが変更されましたが、データがコピーされていません", + "app_data.restart_notice": "変更を適用するには、アプリを再起動する必要があります。", + "app_data.copy_data_option": "データをコピーする, 開くと元のディレクトリのデータが新しいディレクトリにコピーされます。", + "app_data.copy_time_notice": "データコピーには時間がかかります。アプリを強制終了しないでください。", + "app_data.path_changed_without_copy": "パスが変更されました。", "app_data.copying_warning": "データコピー中、アプリを強制終了しないでください", "app_data.copying": "新しい場所にデータをコピーしています...", "app_data.copy_success": "データを新しい場所に正常にコピーしました", @@ -1101,6 +1101,9 @@ "app_data.select_error_root_path": "新しいパスはルートパスにできません", "app_data.select_error_write_permission": "新しいパスに書き込み権限がありません", "app_data.stop_quit_app_reason": "アプリは現在データを移行しているため、終了できません", + "app_data.select_not_empty_dir": "新しいパスは空ではありません", + "app_data.select_not_empty_dir_content": "新しいパスは空ではありません。コピーを選択すると、新しいパスのデータが上書きされます。データが失われるリスクがあります。続行しますか?", + "app_data.select_error_same_path": "新しいパスは元のパスと同じです。別のパスを選択してください", "app_knowledge": "知識ベースファイル", "app_knowledge.button.delete": "ファイルを削除", "app_knowledge.remove_all": "ナレッジベースファイルを削除", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index 8b325b4b31..af26cddf92 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -1085,10 +1085,10 @@ "app_data": "Данные приложения", "app_data.select": "Изменить директорию", "app_data.select_title": "Изменить директорию данных приложения", - "app_data.restart_notice": "Для применения изменений потребуется перезапуск приложения", - "app_data.copy_data_option": "Копировать данные из исходной директории в новую директорию", + "app_data.restart_notice": "Для применения изменений может потребоваться несколько перезапусков приложения", + "app_data.copy_data_option": "Копировать данные, будет автоматически перезапущено после копирования данных из исходной директории в новую директорию", "app_data.copy_time_notice": "Копирование данных из исходной директории займет некоторое время, пожалуйста, будьте терпеливы", - "app_data.path_changed_without_copy": "Путь изменен успешно, но данные не скопированы", + "app_data.path_changed_without_copy": "Путь изменен успешно", "app_data.copying_warning": "Копирование данных, нельзя взаимодействовать с приложением, не закрывайте приложение", "app_data.copying": "Копирование данных в новое место...", "app_data.copy_success": "Данные успешно скопированы в новое место", @@ -1101,6 +1101,9 @@ "app_data.select_error_root_path": "Новый путь не может быть корневым", "app_data.select_error_write_permission": "Новый путь не имеет разрешения на запись", "app_data.stop_quit_app_reason": "Приложение в настоящее время перемещает данные и не может быть закрыто", + "app_data.select_not_empty_dir": "Новый путь не пуст", + "app_data.select_not_empty_dir_content": "Новый путь не пуст, если вы выбираете копирование, он перезапишет данные в новом пути, есть риск потери данных, продолжить?", + "app_data.select_error_same_path": "Новый путь совпадает с исходным путем, пожалуйста, выберите другой путь", "app_knowledge": "Файлы базы знаний", "app_knowledge.button.delete": "Удалить файл", "app_knowledge.remove_all": "Удалить файлы базы знаний", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index a60589c71a..fd393e36ce 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -1087,10 +1087,10 @@ "app_data": "应用数据", "app_data.select": "修改目录", "app_data.select_title": "更改应用数据目录", - "app_data.restart_notice": "应用需要重启以应用更改", - "app_data.copy_data_option": "复制数据,开启后会将原始目录数据复制到新目录", + "app_data.restart_notice": "应用可能会重启多次以应用更改", + "app_data.copy_data_option": "复制数据,会自动重启后将原始目录数据复制到新目录", "app_data.copy_time_notice": "复制数据将需要一些时间,复制期间不要关闭应用", - "app_data.path_changed_without_copy": "路径已更改成功,但数据未复制", + "app_data.path_changed_without_copy": "路径已更改成功", "app_data.copying_warning": "数据复制中,不要强制退出app", "app_data.copying": "正在将数据复制到新位置...", "app_data.copy_success": "已成功复制数据到新位置", @@ -1103,6 +1103,9 @@ "app_data.select_error_root_path": "新路径不能是根路径", "app_data.select_error_write_permission": "新路径没有写入权限", "app_data.stop_quit_app_reason": "应用目前在迁移数据, 不能退出", + "app_data.select_not_empty_dir": "新路径不为空", + "app_data.select_not_empty_dir_content": "新路径不为空,选择复制将覆盖新路径中的数据, 有数据丢失的风险,是否继续?", + "app_data.select_error_same_path": "新路径与旧路径相同,请选择其他路径", "app_knowledge": "知识库文件", "app_knowledge.button.delete": "删除文件", "app_knowledge.remove_all": "删除知识库文件", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 4742cff3a3..995bee24bc 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -1087,10 +1087,10 @@ "app_data": "應用數據", "app_data.select": "修改目錄", "app_data.select_title": "變更應用數據目錄", - "app_data.restart_notice": "變更數據目錄後需要重啟應用才能生效", - "app_data.copy_data_option": "複製數據, 開啟後會將原始目錄數據複製到新目錄", + "app_data.restart_notice": "變更數據目錄後可能需要重啟應用才能生效", + "app_data.copy_data_option": "複製數據, 會自動重啟後將原始目錄數據複製到新目錄", "app_data.copy_time_notice": "複製數據將需要一些時間,複製期間不要關閉應用", - "app_data.path_changed_without_copy": "路徑已變更成功,但數據未複製", + "app_data.path_changed_without_copy": "路徑已變更成功", "app_data.copying_warning": "數據複製中,不要強制退出應用", "app_data.copying": "正在複製數據到新位置...", "app_data.copy_success": "成功複製數據到新位置", @@ -1103,6 +1103,9 @@ "app_data.select_error_root_path": "新路徑不能是根路徑", "app_data.select_error_write_permission": "新路徑沒有寫入權限", "app_data.stop_quit_app_reason": "應用目前正在遷移數據,不能退出", + "app_data.select_not_empty_dir": "新路徑不為空", + "app_data.select_not_empty_dir_content": "新路徑不為空,選擇複製將覆蓋新路徑中的數據, 有數據丟失的風險,是否繼續?", + "app_data.select_error_same_path": "新路徑與舊路徑相同,請選擇其他路徑", "app_knowledge": "知識庫文件", "app_knowledge.button.delete": "刪除檔案", "app_knowledge.remove_all": "刪除知識庫檔案", diff --git a/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx b/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx index 5a80545345..52b1c0bbb7 100644 --- a/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx +++ b/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx @@ -202,6 +202,12 @@ const DataSettings: FC = () => { return } + // check new app data path is same as old app data path + if (newAppDataPath.startsWith(appInfo!.appDataPath)) { + window.message.error(t('settings.data.app_data.select_error_same_path')) + return + } + // check new app data path has write permission const hasWritePermission = await window.api.hasWritePermission(newAppDataPath) if (!hasWritePermission) { @@ -213,22 +219,34 @@ const DataSettings: FC = () => {
{t('settings.data.app_data.migration_title')}
) const migrationClassName = 'migration-modal' - const messageKey = 'data-migration' - // 显示确认对话框 - showMigrationConfirmModal(appInfo.appDataPath, newAppDataPath, migrationTitle, migrationClassName, messageKey) + if (await window.api.isNotEmptyDir(newAppDataPath)) { + const modal = window.modal.confirm({ + title: t('settings.data.app_data.select_not_empty_dir'), + content: t('settings.data.app_data.select_not_empty_dir_content'), + centered: true, + okText: t('common.confirm'), + cancelText: t('common.cancel'), + onOk: () => { + modal.destroy() + // 显示确认对话框 + showMigrationConfirmModal(appInfo.appDataPath, newAppDataPath, migrationTitle, migrationClassName) + } + }) + return + } + showMigrationConfirmModal(appInfo.appDataPath, newAppDataPath, migrationTitle, migrationClassName) } // 显示确认迁移的对话框 - const showMigrationConfirmModal = ( + const showMigrationConfirmModal = async ( originalPath: string, newPath: string, title: React.ReactNode, - className: string, - messageKey: string + className: string ) => { // 复制数据选项状态 - let shouldCopyData = true + let shouldCopyData = !(await window.api.isNotEmptyDir(newPath)) // 创建路径内容组件 const PathsContent = () => ( @@ -248,7 +266,7 @@ const DataSettings: FC = () => {
{ shouldCopyData = checked }} @@ -290,22 +308,17 @@ const DataSettings: FC = () => { // 立即关闭确认对话框 modal.destroy() - // 设置停止退出应用 - window.api.setStopQuitApp(true, t('settings.data.app_data.stop_quit_app_reason')) - if (shouldCopyData) { // 如果选择复制数据,显示进度模态框并执行迁移 - const { loadingModal, progressInterval, updateProgress } = showProgressModal(title, className, PathsContent) - - try { - await startMigration(originalPath, newPath, progressInterval, updateProgress, loadingModal, messageKey) - } catch (error) { - if (progressInterval) { - clearInterval(progressInterval) - } - loadingModal.destroy() - throw error - } + window.message.info({ + content: t('settings.data.app_data.restart_notice'), + duration: 3 + }) + setTimeout(() => { + window.api.relaunchApp({ + args: ['--new-data-path=' + newPath] + }) + }, 300) } else { // 如果不复制数据,直接设置新的应用数据路径 await window.api.setAppDataPath(newPath) @@ -324,12 +337,7 @@ const DataSettings: FC = () => { } catch (error) { window.api.setStopQuitApp(false, '') window.message.error({ - content: - (shouldCopyData - ? t('settings.data.app_data.copy_failed') - : t('settings.data.app_data.path_change_failed')) + - ': ' + - error, + content: t('settings.data.app_data.path_change_failed') + ': ' + error, duration: 5 }) } @@ -337,6 +345,68 @@ const DataSettings: FC = () => { }) } + useEffect(() => { + const handleDataMigration = async () => { + const newDataPath = await window.api.getDataPathFromArgs() + if (!newDataPath) return + + const originalPath = (await window.api.getAppInfo())?.appDataPath + if (!originalPath) return + + const title = ( +
{t('settings.data.app_data.migration_title')}
+ ) + const className = 'migration-modal' + const messageKey = 'data-migration' + + // Create PathsContent component for this specific migration + const PathsContent = () => ( +
+ + {t('settings.data.app_data.original_path')}: + {originalPath} + + + {t('settings.data.app_data.new_path')}: + {newDataPath} + +
+ ) + + const { loadingModal, progressInterval, updateProgress } = showProgressModal(title, className, PathsContent) + try { + window.api.setStopQuitApp(true, t('settings.data.app_data.stop_quit_app_reason')) + await startMigration(originalPath, newDataPath, progressInterval, updateProgress, loadingModal, messageKey) + + // 更新应用数据路径 + setAppInfo(await window.api.getAppInfo()) + + // 通知用户并重启应用 + setTimeout(() => { + window.message.success(t('settings.data.app_data.select_success')) + window.api.setStopQuitApp(false, '') + window.api.relaunchApp({ + args: ['--user-data-dir=' + newDataPath] + }) + }, 1000) + } catch (error) { + window.api.setStopQuitApp(false, '') + window.message.error({ + content: t('settings.data.app_data.copy_failed') + ': ' + error, + key: messageKey, + duration: 5 + }) + } finally { + if (progressInterval) { + clearInterval(progressInterval) + } + loadingModal.destroy() + } + } + + handleDataMigration() + }, []) + // 显示进度模态框 const showProgressModal = (title: React.ReactNode, className: string, PathsContent: React.FC) => { let currentProgress = 0 @@ -411,6 +481,9 @@ const DataSettings: FC = () => { loadingModal: { destroy: () => void }, messageKey: string ): Promise => { + // flush app data + await window.api.flushAppData() + // 开始复制过程 const copyResult = await window.api.copy(originalPath, newPath)