diff --git a/src/components/layout/AppShell.tsx b/src/components/layout/AppShell.tsx index d9e8169..dc820ae 100644 --- a/src/components/layout/AppShell.tsx +++ b/src/components/layout/AppShell.tsx @@ -1,11 +1,9 @@ -import { useState, useCallback, useEffect } from 'react'; +import { useState } from 'react'; import { useAppStore, type TabId } from '@/store/app-store'; import { useThemeStore } from '@/store/theme-store'; import { useTranslation } from 'react-i18next'; import i18n from '@/i18n'; import { TargetType } from '@/core/undo-redo'; -import { open } from '@tauri-apps/plugin-dialog'; -import { importFromContent, exportToJson, flattenImportResult } from '@/core/import-export'; import { StatusBar } from './StatusBar'; import { TitleBar } from './TitleBar'; import { ToolBar } from '@/components/toolbar/ToolBar'; @@ -14,177 +12,28 @@ import { MergePreview } from '@/components/path-list/MergePreview'; import { PathEditDialog } from '@/components/dialogs/PathEditDialog'; import { HelpDialog } from '@/components/dialogs/HelpDialog'; import { ImportDialog } from '@/components/dialogs/ImportDialog'; -import { useKeyboard } from '@/hooks/use-keyboard'; +import { useAppActions, type DialogState } from '@/hooks/use-app-actions'; export function AppShell() { const { t } = useTranslation(); const activeTab = useAppStore((s) => s.activeTab); const setActiveTab = useAppStore((s) => s.setActiveTab); - const selectedIndices = useAppStore((s) => s.selectedIndices); const setSelectedIndices = useAppStore((s) => s.setSelectedIndices); - // 对话弹窗状态 - const [editDialog, setEditDialog] = useState<{ open: boolean; index: number; value: string; target: TargetType }>({ + const [editDialog, setEditDialog] = useState({ open: false, index: -1, value: '', target: TargetType.SYSTEM, }); const [newDialog, setNewDialog] = useState(false); const [helpOpen, setHelpOpen] = useState(false); - const [importDialog, setImportDialog] = useState<{ open: boolean; system: string[]; user: string[] }>({ + const [importDialog, setImportDialog] = useState({ open: false, system: [], user: [], }); - // ── 操作处理 ── - - const getCurrentTarget = useCallback((): TargetType => { - return activeTab === 'user' ? TargetType.USER : TargetType.SYSTEM; - }, [activeTab]); - - const handleNew = useCallback(() => { - setNewDialog(true); - }, []); - - const handleEdit = useCallback(() => { - if (selectedIndices.length !== 1) return; - const idx = selectedIndices[0]; - const target = getCurrentTarget(); - const list = target === TargetType.SYSTEM - ? useAppStore.getState().sysPaths - : useAppStore.getState().userPaths; - const value = list[idx]; - if (value) { - setEditDialog({ open: true, index: idx, value, target }); - } - }, [selectedIndices, getCurrentTarget]); - - 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(() => { - if (selectedIndices.length === 0) return; - useAppStore.getState().deletePaths(selectedIndices, getCurrentTarget()); - }, [selectedIndices, getCurrentTarget]); - - const handleMoveUp = useCallback(() => { - if (selectedIndices.length !== 1) return; - useAppStore.getState().moveUp(selectedIndices[0], getCurrentTarget()); - }, [selectedIndices, getCurrentTarget]); - - const handleMoveDown = useCallback(() => { - if (selectedIndices.length !== 1) return; - useAppStore.getState().moveDown(selectedIndices[0], getCurrentTarget()); - }, [selectedIndices, 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( - t('status.deleted', { count: removed.length }), - ); - } - }, [getCurrentTarget, t]); - - 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(); - }, []); - - const handleExport = useCallback(() => { - const state = useAppStore.getState(); - const data = { system: state.sysPaths, user: state.userPaths }; - - const content = exportToJson(data); - const mime = 'application/json'; - const ext = '.json'; - - const blob = new Blob([content], { type: mime }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `patheditor_export${ext}`; - 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), + const actions = useAppActions(activeTab, { + editDialog, newDialog, helpOpen, importDialog, + setEditDialog, setNewDialog, setHelpOpen, setImportDialog, }); - // ── 双击编辑监听 ── - - 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]); - - const handleNewConfirm = useCallback((value: string) => { - setNewDialog(false); - if (value.trim()) { - useAppStore.getState().addPath(value.trim(), getCurrentTarget()); - } - }, [getCurrentTarget]); - - const handleEditConfirm = useCallback((value: string) => { - setEditDialog({ open: false, index: -1, value: '', target: TargetType.SYSTEM }); - if (value.trim()) { - useAppStore.getState().editPath(editDialog.index, value.trim(), editDialog.target); - } - }, [editDialog]); - - const handleImportSelect = useCallback((target: 'system' | 'user' | 'both') => { - const { system, user } = 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: [] }); - }, [importDialog]); - - // Tab 切换 const tabConfig: { id: TabId; label: string }[] = [ { id: 'system', label: t('tab.system') }, { id: 'user', label: t('tab.user') }, @@ -192,27 +41,15 @@ export function AppShell() { ]; return ( -
+
- {/* Tab 栏 */} -
+
{tabConfig.map((tab) => (
- {/* 工具栏 */}
{ const state = useAppStore.getState(); if (state.isModified && !window.confirm('有未保存的修改,确定退出吗?')) return; @@ -247,60 +83,27 @@ export function AppShell() { />
- {/* 路径列表(支持拖拽文件夹) */}
{ - e.preventDefault(); - e.dataTransfer.dropEffect = 'link'; - }} + onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'link'; }} onDrop={(e) => { e.preventDefault(); if (activeTab === 'merged') return; - const files = e.dataTransfer.files; - for (let i = 0; i < files.length; i++) { - const path = (files[i] as any).path; - if (path) { - useAppStore.getState().addPath(path, getCurrentTarget()); - } + for (let i = 0; i < e.dataTransfer.files.length; i++) { + const path = (e.dataTransfer.files[i] as any).path; + if (path) useAppStore.getState().addPath(path, activeTab === 'user' ? TargetType.USER : TargetType.SYSTEM); } }} > - {activeTab === 'merged' ? ( - - ) : ( - - )} + {activeTab === 'merged' ? : }
- {/* 弹窗 */} - setNewDialog(false)} - /> - - setEditDialog({ open: false, index: -1, value: '', target: TargetType.SYSTEM })} - /> - + setNewDialog(false)} /> + setEditDialog({ open: false, index: -1, value: '', target: TargetType.SYSTEM })} /> setHelpOpen(false)} /> - - setImportDialog({ open: false, system: [], user: [] })} - /> + setImportDialog({ open: false, system: [], user: [] })} />
); } diff --git a/src/hooks/use-app-actions.ts b/src/hooks/use-app-actions.ts new file mode 100644 index 0000000..9a40d2c --- /dev/null +++ b/src/hooks/use-app-actions.ts @@ -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, + }; +} diff --git a/src/store/app-store.ts b/src/store/app-store.ts index 1cc7e24..fdc88d5 100644 --- a/src/store/app-store.ts +++ b/src/store/app-store.ts @@ -230,15 +230,20 @@ export const useAppStore = create((set, get) => ({ set({ statusMessage: i18n.t('status.saving') }); - // 保存前备份 - try { await invoke('backup_registry', { customDir: null, sysPaths, userPaths }); } catch { /* 备份失败不阻止保存 */ } + // 备份(不阻塞保存) + invoke('backup_registry', { customDir: null, sysPaths, userPaths }).catch(() => {}); - let sysOk = true, userOk = true; - try { await invoke('save_system_paths', { paths: sysPaths }); } catch { sysOk = false; } - try { await invoke('save_user_paths', { paths: userPaths }); } catch { userOk = false; } + // 并行保存 + const [sysResult, userResult] = await Promise.allSettled([ + invoke('save_system_paths', { paths: sysPaths }), + invoke('save_user_paths', { paths: userPaths }), + ]); + + const sysOk = sysResult.status === 'fulfilled'; + const userOk = userResult.status === 'fulfilled'; if (sysOk && userOk) { - try { await invoke('broadcast_env_change'); } catch { /* 广播失败不阻止 */ } + invoke('broadcast_env_change').catch(() => {}); set({ isModified: false, statusMessage: i18n.t('status.saved') }); } else if (sysOk) { set({ statusMessage: '用户 PATH 保存失败,系统 PATH 已保存' });