From 85eb3b3ca11aaa75233b144eda7dc296e6580d71 Mon Sep 17 00:00:00 2001 From: Zain Date: Wed, 18 Feb 2026 22:52:16 +0800 Subject: [PATCH] feat: integrate conflict detection and crash recovery --- src/content/components/file-selector-panel.ts | 125 ++++++++++++++++-- src/locales/en.json | 1 + src/locales/zh_CN.json | 1 + src/locales/zh_TW.json | 1 + 4 files changed, 116 insertions(+), 12 deletions(-) diff --git a/src/content/components/file-selector-panel.ts b/src/content/components/file-selector-panel.ts index 2757da2..e37b2fe 100644 --- a/src/content/components/file-selector-panel.ts +++ b/src/content/components/file-selector-panel.ts @@ -5,6 +5,14 @@ import { FileType, PreviewItem } from '../../types/file-selector'; import { RuleConfig } from '../../types/rule'; import { RuleFactory } from '../../rules/rule-factory'; import { BatchExecutor, ExecutorState } from '../../core/executor'; +import { buildExecutionPlan } from '../../core/execution-plan'; +import { + ConflictDetector, + ConflictResolution, + ConflictResult, + showConflictResolutionDialog, +} from '../../core/conflict-detector'; +import { crashRecovery, initCrashRecovery } from '../../core/crash-recovery'; import { BatchResults, ProgressEvent } from '../../types/core'; import { I18nService } from '../../utils/i18n'; import { parseFileName } from '../../utils/helpers'; @@ -133,6 +141,8 @@ export class FileSelectorPanel extends LitElement { private syncMessage: string | null = null; private executor: BatchExecutor | null = null; + private recoveryChecked = false; + private operationIndexByFileId: Map | null = null; /** * Error message @@ -197,15 +207,21 @@ export class FileSelectorPanel extends LitElement { if (this.open) { this.resetExecutionState(); await this.loadAllFiles(); + await this.ensureCrashRecovery(); } } protected updated(changedProperties: PropertyValues): void { super.updated(changedProperties); - if (changedProperties.has('open') && this.open) { - this.resetExecutionState(); - void this.loadAllFiles(); + if (changedProperties.has('open')) { + if (this.open) { + this.resetExecutionState(); + void this.loadAllFiles(); + void this.ensureCrashRecovery(); + } else { + this.recoveryChecked = false; + } } } @@ -241,6 +257,20 @@ export class FileSelectorPanel extends LitElement { } } + private async ensureCrashRecovery(): Promise { + if (this.recoveryChecked || !this.adapter) { + return; + } + + this.recoveryChecked = true; + try { + await initCrashRecovery(this.adapter); + } catch (error) { + const errorObj = error instanceof Error ? error : new Error(String(error)); + logger.warn('[FileSelectorPanel] Crash recovery check failed:', errorObj); + } + } + /** * Get file type from extension * @private @@ -440,29 +470,68 @@ export class FileSelectorPanel extends LitElement { return; } - const executionPlan = this.previewList; - if (executionPlan.length === 0) { + const selectedFiles = this.selectedFiles; + const previewPlan = this.previewList; + + if (previewPlan.length === 0) { alert(I18nService.t('no_rename_needed')); return; } - // Confirm if there are conflicts - if (this.conflictIds.size > 0) { - const conflictMessage = I18nService.t('conflicts_detected_confirm', [String(this.conflictIds.size)]); - const confirmed = confirm(conflictMessage); + const newNames = selectedFiles.map((file) => this.newNameMap.get(file.id) || file.name); + let conflicts: Map | null = null; + let resolution: ConflictResolution | null = null; + + try { + const detector = new ConflictDetector(this.adapter); + conflicts = await detector.detectConflicts(selectedFiles, newNames); + + const conflictCount = Array.from(conflicts.values()).filter((result) => result.hasConflict).length; + if (conflictCount > 0) { + resolution = showConflictResolutionDialog(conflictCount); + if (!resolution) { + return; + } + } + } catch (error) { + const errorObj = error instanceof Error ? error : new Error(String(error)); + logger.error('[FileSelectorPanel] Conflict detection failed:', errorObj); + const confirmed = confirm(I18nService.t('conflict_check_failed_confirm')); if (!confirmed) { return; } } + const executionPlan = buildExecutionPlan({ + files: selectedFiles, + newNames, + conflicts: conflicts ?? undefined, + resolution, + skipUnchanged: true, + }); + + if (executionPlan.tasks.length === 0) { + alert(I18nService.t('no_rename_needed')); + return; + } + + const resolvedNameMap = new Map(); + selectedFiles.forEach((file, index) => { + resolvedNameMap.set(file.id, executionPlan.resolvedNames[index] || file.name); + }); + this.newNameMap = resolvedNameMap; + this.conflictIds = new Set(); + try { this.resetExecutionState(); this.executing = true; this.executorState = ExecutorState.RUNNING; - this.executionItems = executionPlan.map((item) => ({ - ...item, + this.executionItems = executionPlan.tasks.map((task) => ({ + file: task.file, + newName: task.newName, + conflict: false, done: undefined, error: undefined, })); @@ -474,15 +543,29 @@ export class FileSelectorPanel extends LitElement { failed: 0, }; + this.operationIndexByFileId = new Map( + executionPlan.tasks.map((task) => [task.file.id, task.index]) + ); + + await crashRecovery.saveOperationState({ + platform: this.adapter.platform, + files: executionPlan.tasks.map((task) => task.file), + rule: this.ruleConfig, + completed: [], + failed: [], + tasks: executionPlan.tasks, + }); + // Execute batch rename const executor = new BatchExecutor( - this.selectedFiles, + executionPlan.tasks.map((task) => task.file), this.ruleConfig, this.adapter, { requestInterval: this.adapter.getConfig().requestInterval, maxConcurrent: this.adapter.getConfig().maxConcurrent, skipUnchanged: true, + tasks: executionPlan.tasks, onProgress: (progress) => { this.handleProgress(progress); }, @@ -501,6 +584,10 @@ export class FileSelectorPanel extends LitElement { this.applyExecutionResults(results); void this.syncAfterRename(); + + if (this.executorState !== ExecutorState.CANCELLED) { + await crashRecovery.clearOperationState(); + } } catch (error) { const errorObj = error instanceof Error ? error : new Error(String(error)); this.error = errorObj.message; @@ -517,6 +604,17 @@ export class FileSelectorPanel extends LitElement { return; } + if (this.operationIndexByFileId) { + const index = this.operationIndexByFileId.get(progress.fileId); + if (index !== undefined) { + if (progress.status === 'failed') { + void crashRecovery.markAsFailed(index); + } else if (progress.status === 'success') { + void crashRecovery.markAsCompleted(index); + } + } + } + this.executionItems = this.executionItems.map((item) => { if (item.file.id !== progress.fileId) { return item; @@ -643,6 +741,7 @@ export class FileSelectorPanel extends LitElement { this.executor.cancel(); this.executorState = this.executor.getState(); + void crashRecovery.clearOperationState(); } private handleSync(): void { @@ -752,6 +851,7 @@ export class FileSelectorPanel extends LitElement { this.executionResults = null; this.syncStatus = 'idle'; this.syncMessage = null; + this.operationIndexByFileId = null; } /** @@ -760,6 +860,7 @@ export class FileSelectorPanel extends LitElement { */ private handleClose(): void { this.resetExecutionState(); + this.recoveryChecked = false; this.dispatchEvent( new CustomEvent('panel-close', { bubbles: true, diff --git a/src/locales/en.json b/src/locales/en.json index 87e3e9a..0174a74 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -106,6 +106,7 @@ "sync_not_supported": "Auto sync is not supported on this platform. Please refresh the page.", "conflicts_detected": "Conflicts detected: $1", "conflicts_detected_confirm": "Conflicts detected: $1. Continue?", + "conflict_check_failed_confirm": "Conflict check failed. Continue?", "preview_empty": "No files to preview", "preview_empty_hint": "Select files and configure rules to see the preview", "preview_loading": "Generating preview...", diff --git a/src/locales/zh_CN.json b/src/locales/zh_CN.json index 8dce0d5..83c6be2 100644 --- a/src/locales/zh_CN.json +++ b/src/locales/zh_CN.json @@ -106,6 +106,7 @@ "sync_not_supported": "当前平台暂不支持自动同步,请手动刷新页面", "conflicts_detected": "检测到 $1 个冲突", "conflicts_detected_confirm": "检测到 $1 个冲突,是否继续?", + "conflict_check_failed_confirm": "冲突检测失败,是否继续执行?", "preview_empty": "暂无文件可预览", "preview_empty_hint": "选择文件并配置规则以查看预览", "preview_loading": "生成预览中...", diff --git a/src/locales/zh_TW.json b/src/locales/zh_TW.json index f3c1b93..061765d 100644 --- a/src/locales/zh_TW.json +++ b/src/locales/zh_TW.json @@ -106,6 +106,7 @@ "sync_not_supported": "目前平台暫不支援自動同步,請手動重新整理頁面", "conflicts_detected": "偵測到 $1 個衝突", "conflicts_detected_confirm": "偵測到 $1 個衝突,是否繼續?", + "conflict_check_failed_confirm": "衝突偵測失敗,是否繼續執行?", "preview_empty": "暫無檔案可預覽", "preview_empty_hint": "選擇檔案並設定規則以查看預覽", "preview_loading": "產生預覽中...",