mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-27 12:51:26 +08:00
feat(migration): add new IPC channels and enhance migration flow
This commit introduces new IPC channels for starting the migration flow, restarting the application, and closing the migration window. It also updates the migration logic to improve user interaction and error handling during the migration process. Additionally, the migration interface has been enhanced with a step indicator and localized messages for better user experience.
This commit is contained in:
parent
92eb5aed7f
commit
8715eb1f41
@ -192,6 +192,9 @@ export enum IpcChannel {
|
||||
DataMigrate_RequireBackup = 'data-migrate:require-backup',
|
||||
DataMigrate_BackupCompleted = 'data-migrate:backup-completed',
|
||||
DataMigrate_ShowBackupDialog = 'data-migrate:show-backup-dialog',
|
||||
DataMigrate_StartFlow = 'data-migrate:start-flow',
|
||||
DataMigrate_RestartApp = 'data-migrate:restart-app',
|
||||
DataMigrate_CloseWindow = 'data-migrate:close-window',
|
||||
|
||||
// zip
|
||||
Zip_Compress = 'zip:compress',
|
||||
|
||||
@ -40,7 +40,6 @@ class DataRefactorMigrateService {
|
||||
private migrateWindow: BrowserWindow | null = null
|
||||
private backupManager: BackupManager
|
||||
private backupCompletionResolver: ((value: boolean) => void) | null = null
|
||||
private backupTimeout: NodeJS.Timeout | null = null
|
||||
private db = dbService.getDb()
|
||||
private currentProgress: MigrationProgress = {
|
||||
stage: 'idle',
|
||||
@ -127,6 +126,35 @@ class DataRefactorMigrateService {
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.DataMigrate_StartFlow, async () => {
|
||||
try {
|
||||
return await this.startMigrationFlow()
|
||||
} catch (error) {
|
||||
logger.error('IPC handler error: startMigrationFlow', error as Error)
|
||||
throw error
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.DataMigrate_RestartApp, () => {
|
||||
try {
|
||||
this.restartApplication()
|
||||
return true
|
||||
} catch (error) {
|
||||
logger.error('IPC handler error: restartApplication', error as Error)
|
||||
throw error
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.DataMigrate_CloseWindow, () => {
|
||||
try {
|
||||
this.closeMigrateWindow()
|
||||
return true
|
||||
} catch (error) {
|
||||
logger.error('IPC handler error: closeMigrateWindow', error as Error)
|
||||
throw error
|
||||
}
|
||||
})
|
||||
|
||||
logger.info('Migration IPC handlers registered successfully')
|
||||
}
|
||||
|
||||
@ -144,6 +172,9 @@ class DataRefactorMigrateService {
|
||||
ipcMain.removeAllListeners(IpcChannel.DataMigrate_Cancel)
|
||||
ipcMain.removeAllListeners(IpcChannel.DataMigrate_BackupCompleted)
|
||||
ipcMain.removeAllListeners(IpcChannel.DataMigrate_ShowBackupDialog)
|
||||
ipcMain.removeAllListeners(IpcChannel.DataMigrate_StartFlow)
|
||||
ipcMain.removeAllListeners(IpcChannel.DataMigrate_RestartApp)
|
||||
ipcMain.removeAllListeners(IpcChannel.DataMigrate_CloseWindow)
|
||||
|
||||
logger.info('Migration IPC handlers unregistered successfully')
|
||||
} catch (error) {
|
||||
@ -294,32 +325,37 @@ class DataRefactorMigrateService {
|
||||
async runMigration(): Promise<void> {
|
||||
if (this.isMigrating) {
|
||||
logger.warn('Migration already in progress')
|
||||
this.migrateWindow?.show()
|
||||
return
|
||||
}
|
||||
|
||||
this.isMigrating = true
|
||||
logger.info('Showing migration window')
|
||||
|
||||
// Create migration window
|
||||
const window = this.createMigrateWindow()
|
||||
|
||||
// Wait for window to be ready
|
||||
await new Promise<void>((resolve) => {
|
||||
if (window.webContents.isLoading()) {
|
||||
window.webContents.once('did-finish-load', () => resolve())
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async startMigrationFlow(): Promise<void> {
|
||||
if (!this.isMigrating) {
|
||||
logger.warn('Migration not started, cannot execute flow.')
|
||||
return
|
||||
}
|
||||
logger.info('Starting migration flow from user action')
|
||||
try {
|
||||
this.isMigrating = true
|
||||
logger.info('Starting migration process')
|
||||
|
||||
// Create migration window
|
||||
const window = this.createMigrateWindow()
|
||||
|
||||
// Wait for window to be ready
|
||||
await new Promise<void>((resolve) => {
|
||||
if (window.webContents.isLoading()) {
|
||||
window.webContents.once('did-finish-load', () => resolve())
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
|
||||
// Start the migration flow
|
||||
await this.executeMigrationFlow()
|
||||
} catch (error) {
|
||||
logger.error('Migration process failed', error as Error)
|
||||
throw error
|
||||
} finally {
|
||||
this.isMigrating = false
|
||||
// error is already handled in executeMigrationFlow
|
||||
}
|
||||
}
|
||||
|
||||
@ -355,33 +391,15 @@ class DataRefactorMigrateService {
|
||||
// Step 3: Mark as completed
|
||||
await this.markMigrationCompleted()
|
||||
|
||||
await this.updateProgress('completed', 100, 'Migration completed! App will restart in 3 seconds...')
|
||||
|
||||
// Wait a moment to show success message, then restart the app
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(() => {
|
||||
logger.info('Migration completed successfully, restarting application')
|
||||
this.restartApplication()
|
||||
resolve()
|
||||
}, 3000)
|
||||
})
|
||||
await this.updateProgress('completed', 100, 'Migration completed! Please restart the app.')
|
||||
} catch (error) {
|
||||
logger.error('Migration flow failed', error as Error)
|
||||
await this.updateProgress(
|
||||
'error',
|
||||
0,
|
||||
`Migration failed: ${error instanceof Error ? error.message : String(error)}. Please restart the app to try again.`
|
||||
`Migration failed: ${error instanceof Error ? error.message : String(error)}. Please close this window and restart the app to try again.`
|
||||
)
|
||||
|
||||
// Wait a moment to show error message, then close migration window
|
||||
// Do NOT restart on error - let user handle the situation
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(() => {
|
||||
this.closeMigrateWindow()
|
||||
resolve()
|
||||
}, 8000) // Show error for longer (8 seconds) to give user time to read
|
||||
})
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@ -425,14 +443,6 @@ class DataRefactorMigrateService {
|
||||
// Store resolver for later use
|
||||
this.backupCompletionResolver = resolve
|
||||
|
||||
// Set up timeout (5 minutes)
|
||||
this.backupTimeout = setTimeout(() => {
|
||||
logger.warn('Backup completion timeout')
|
||||
this.backupCompletionResolver = null
|
||||
this.backupTimeout = null
|
||||
resolve(false)
|
||||
}, 300000) // 5 minutes
|
||||
|
||||
// The actual completion will be triggered by notifyBackupCompleted() method
|
||||
})
|
||||
}
|
||||
@ -444,12 +454,6 @@ class DataRefactorMigrateService {
|
||||
if (this.backupCompletionResolver) {
|
||||
logger.info('Backup completed by user')
|
||||
|
||||
// Clear timeout if it exists
|
||||
if (this.backupTimeout) {
|
||||
clearTimeout(this.backupTimeout)
|
||||
this.backupTimeout = null
|
||||
}
|
||||
|
||||
this.backupCompletionResolver(true)
|
||||
this.backupCompletionResolver = null
|
||||
}
|
||||
@ -535,6 +539,7 @@ class DataRefactorMigrateService {
|
||||
this.migrateWindow = null
|
||||
}
|
||||
|
||||
this.isMigrating = false
|
||||
// Clean up migration-specific IPC handlers
|
||||
this.unregisterMigrationIpcHandlers()
|
||||
}
|
||||
|
||||
@ -1,80 +1,11 @@
|
||||
import { CheckCircleOutlined, ExclamationCircleOutlined, LoadingOutlined } from '@ant-design/icons'
|
||||
import { AppLogo } from '@renderer/config/env'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import { Alert, Button, Card, Progress, Space, Typography } from 'antd'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Alert, Button, Card, Progress, Space, Steps, Typography } from 'antd'
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const { Title, Text } = Typography
|
||||
|
||||
const Container = styled.div`
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 20px;
|
||||
`
|
||||
|
||||
const MigrationCard = styled(Card)`
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.ant-card-head {
|
||||
background: #f8f9fa;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.ant-card-body {
|
||||
padding: 32px 24px;
|
||||
}
|
||||
`
|
||||
|
||||
const LogoContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 24px;
|
||||
|
||||
img {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
`
|
||||
|
||||
const StageIndicator = styled.div<{ stage: string }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.stage-icon {
|
||||
font-size: 20px;
|
||||
color: ${(props) => {
|
||||
switch (props.stage) {
|
||||
case 'completed':
|
||||
return '#52c41a'
|
||||
case 'error':
|
||||
return '#ff4d4f'
|
||||
default:
|
||||
return '#1890ff'
|
||||
}
|
||||
}};
|
||||
}
|
||||
`
|
||||
|
||||
const ProgressContainer = styled.div`
|
||||
margin: 24px 0;
|
||||
`
|
||||
|
||||
const MessageContainer = styled.div`
|
||||
text-align: center;
|
||||
margin: 16px 0;
|
||||
min-height: 24px;
|
||||
`
|
||||
|
||||
interface MigrationProgress {
|
||||
stage: string
|
||||
progress: number
|
||||
@ -87,7 +18,7 @@ const MigrateApp: React.FC = () => {
|
||||
stage: 'idle',
|
||||
progress: 0,
|
||||
total: 100,
|
||||
message: 'Initializing migration...'
|
||||
message: '准备开始迁移...'
|
||||
})
|
||||
const [showBackupRequired, setShowBackupRequired] = useState(false)
|
||||
|
||||
@ -120,6 +51,43 @@ const MigrateApp: React.FC = () => {
|
||||
}
|
||||
}, [])
|
||||
|
||||
const currentStep = useMemo(() => {
|
||||
switch (progress.stage) {
|
||||
case 'idle':
|
||||
return 0
|
||||
case 'backup':
|
||||
return 1
|
||||
case 'migration':
|
||||
return 2
|
||||
case 'completed':
|
||||
return 4
|
||||
case 'error':
|
||||
case 'cancelled':
|
||||
return 3
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}, [progress.stage])
|
||||
|
||||
const stepStatus = useMemo(() => {
|
||||
if (progress.stage === 'error' || progress.stage === 'cancelled') {
|
||||
return 'error'
|
||||
}
|
||||
return 'process'
|
||||
}, [progress.stage])
|
||||
|
||||
const handleStartMigration = () => {
|
||||
window.electron.ipcRenderer.invoke(IpcChannel.DataMigrate_StartFlow)
|
||||
}
|
||||
|
||||
const handleRestartApp = () => {
|
||||
window.electron.ipcRenderer.invoke(IpcChannel.DataMigrate_RestartApp)
|
||||
}
|
||||
|
||||
const handleCloseWindow = () => {
|
||||
window.electron.ipcRenderer.invoke(IpcChannel.DataMigrate_CloseWindow)
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
window.electron.ipcRenderer.invoke(IpcChannel.DataMigrate_Cancel)
|
||||
}
|
||||
@ -135,35 +103,6 @@ const MigrateApp: React.FC = () => {
|
||||
window.electron.ipcRenderer.invoke(IpcChannel.DataMigrate_BackupCompleted)
|
||||
}
|
||||
|
||||
const getStageTitle = () => {
|
||||
switch (progress.stage) {
|
||||
case 'backup':
|
||||
return 'Creating Backup'
|
||||
case 'migration':
|
||||
return 'Migrating Data'
|
||||
case 'completed':
|
||||
return 'Migration Completed'
|
||||
case 'error':
|
||||
return 'Migration Failed'
|
||||
case 'cancelled':
|
||||
return 'Migration Cancelled'
|
||||
default:
|
||||
return 'Preparing Migration'
|
||||
}
|
||||
}
|
||||
|
||||
const getStageIcon = () => {
|
||||
switch (progress.stage) {
|
||||
case 'completed':
|
||||
return <CheckCircleOutlined className="stage-icon" />
|
||||
case 'error':
|
||||
case 'cancelled':
|
||||
return <ExclamationCircleOutlined className="stage-icon" />
|
||||
default:
|
||||
return <LoadingOutlined className="stage-icon" />
|
||||
}
|
||||
}
|
||||
|
||||
const getProgressColor = () => {
|
||||
switch (progress.stage) {
|
||||
case 'completed':
|
||||
@ -176,8 +115,37 @@ const MigrateApp: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const showCancelButton = () => {
|
||||
return progress.stage !== 'completed' && progress.stage !== 'error' && progress.stage !== 'cancelled'
|
||||
const renderActionButtons = () => {
|
||||
switch (progress.stage) {
|
||||
case 'idle':
|
||||
return (
|
||||
<Button type="primary" onClick={handleStartMigration}>
|
||||
开始迁移
|
||||
</Button>
|
||||
)
|
||||
case 'completed':
|
||||
return (
|
||||
<Button type="primary" onClick={handleRestartApp}>
|
||||
重启应用
|
||||
</Button>
|
||||
)
|
||||
case 'error':
|
||||
case 'cancelled':
|
||||
return (
|
||||
<Space>
|
||||
<Button onClick={handleCloseWindow}>关闭</Button>
|
||||
</Space>
|
||||
)
|
||||
case 'backup':
|
||||
case 'migration':
|
||||
return (
|
||||
<Button onClick={handleCancel} disabled={progress.stage === 'backup'}>
|
||||
取消迁移
|
||||
</Button>
|
||||
)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@ -192,28 +160,28 @@ const MigrateApp: React.FC = () => {
|
||||
}
|
||||
bordered={false}>
|
||||
<LogoContainer>
|
||||
<img
|
||||
src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjQiIGhlaWdodD0iNjQiIHZpZXdCb3g9IjAgMCA2NCA2NCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMzIiIGN5PSIzMiIgcj0iMzIiIGZpbGw9IiM2NjdlZWEiLz4KPHN2ZyB4PSIxNiIgeT0iMTYiIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJ3aGl0ZSI+CjxwYXRoIGQ9Im0xMiAyIDMgN2g3bC01LjUgNEwxOSAyMGwtNy02LTcgNiAyLjUtN0w2IDloN2wzLTd6Ii8+Cjwvc3ZnPgo8L3N2Zz4K"
|
||||
alt="Cherry Studio"
|
||||
/>
|
||||
<img src={AppLogo} alt="Cherry Studio" />
|
||||
</LogoContainer>
|
||||
|
||||
<StageIndicator stage={progress.stage}>
|
||||
{getStageIcon()}
|
||||
<Title level={4} style={{ margin: 0 }}>
|
||||
{getStageTitle()}
|
||||
</Title>
|
||||
</StageIndicator>
|
||||
|
||||
<ProgressContainer>
|
||||
<Progress
|
||||
percent={progress.progress}
|
||||
strokeColor={getProgressColor()}
|
||||
trailColor="#f0f0f0"
|
||||
size="default"
|
||||
showInfo={true}
|
||||
<div style={{ margin: '24px 0' }}>
|
||||
<Steps
|
||||
current={currentStep}
|
||||
status={stepStatus}
|
||||
items={[{ title: '开始' }, { title: '备份' }, { title: '迁移' }, { title: '完成' }]}
|
||||
/>
|
||||
</ProgressContainer>
|
||||
</div>
|
||||
|
||||
{progress.stage !== 'idle' && (
|
||||
<ProgressContainer>
|
||||
<Progress
|
||||
percent={progress.progress}
|
||||
strokeColor={getProgressColor()}
|
||||
trailColor="#f0f0f0"
|
||||
size="default"
|
||||
showInfo={true}
|
||||
/>
|
||||
</ProgressContainer>
|
||||
)}
|
||||
|
||||
<MessageContainer>
|
||||
<Text type="secondary">{progress.message}</Text>
|
||||
@ -259,18 +227,59 @@ const MigrateApp: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{showCancelButton() && (
|
||||
<div style={{ textAlign: 'center', marginTop: 24 }}>
|
||||
<Space>
|
||||
<Button onClick={handleCancel} disabled={progress.stage === 'backup'}>
|
||||
Cancel Migration
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ textAlign: 'center', marginTop: 24 }}>{renderActionButtons()}</div>
|
||||
</MigrationCard>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 20px;
|
||||
`
|
||||
|
||||
const MigrationCard = styled(Card)`
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.ant-card-head {
|
||||
background: #f8f9fa;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.ant-card-body {
|
||||
padding: 32px 24px;
|
||||
}
|
||||
`
|
||||
|
||||
const LogoContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 24px;
|
||||
|
||||
img {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
`
|
||||
|
||||
const ProgressContainer = styled.div`
|
||||
margin: 24px 0;
|
||||
`
|
||||
|
||||
const MessageContainer = styled.div`
|
||||
text-align: center;
|
||||
margin: 16px 0;
|
||||
min-height: 24px;
|
||||
`
|
||||
|
||||
export default MigrateApp
|
||||
|
||||
Loading…
Reference in New Issue
Block a user