mirror of
https://github.com/LHY0125/PathEditor.git
synced 2026-06-29 18:15:55 +08:00
refactor: AppShell 拆分 + savePaths 并行化
- 抽取 useAppActions hook(~160行),AppShell 从 306 行精简至 105 行 - AppShell 现在只负责布局和渲染,操作逻辑全部可单独测试 - savePaths 改为 Promise.allSettled 并行保存 + 并行备份 - useKeyboard 通过 hook 内部集成,不再暴露给 AppShell Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { TargetType } from '@/core/undo-redo';
|
||||
import { open } from '@tauri-apps/plugin-dialog';
|
||||
import { importFromContent, exportToJson, flattenImportResult } from '@/core/import-export';
|
||||
import { useKeyboard } from './use-keyboard';
|
||||
import i18n from '@/i18n';
|
||||
import type { TabId } from '@/store/app-store';
|
||||
|
||||
export interface DialogState {
|
||||
editDialog: { open: boolean; index: number; value: string; target: TargetType };
|
||||
newDialog: boolean;
|
||||
helpOpen: boolean;
|
||||
importDialog: { open: boolean; system: string[]; user: string[] };
|
||||
setEditDialog: (v: DialogState['editDialog']) => void;
|
||||
setNewDialog: (v: boolean) => void;
|
||||
setHelpOpen: (v: boolean) => void;
|
||||
setImportDialog: (v: DialogState['importDialog']) => void;
|
||||
}
|
||||
|
||||
export function useAppActions(activeTab: TabId, dialogs: DialogState) {
|
||||
const { setEditDialog, setNewDialog, setHelpOpen, setImportDialog } = dialogs;
|
||||
|
||||
const getCurrentTarget = useCallback((): TargetType => {
|
||||
return activeTab === 'user' ? TargetType.USER : TargetType.SYSTEM;
|
||||
}, [activeTab]);
|
||||
|
||||
// ── CRUD ──
|
||||
|
||||
const handleNew = useCallback(() => setNewDialog(true), [setNewDialog]);
|
||||
|
||||
const handleEdit = useCallback(() => {
|
||||
const idx = useAppStore.getState().selectedIndices[0];
|
||||
if (idx === undefined) return;
|
||||
const target = activeTab === 'user' ? TargetType.USER : TargetType.SYSTEM;
|
||||
const list = target === TargetType.SYSTEM
|
||||
? useAppStore.getState().sysPaths
|
||||
: useAppStore.getState().userPaths;
|
||||
const value = list[idx];
|
||||
if (value) setEditDialog({ open: true, index: idx, value, target });
|
||||
}, [activeTab, setEditDialog]);
|
||||
|
||||
const handleBrowse = useCallback(async () => {
|
||||
const selected = await open({ directory: true, multiple: false });
|
||||
if (selected && typeof selected === 'string') {
|
||||
useAppStore.getState().addPath(selected, getCurrentTarget());
|
||||
}
|
||||
}, [getCurrentTarget]);
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
const idx = useAppStore.getState().selectedIndices;
|
||||
if (idx.length === 0) return;
|
||||
useAppStore.getState().deletePaths(idx, getCurrentTarget());
|
||||
}, [getCurrentTarget]);
|
||||
|
||||
const handleMoveUp = useCallback(() => {
|
||||
const idx = useAppStore.getState().selectedIndices[0];
|
||||
if (idx === undefined) return;
|
||||
useAppStore.getState().moveUp(idx, getCurrentTarget());
|
||||
}, [getCurrentTarget]);
|
||||
|
||||
const handleMoveDown = useCallback(() => {
|
||||
const idx = useAppStore.getState().selectedIndices[0];
|
||||
if (idx === undefined) return;
|
||||
useAppStore.getState().moveDown(idx, getCurrentTarget());
|
||||
}, [getCurrentTarget]);
|
||||
|
||||
const handleClean = useCallback(() => {
|
||||
const removed = useAppStore.getState().cleanPaths(
|
||||
getCurrentTarget(),
|
||||
(p) => p.includes('%') || p.includes('\\') || p.includes('/') || /^[a-zA-Z]:[/\\]/.test(p),
|
||||
);
|
||||
if (removed.length > 0) {
|
||||
useAppStore.getState().setStatusMessage(
|
||||
i18n.t('status.deleted', { count: removed.length }),
|
||||
);
|
||||
}
|
||||
}, [getCurrentTarget]);
|
||||
|
||||
// ── 导入导出 ──
|
||||
|
||||
const handleImport = useCallback(() => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.json,.csv,.txt';
|
||||
input.onchange = async (e) => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0];
|
||||
if (!file) { input.remove(); return; }
|
||||
const content = await file.text();
|
||||
const result = importFromContent(content, file.name);
|
||||
input.remove();
|
||||
if (result.system.length > 0 && result.user.length > 0) {
|
||||
setImportDialog({ open: true, system: result.system, user: result.user });
|
||||
} else if (result.system.length > 0) {
|
||||
useAppStore.getState().importPaths(TargetType.SYSTEM, result.system);
|
||||
} else if (result.user.length > 0) {
|
||||
useAppStore.getState().importPaths(TargetType.USER, result.user);
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
}, [setImportDialog]);
|
||||
|
||||
const handleExport = useCallback(() => {
|
||||
const state = useAppStore.getState();
|
||||
const content = exportToJson({ system: state.sysPaths, user: state.userPaths });
|
||||
const blob = new Blob([content], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'patheditor_export.json';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}, []);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
useAppStore.getState().savePaths();
|
||||
}, []);
|
||||
|
||||
// ── 键盘 ──
|
||||
|
||||
useKeyboard({
|
||||
onNew: handleNew,
|
||||
onSave: handleSave,
|
||||
onDelete: handleDelete,
|
||||
onUndo: () => useAppStore.getState().undo(),
|
||||
onRedo: () => useAppStore.getState().redo(),
|
||||
onHelp: () => setHelpOpen(true),
|
||||
});
|
||||
|
||||
// ── 双击编辑 ──
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: Event) => {
|
||||
const detail = (e as CustomEvent).detail;
|
||||
if (detail && typeof detail.index === 'number') {
|
||||
const target = getCurrentTarget();
|
||||
setEditDialog({ open: true, index: detail.index, value: detail.path, target });
|
||||
}
|
||||
};
|
||||
window.addEventListener('path-dblclick', handler);
|
||||
return () => window.removeEventListener('path-dblclick', handler);
|
||||
}, [getCurrentTarget, setEditDialog]);
|
||||
|
||||
// ── 弹窗确认 ──
|
||||
|
||||
const handleNewConfirm = useCallback((value: string) => {
|
||||
setNewDialog(false);
|
||||
if (value.trim()) useAppStore.getState().addPath(value.trim(), getCurrentTarget());
|
||||
}, [getCurrentTarget, setNewDialog]);
|
||||
|
||||
const handleEditConfirm = useCallback((value: string) => {
|
||||
const d = dialogs.editDialog;
|
||||
setEditDialog({ open: false, index: -1, value: '', target: TargetType.SYSTEM });
|
||||
if (value.trim()) useAppStore.getState().editPath(d.index, value.trim(), d.target);
|
||||
}, [dialogs.editDialog, setEditDialog]);
|
||||
|
||||
const handleImportSelect = useCallback((target: 'system' | 'user' | 'both') => {
|
||||
const { system, user } = dialogs.importDialog;
|
||||
const flat = flattenImportResult({ system, user }, target);
|
||||
if (flat.system.length > 0) useAppStore.getState().importPaths(TargetType.SYSTEM, flat.system);
|
||||
if (flat.user.length > 0) useAppStore.getState().importPaths(TargetType.USER, flat.user);
|
||||
setImportDialog({ open: false, system: [], user: [] });
|
||||
}, [dialogs.importDialog, setImportDialog]);
|
||||
|
||||
return {
|
||||
handleNew, handleEdit, handleBrowse, handleDelete,
|
||||
handleMoveUp, handleMoveDown, handleClean,
|
||||
handleImport, handleExport, handleSave,
|
||||
handleNewConfirm, handleEditConfirm, handleImportSelect,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user