镜像自地址
https://github.com/lhly/cloud-drive-renamer.git
已同步 2026-10-20 13:47:47 +08:00
feat: integrate conflict detection and crash recovery
这个提交包含在:
父节点
a13175441c
当前提交
85eb3b3ca1
@ -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<string, number> | 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<this>): 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<void> {
|
||||
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<string, ConflictResult> | 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<string, string>();
|
||||
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,
|
||||
|
||||
@ -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...",
|
||||
|
||||
@ -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": "生成预览中...",
|
||||
|
||||
@ -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": "產生預覽中...",
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户