feat: Add app data path selection and relaunch functionality (#6096)

* feat: Add app data path selection and relaunch functionality

* Introduced new IPC channels for selecting and setting the app data path.
* Implemented logic to initialize the app data path on startup.
* Added confirmation modal for changing the app data directory in the settings.
* Updated translations for new app data path features in multiple languages.

* feat: Implement user data copying and app data path management

* Added IPC channels for copying user data to a new location and setting the app data path.
* Enhanced the user interface to support data copying with progress notifications.
* Updated translations to reflect new features related to app data management.
* Refactored file utility functions to streamline data path handling.

* refactor: update IPC channel names and streamline app data path handling

- Renamed IPC channels for selecting app data path and copying user data for clarity.
- Simplified the logic for selecting and setting app data paths, removing unnecessary success/error handling.
- Updated related functions and components to reflect the new IPC channel names and improved data handling.
- Removed unused copyUserDataToNewLocation function to clean up the codebase.

* fix: update app data directory selection text in multiple locales

- Changed the text for selecting the app data directory from "Select Directory" to "Modify Directory" in English, Japanese, Russian, Simplified Chinese, and Traditional Chinese locales to better reflect the action being performed.

* refactor: remove redundant success messages in DataSettings component

- Eliminated unnecessary success messages related to app data copying and app relaunching to streamline user feedback and improve code clarity.

* refactor: streamline file utility functions and update app data initialization

- Moved `getDataPath` function to the `utils/index.ts` for better organization and accessibility.
- Renamed `initUserDataDir` to `initAppDataDir` for clarity in its purpose.
- Removed commented-out code in `ConfigManager` to enhance code cleanliness.

* refactor: update import paths and localization strings for app data

- Refactored import statements for `getDataPath` to streamline utility access.
- Updated localization strings for app data in English, Japanese, Russian, Simplified Chinese, and Traditional Chinese to enhance clarity and consistency.

* update i18n

* add fc

* fix: handle errors in app data path retrieval

- Added error handling to the `getAppDataPathFromConfig` function to return null if the configuration file cannot be read or parsed, improving robustness.

* refactor: simplify app data path handling in IPC

- Removed error handling for setting the app data path in the IPC channel, streamlining the process by directly updating the configuration and user data path without try-catch blocks.

* fix: update userData path handling for portable applications

- Modified the initAppDataDir function to set the userData path based on the PORTABLE_EXECUTABLE_DIR environment variable, ensuring compatibility with portable application setups.

* feat: enhance app data path migration with progress indication

- Implemented a loading modal with progress tracking during the app data path migration process.
- Added visual feedback using a progress bar to inform users of the copying status.
- Improved error handling and user notifications for successful and failed migrations.
- Refactored the modal confirmation logic to streamline user interactions during the path selection and migration process.

* feat: add migration paths and update UI for data migration process

- Introduced new translation keys for migration paths in Japanese, Russian, Simplified Chinese, and Traditional Chinese.
- Enhanced the DataSettings component with a structured layout for displaying original and new paths during data migration.
- Updated modal titles and content to improve user experience during the migration process.

* feat: enhance data migration process with improved UI and progress tracking

- Refactored the DataSettings component to streamline the data migration workflow.
- Added a new function to display progress during the migration process, enhancing user feedback.
- Updated modal logic to improve clarity and user experience when selecting new app data paths.
- Implemented error handling and notifications for successful and failed migrations.

* feat: add stop quit app functionality during data migration

- Introduced a new IPC channel to manage the application's quit behavior during data transfer.
- Updated the DataSettings component to prevent the app from quitting while migration is in progress, enhancing user experience.
- Improved modal configurations for better responsiveness and visual appeal.

* feat: enhance app data path handling and localization updates

- Updated IPC handler to use 'filePath' for clarity in app data path management.
- Improved validation to ensure the new app data path is not the root path, enhancing user feedback during path selection.
- Added new translation keys for error messages related to app data path selection in English, Japanese, Russian, Simplified Chinese, and Traditional Chinese, improving localization support.

* feat: add write permission check and enhance quit prevention during data migration

- Introduced a new IPC channel to check write permissions for the app data path.
- Updated the DataSettings component to validate write permissions before proceeding with data migration.
- Enhanced the quit prevention logic to include a reason for blocking the app from quitting during data transfer.
- Added new localization keys for error messages related to write permissions in multiple languages, improving user feedback.

* feat: enhance confirmation modal in DataSettings component

- Updated the confirmation modal to include danger styling for the OK button, improving visual feedback.
- Added localized text for the OK and Cancel buttons, enhancing user experience through better accessibility.

* feat: add localization keys and improve quit prevention during data migration

- Added new localization keys for data migration, including titles and original path labels, enhancing user experience.
- Updated the DataSettings component to ensure the app does not quit during data migration, improving reliability and user feedback.

* feat(DataSettings): add data copying option and update related messages

- Introduced a switch to allow users to choose whether to copy data from the original directory when changing the app data path.
- Updated user notifications and progress messages to reflect the new functionality, including warnings about data copying.
- Enhanced localization files for multiple languages to include new strings related to data copying options and notifications.

* fix(DataSettings): set default for data copying option to true

- Updated the DataSettings component to set the default state of the data copying option to true.
- Added a new CopyDataContent component to enhance the user interface by displaying the data copying option alongside the existing path settings.
- Improved layout by ensuring proper spacing and alignment for better user experience.

---------

Co-authored-by: beyondkmp <beyondkmkp@gmail.com>
This commit is contained in:
beyondkmp 2025-06-18 17:39:26 +08:00 committed by GitHub
parent 9e8f14c9d3
commit d409ac1b73
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 547 additions and 25 deletions

View File

@ -15,7 +15,12 @@ export enum IpcChannel {
App_SetAutoUpdate = 'app:set-auto-update',
App_SetFeedUrl = 'app:set-feed-url',
App_HandleZoomFactor = 'app:handle-zoom-factor',
App_Select = 'app:select',
App_HasWritePermission = 'app:has-write-permission',
App_Copy = 'app:copy',
App_SetStopQuitApp = 'app:set-stop-quit-app',
App_SetAppDataPath = 'app:set-app-data-path',
App_RelaunchApp = 'app:relaunch-app',
App_IsBinaryExist = 'app:is-binary-exist',
App_GetBinaryPath = 'app:get-binary-path',
App_InstallUvBinary = 'app:install-uv-binary',

View File

@ -1,7 +1,6 @@
import { app } from 'electron'
import { getDataPath } from './utils'
const isDev = process.env.NODE_ENV === 'development'
if (isDev) {

View File

@ -1,6 +1,7 @@
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'
@ -20,8 +21,8 @@ import selectionService, { initSelectionService } from './services/SelectionServ
import { registerShortcuts } from './services/ShortcutService'
import { TrayService } from './services/TrayService'
import { windowService } from './services/WindowService'
import { setUserDataDir } from './utils/file'
initAppDataDir()
Logger.initialize()
/**
@ -72,9 +73,6 @@ if (!app.requestSingleInstanceLock()) {
app.quit()
process.exit(0)
} else {
// Portable dir must be setup before app ready
setUserDataDir()
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.

View File

@ -7,7 +7,7 @@ import { handleZoomFactor } from '@main/utils/zoom'
import { FeedUrl } from '@shared/config/constant'
import { IpcChannel } from '@shared/IpcChannel'
import { Shortcut, ThemeMode } from '@types'
import { BrowserWindow, ipcMain, session, shell } from 'electron'
import { dialog, BrowserWindow, ipcMain, session, shell } from 'electron'
import log from 'electron-log'
import { Notification } from 'src/renderer/src/types/notification'
@ -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 } from './utils/file'
import { getCacheDir, getConfigDir, getFilesDir, hasWritePermission, updateConfig } from './utils/file'
import { compress, decompress } from './utils/zip'
const fileManager = new FileStorage()
@ -175,6 +175,70 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
}
})
let preventQuitListener: ((event: Electron.Event) => void) | null = null
ipcMain.handle(IpcChannel.App_SetStopQuitApp, (_, stop: boolean = false, reason: string = '') => {
if (stop) {
// Only add listener if not already added
if (!preventQuitListener) {
preventQuitListener = (event: Electron.Event) => {
event.preventDefault()
notificationService.sendNotification({
title: reason,
message: reason
} as Notification)
}
app.on('before-quit', preventQuitListener)
}
} else {
// Remove listener if it exists
if (preventQuitListener) {
app.removeListener('before-quit', preventQuitListener)
preventQuitListener = null
}
}
})
// Select app data path
ipcMain.handle(IpcChannel.App_Select, async (_, options: Electron.OpenDialogOptions) => {
try {
const { canceled, filePaths } = await dialog.showOpenDialog(options)
if (canceled || filePaths.length === 0) {
return null
}
return filePaths[0]
} catch (error: any) {
log.error('Failed to select app data path:', error)
return null
}
})
ipcMain.handle(IpcChannel.App_HasWritePermission, async (_, filePath: string) => {
return hasWritePermission(filePath)
})
// Set app data path
ipcMain.handle(IpcChannel.App_SetAppDataPath, async (_, filePath: string) => {
updateConfig(filePath)
app.setPath('userData', filePath)
})
// Copy user data to new location
ipcMain.handle(IpcChannel.App_Copy, async (_, oldPath: string, newPath: string) => {
try {
await fs.promises.cp(oldPath, newPath, { recursive: true })
return { success: true }
} catch (error: any) {
log.error('Failed to copy user data:', error)
return { success: false, error: error.message }
}
})
// Relaunch app
ipcMain.handle(IpcChannel.App_RelaunchApp, () => {
app.relaunch()
app.exit(0)
})
// check for update
ipcMain.handle(IpcChannel.App_CheckForUpdate, async () => {
return await appUpdater.checkForUpdates()

View File

@ -9,6 +9,7 @@ import StreamZip from 'node-stream-zip'
import * as path from 'path'
import { CreateDirectoryOptions, FileStat } from 'webdav'
import { getDataPath } from '../utils'
import WebDav from './WebDav'
import { windowService } from './WindowService'
@ -253,7 +254,7 @@ class BackupManager {
Logger.log('[backup] step 3: restore Data directory')
// 恢复 Data 目录
const sourcePath = path.join(this.tempDir, 'Data')
const destPath = path.join(app.getPath('userData'), 'Data')
const destPath = getDataPath()
const dataExists = await fs.pathExists(sourcePath)
const dataFiles = dataExists ? await fs.readdir(sourcePath) : []

View File

@ -25,12 +25,12 @@ import Embeddings from '@main/embeddings/Embeddings'
import { addFileLoader } from '@main/loader'
import Reranker from '@main/reranker/Reranker'
import { windowService } from '@main/services/WindowService'
import { getDataPath } from '@main/utils'
import { getAllFiles } from '@main/utils/file'
import { MB } from '@shared/config/constant'
import type { LoaderReturn } from '@shared/config/types'
import { IpcChannel } from '@shared/IpcChannel'
import { FileType, KnowledgeBaseParams, KnowledgeItem } from '@types'
import { app } from 'electron'
import Logger from 'electron-log'
import { v4 as uuidv4 } from 'uuid'
@ -88,7 +88,7 @@ const loaderTaskIntoOfSet = (loaderTask: LoaderTask): LoaderTaskOfSet => {
}
class KnowledgeService {
private storageDir = path.join(app.getPath('userData'), 'Data', 'KnowledgeBase')
private storageDir = path.join(getDataPath(), 'KnowledgeBase')
// Byte based
private workload = 0
private processingItemCount = 0

View File

@ -2,7 +2,7 @@ import * as fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { isMac } from '@main/constant'
import { isPortable } from '@main/constant'
import { audioExts, documentExts, imageExts, textExts, videoExts } from '@shared/config/constant'
import { FileType, FileTypes } from '@types'
import { app } from 'electron'
@ -23,6 +23,61 @@ function initFileTypeMap() {
// 初始化映射表
initFileTypeMap()
export function hasWritePermission(path: string) {
try {
fs.accessSync(path, fs.constants.W_OK)
return true
} catch (error) {
return false
}
}
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
}
}
} 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) {
const configDir = getConfigDir()
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true })
}
const configPath = path.join(getConfigDir(), 'config.json')
if (!fs.existsSync(configPath)) {
fs.writeFileSync(configPath, JSON.stringify({ appDataPath }, null, 2))
return
}
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
config.appDataPath = appDataPath
fs.writeFileSync(configPath, JSON.stringify(config, null, 2))
}
export function getFileType(ext: string): FileTypes {
ext = ext.toLowerCase()
return fileTypeMap.get(ext) || FileTypes.OTHER
@ -88,12 +143,3 @@ export function getCacheDir() {
export function getAppConfigDir(name: string) {
return path.join(getConfigDir(), name)
}
export function setUserDataDir() {
if (!isMac) {
const dir = path.join(path.dirname(app.getPath('exe')), 'data')
if (fs.existsSync(dir) && fs.statSync(dir).isDirectory()) {
app.setPath('userData', dir)
}
}
}

View File

@ -26,6 +26,12 @@ const api = {
handleZoomFactor: (delta: number, reset: boolean = false) =>
ipcRenderer.invoke(IpcChannel.App_HandleZoomFactor, delta, reset),
setAutoUpdate: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetAutoUpdate, isActive),
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),
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),
openWebsite: (url: string) => ipcRenderer.invoke(IpcChannel.Open_Website, url),
getCacheSize: () => ipcRenderer.invoke(IpcChannel.App_GetCacheSize),
clearCache: () => ipcRenderer.invoke(IpcChannel.App_ClearCache),

View File

@ -1084,6 +1084,24 @@
"assistant.title": "Default Assistant",
"data": {
"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.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.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",
"app_data.copy_failed": "Failed to copy data",
"app_data.select_success": "Data directory changed, the app will restart to apply changes",
"app_data.select_error": "Failed to change data directory",
"app_data.migration_title": "Data Migration",
"app_data.original_path": "Original Path",
"app_data.new_path": "New Path",
"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_knowledge": "Knowledge Base Files",
"app_knowledge.button.delete": "Delete File",
"app_knowledge.remove_all": "Remove Knowledge Base Files",

View File

@ -1081,7 +1081,25 @@
"assistant.title": "デフォルトアシスタント",
"data": {
"app_data": "アプリデータ",
"app_knowledge": "ナレッジベースファイル",
"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.copying_warning": "データコピー中、アプリを強制終了しないでください",
"app_data.copying": "新しい場所にデータをコピーしています...",
"app_data.copy_success": "データを新しい場所に正常にコピーしました",
"app_data.copy_failed": "データのコピーに失敗しました",
"app_data.select_success": "データディレクトリが変更されました。変更を適用するためにアプリが再起動します",
"app_data.select_error": "データディレクトリの変更に失敗しました",
"app_data.migration_title": "データ移行",
"app_data.original_path": "元のパス",
"app_data.new_path": "新しいパス",
"app_data.select_error_root_path": "新しいパスはルートパスにできません",
"app_data.select_error_write_permission": "新しいパスに書き込み権限がありません",
"app_data.stop_quit_app_reason": "アプリは現在データを移行しているため、終了できません",
"app_knowledge": "知識ベースファイル",
"app_knowledge.button.delete": "ファイルを削除",
"app_knowledge.remove_all": "ナレッジベースファイルを削除",
"app_knowledge.remove_all_confirm": "ナレッジベースファイルを削除すると、ナレッジベース自体は削除されません。これにより、ストレージ容量を節約できます。続行しますか?",

View File

@ -1092,7 +1092,25 @@
"assistant.title": "Ассистент по умолчанию",
"data": {
"app_data": "Данные приложения",
"app_knowledge": "База знаний",
"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.copying_warning": "Копирование данных, нельзя взаимодействовать с приложением, не закрывайте приложение",
"app_data.copying": "Копирование данных в новое место...",
"app_data.copy_success": "Данные успешно скопированы в новое место",
"app_data.copy_failed": "Не удалось скопировать данные",
"app_data.select_success": "Директория данных изменена, приложение будет перезапущено для применения изменений",
"app_data.select_error": "Не удалось изменить директорию данных",
"app_data.migration_title": "Миграция данных",
"app_data.original_path": "Исходный путь",
"app_data.new_path": "Новый путь",
"app_data.select_error_root_path": "Новый путь не может быть корневым",
"app_data.select_error_write_permission": "Новый путь не имеет разрешения на запись",
"app_data.stop_quit_app_reason": "Приложение в настоящее время перемещает данные и не может быть закрыто",
"app_knowledge": "Файлы базы знаний",
"app_knowledge.button.delete": "Удалить файл",
"app_knowledge.remove_all": "Удалить файлы базы знаний",
"app_knowledge.remove_all_confirm": "Удаление файлов базы знаний не удалит саму базу знаний, что позволит уменьшить занимаемый объем памяти, продолжить?",

View File

@ -1084,6 +1084,24 @@
"assistant.title": "默认助手",
"data": {
"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.copying_warning": "数据复制中不要强制退出app",
"app_data.copying": "正在将数据复制到新位置...",
"app_data.copy_success": "已成功复制数据到新位置",
"app_data.copy_failed": "复制数据失败",
"app_data.select_success": "数据目录已更改,应用将重启以应用更改",
"app_data.select_error": "更改数据目录失败",
"app_data.migration_title": "数据迁移",
"app_data.original_path": "原始路径",
"app_data.new_path": "新路径",
"app_data.select_error_root_path": "新路径不能是根路径",
"app_data.select_error_write_permission": "新路径没有写入权限",
"app_data.stop_quit_app_reason": "应用目前在迁移数据, 不能退出",
"app_knowledge": "知识库文件",
"app_knowledge.button.delete": "删除文件",
"app_knowledge.remove_all": "删除知识库文件",

View File

@ -1082,7 +1082,25 @@
"assistant.icon.type.none": "不顯示",
"assistant.title": "預設助手",
"data": {
"app_data": "應用程式資料",
"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.copying_warning": "數據複製中,不要強制退出應用",
"app_data.copying": "正在複製數據到新位置...",
"app_data.copy_success": "成功複製數據到新位置",
"app_data.copy_failed": "複製數據失敗",
"app_data.select_success": "數據目錄已變更,應用將重啟以應用變更",
"app_data.select_error": "變更數據目錄失敗",
"app_data.migration_title": "數據遷移",
"app_data.original_path": "原始路徑",
"app_data.new_path": "新路徑",
"app_data.select_error_root_path": "新路徑不能是根路徑",
"app_data.select_error_write_permission": "新路徑沒有寫入權限",
"app_data.stop_quit_app_reason": "應用目前正在遷移數據,不能退出",
"app_knowledge": "知識庫文件",
"app_knowledge.button.delete": "刪除檔案",
"app_knowledge.remove_all": "刪除知識庫檔案",

View File

@ -2,6 +2,7 @@ import {
CloudSyncOutlined,
FileSearchOutlined,
FolderOpenOutlined,
LoadingOutlined,
SaveOutlined,
YuqueOutlined
} from '@ant-design/icons'
@ -18,7 +19,7 @@ import store, { useAppDispatch } from '@renderer/store'
import { setSkipBackupFile as _setSkipBackupFile } from '@renderer/store/settings'
import { AppInfo } from '@renderer/types'
import { formatFileSize } from '@renderer/utils'
import { Button, Switch, Typography } from 'antd'
import { Button, Progress, Switch, Typography } from 'antd'
import { FileText, FolderCog, FolderInput, Sparkle } from 'lucide-react'
import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -179,6 +180,281 @@ const DataSettings: FC = () => {
})
}
const handleSelectAppDataPath = async () => {
if (!appInfo || !appInfo.appDataPath) {
return
}
const newAppDataPath = await window.api.select({
properties: ['openDirectory', 'createDirectory'],
title: t('settings.data.app_data.select_title')
})
if (!newAppDataPath) {
return
}
// check new app data path is root path
// if is root path, show error
const pathParts = newAppDataPath.split(/[/\\]/).filter((part: string) => part !== '')
if (pathParts.length <= 1) {
window.message.error(t('settings.data.app_data.select_error_root_path'))
return
}
// check new app data path has write permission
const hasWritePermission = await window.api.hasWritePermission(newAppDataPath)
if (!hasWritePermission) {
window.message.error(t('settings.data.app_data.select_error_write_permission'))
return
}
const migrationTitle = (
<div style={{ fontSize: '18px', fontWeight: 'bold' }}>{t('settings.data.app_data.migration_title')}</div>
)
const migrationClassName = 'migration-modal'
const messageKey = 'data-migration'
// 显示确认对话框
showMigrationConfirmModal(appInfo.appDataPath, newAppDataPath, migrationTitle, migrationClassName, messageKey)
}
// 显示确认迁移的对话框
const showMigrationConfirmModal = (
originalPath: string,
newPath: string,
title: React.ReactNode,
className: string,
messageKey: string
) => {
// 复制数据选项状态
let shouldCopyData = true
// 创建路径内容组件
const PathsContent = () => (
<div>
<MigrationPathRow>
<MigrationPathLabel>{t('settings.data.app_data.original_path')}:</MigrationPathLabel>
<MigrationPathValue>{originalPath}</MigrationPathValue>
</MigrationPathRow>
<MigrationPathRow style={{ marginTop: '16px' }}>
<MigrationPathLabel>{t('settings.data.app_data.new_path')}:</MigrationPathLabel>
<MigrationPathValue>{newPath}</MigrationPathValue>
</MigrationPathRow>
</div>
)
const CopyDataContent = () => (
<div>
<MigrationPathRow style={{ marginTop: '20px', flexDirection: 'row', alignItems: 'center' }}>
<Switch
defaultChecked={true}
onChange={(checked) => {
shouldCopyData = checked
}}
style={{ marginRight: '8px' }}
/>
<MigrationPathLabel style={{ fontWeight: 'normal', fontSize: '14px' }}>
{t('settings.data.app_data.copy_data_option')}
</MigrationPathLabel>
</MigrationPathRow>
</div>
)
// 显示确认模态框
const modal = window.modal.confirm({
title,
className,
width: 'min(600px, 90vw)',
style: { minHeight: '400px' },
content: (
<MigrationModalContent>
<PathsContent />
<CopyDataContent />
<MigrationNotice>
<p style={{ color: 'var(--color-warning)' }}>{t('settings.data.app_data.restart_notice')}</p>
<p style={{ color: 'var(--color-text-3)', marginTop: '8px' }}>
{t('settings.data.app_data.copy_time_notice')}
</p>
</MigrationNotice>
</MigrationModalContent>
),
centered: true,
okButtonProps: {
danger: true
},
okText: t('common.confirm'),
cancelText: t('common.cancel'),
onOk: async () => {
try {
// 立即关闭确认对话框
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
}
} else {
// 如果不复制数据,直接设置新的应用数据路径
await window.api.setAppDataPath(newPath)
window.message.success(t('settings.data.app_data.path_changed_without_copy'))
}
// 更新应用数据路径
setAppInfo(await window.api.getAppInfo())
// 通知用户并重启应用
setTimeout(() => {
window.message.success(t('settings.data.app_data.select_success'))
window.api.setStopQuitApp(false, '')
window.api.relaunchApp()
}, 1000)
} 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,
duration: 5
})
}
}
})
}
// 显示进度模态框
const showProgressModal = (title: React.ReactNode, className: string, PathsContent: React.FC) => {
let currentProgress = 0
let progressInterval: NodeJS.Timeout | null = null
// 创建进度更新模态框
const loadingModal = window.modal.info({
title,
className,
width: 'min(600px, 90vw)',
style: { minHeight: '400px' },
icon: <LoadingOutlined style={{ fontSize: 18 }} />,
content: (
<MigrationModalContent>
<PathsContent />
<MigrationNotice>
<p>{t('settings.data.app_data.copying')}</p>
<div style={{ marginTop: '12px' }}>
<Progress percent={currentProgress} status="active" strokeWidth={8} />
</div>
<p style={{ color: 'var(--color-warning)', marginTop: '12px', fontSize: '13px' }}>
{t('settings.data.app_data.copying_warning')}
</p>
</MigrationNotice>
</MigrationModalContent>
),
centered: true,
closable: false,
maskClosable: false,
okButtonProps: { style: { display: 'none' } }
})
// 更新进度的函数
const updateProgress = (progress: number, status: 'active' | 'success' = 'active') => {
loadingModal.update({
title,
content: (
<MigrationModalContent>
<PathsContent />
<MigrationNotice>
<p>{t('settings.data.app_data.copying')}</p>
<div style={{ marginTop: '12px' }}>
<Progress percent={Math.round(progress)} status={status} strokeWidth={8} />
</div>
<p style={{ color: 'var(--color-warning)', marginTop: '12px', fontSize: '13px' }}>
{t('settings.data.app_data.copying_warning')}
</p>
</MigrationNotice>
</MigrationModalContent>
)
})
}
// 开始模拟进度更新
progressInterval = setInterval(() => {
if (currentProgress < 95) {
currentProgress += Math.random() * 5 + 1
if (currentProgress > 95) currentProgress = 95
updateProgress(currentProgress)
}
}, 500)
return { loadingModal, progressInterval, updateProgress }
}
// 开始迁移数据
const startMigration = async (
originalPath: string,
newPath: string,
progressInterval: NodeJS.Timeout | null,
updateProgress: (progress: number, status?: 'active' | 'success') => void,
loadingModal: { destroy: () => void },
messageKey: string
): Promise<void> => {
// 开始复制过程
const copyResult = await window.api.copy(originalPath, newPath)
// 停止进度更新
if (progressInterval) {
clearInterval(progressInterval)
}
// 显示100%完成
updateProgress(100, 'success')
if (!copyResult.success) {
// 延迟关闭加载模态框
await new Promise<void>((resolve) => {
setTimeout(() => {
loadingModal.destroy()
window.message.error({
content: t('settings.data.app_data.copy_failed') + ': ' + copyResult.error,
key: messageKey,
duration: 5
})
resolve()
}, 500)
})
throw new Error(copyResult.error || 'Unknown error during copy')
}
// 在复制成功后设置新的AppDataPath
await window.api.setAppDataPath(newPath)
// 短暂延迟以显示100%完成
await new Promise((resolve) => setTimeout(resolve, 500))
// 关闭加载模态框
loadingModal.destroy()
window.message.success({
content: t('settings.data.app_data.copy_success'),
key: messageKey,
duration: 2
})
}
const onSkipBackupFilesChange = (value: boolean) => {
setSkipBackupFile(value)
dispatch(_setSkipBackupFile(value))
@ -245,6 +521,9 @@ const DataSettings: FC = () => {
<PathRow>
<PathText style={{ color: 'var(--color-text-3)' }}>{appInfo?.appDataPath}</PathText>
<StyledIcon onClick={() => handleOpenPath(appInfo?.appDataPath)} style={{ flexShrink: 0 }} />
<HStack gap="5px" style={{ marginLeft: '8px' }}>
<Button onClick={handleSelectAppDataPath}>{t('settings.data.app_data.select')}</Button>
</HStack>
</PathRow>
</SettingRow>
<SettingDivider />
@ -352,4 +631,38 @@ const PathRow = styled(HStack)`
gap: 5px;
`
// Add styled components for migration modal
const MigrationModalContent = styled.div`
padding: 20px 0 10px;
display: flex;
flex-direction: column;
`
const MigrationNotice = styled.div`
margin-top: 24px;
font-size: 14px;
`
const MigrationPathRow = styled.div`
display: flex;
flex-direction: column;
gap: 5px;
`
const MigrationPathLabel = styled.div`
font-weight: 600;
font-size: 15px;
color: var(--color-text-1);
`
const MigrationPathValue = styled.div`
font-size: 14px;
color: var(--color-text-2);
background-color: var(--color-background-soft);
padding: 8px 12px;
border-radius: 4px;
word-break: break-all;
border: 1px solid var(--color-border);
`
export default DataSettings