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:
fullex 2025-08-10 00:19:09 +08:00
parent 92eb5aed7f
commit 8715eb1f41
3 changed files with 203 additions and 186 deletions

View File

@ -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',

View File

@ -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()
}

View File

@ -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