import { useCallback, useRef } from 'react' import { useTabStore } from '../stores/tabStore' import { useEditorStore } from '../stores/editorStore' import { recentFilesRepository } from '../db/recentFilesRepository' import { logError } from '../lib/errorHandler' import { MeToast, showToast } from '../lib/toast' /** * AR-01: 从 App.tsx 提取的文件操作逻辑 * UX-02: 添加 loading 状态指示 * v0.1.9: handleSave 添加防重入锁,避免编辑器 onSave + 全局 Ctrl+S 双重触发 * v0.6.0: loading 改用 MeToast.loading 链式转换,保存结果用 MeToast.promise 提示 */ export function useFileOperations() { const createTab = useTabStore(s => s.createTab) const getActiveTab = useTabStore(s => s.getActiveTab) const updateTabFilePath = useTabStore(s => s.updateTabFilePath) const setLoading = useEditorStore(s => s.setLoading) // 防重入:onSave 回调 + 全局 Ctrl+S handler 可能在 300ms 内双重触发 const savingGate = useRef(false) const handleOpenFile = useCallback(async (): Promise => { if (!window.electronAPI) return const loading = MeToast.loading('打开文件...') try { setLoading('file-open', true) const result = await window.electronAPI.openFile() if (result && 'filePath' in result) { createTab(result.filePath, result.content) if (result.filePath) recentFilesRepository.add(result.filePath) setTimeout(() => useTabStore.getState().saveToDB(), 100) loading.success('文件已打开') } else if (result && 'error' in result) { loading.error(`打开失败: ${result.error}`) } else { loading.dismiss() } } catch (error) { logError('打开文件失败', error) loading.error('打开文件失败') } finally { setLoading('file-open', false) } }, [createTab, setLoading]) const handleSave = useCallback(async (): Promise => { // 防重入:300ms 内忽略重复调用(编辑器 onSave + 全局 Ctrl+S 双重触发) if (savingGate.current) return savingGate.current = true try { const tab = getActiveTab() if (!tab || !window.electronAPI) return // B1-fix: 保存前记录内容快照 — 保存期间继续输入的话,完成后 // 仅当内容未变化时才清除 isModified,避免新输入被误清标记 const savedContent = tab.content // v0.6.0: promise 监听保存生命周期 — 自动 loading → success/error,返回原 Promise const result = await MeToast.promise( window.electronAPI.saveFile({ filePath: tab.filePath, content: savedContent, }), { loading: '保存中...', success: '已保存', error: '保存失败', }, ) if (result.success) { const latest = useTabStore.getState().tabs.find(t => t.id === tab.id) if (latest && latest.content === savedContent) { useTabStore.getState().setModified(tab.id, false) } } else { // IPC 返回 success:false 不 reject,promise 的 error 文案不会触发,需显式提示 if (!result.canceled) { showToast(`保存失败: ${result.error ?? '未知错误'}`, 'error') } } } catch (error) { logError('保存文件失败', error) } finally { // 300ms 后释放锁,允许下次保存 setTimeout(() => { savingGate.current = false }, 300) } }, [getActiveTab]) const handleSaveAs = useCallback(async (): Promise => { try { const tab = getActiveTab() if (!tab || !window.electronAPI) return const result = await MeToast.promise( window.electronAPI.saveFileAs({ content: tab.content }), { loading: '另存为...', success: '已保存', error: '另存为失败', }, ) if (result.success && result.filePath) { // B2-fix: 另存为成功后把标签重新绑定到新路径(此前标签仍指向旧文件, // 之后 Ctrl+S/自动保存会写回旧文件) updateTabFilePath(tab.id, result.filePath) useTabStore.getState().setModified(tab.id, false) recentFilesRepository.add(result.filePath) setTimeout(() => useTabStore.getState().saveToDB(), 100) } else if (!result.canceled) { showToast('另存为失败', 'error') } } catch (error) { logError('另存为失败', error) } }, [getActiveTab, updateTabFilePath]) const handleOpenRecent = useCallback( async (filePath: string): Promise => { if (!window.electronAPI) return setLoading('file-open', true) try { const result = await window.electronAPI.readFile(filePath) if (result.success && result.content !== undefined) { createTab(filePath, result.content) recentFilesRepository.add(filePath) setTimeout(() => useTabStore.getState().saveToDB(), 100) } else { // B13-fix: 打开失败(文件被删除/移动)显式提示,不再静默 showToast(`无法打开 "${filePath}": ${result.error ?? '未知错误'}`, 'error') } } catch (error) { logError('打开最近文件失败', error) showToast(`无法打开 "${filePath}"`, 'error') } finally { setLoading('file-open', false) } }, [createTab, setLoading], ) return { handleOpenFile, handleSave, handleSaveAs, handleOpenRecent } }