refactor: 删除分屏功能 + 代码审计修复 + 安全加固
## 删除分屏功能 - 删除 ViewMode 'split' 类型,仅保留 editor/preview - 删除 splitRatio 状态和 Resizer 组件 - 删除滚动同步模块 (scrollSync.ts, rehypeSourceLine.ts) - 更新快捷键: Ctrl+1 编辑, Ctrl+2 预览 ## Bug 修复 - 修复 Toast setTimeout 内存泄漏 (App.tsx) - 修复 ModifiedBanner reload 更新错误标签 (改用 modifiedFilePath 匹配) - 修复 FileWatcher error 未重置 isSelfWriting (file-watcher.ts) - 修复另存为时未 stop watcher (ipc-handlers.ts) ## 死代码清理 (10 项) - 删除 main/ipc-channels.ts (与 shared/ 重复) - 删除 main/file-system.ts 未使用的 formatBytes - 删除 constants.ts 6 个未使用常量 - 删除 fileUtils.ts 未使用的 formatBytes - 删除 Icons.tsx 5 个未使用图标 (SplitView/ChevronUp/ChevronDown/X/ArrowUp/ArrowDown) - 删除 useCodeMirror 未使用的 getContent/scrollTo - 删除 TabBar 未使用的 menuRef - 删除 Tab.scrollLeft/previewScrollTop 字段 - 删除 tabStore 未使用的 getTabIndex - 删除 Toast/ModifiedBanner 多余 React import ## 安全加固 - ipc-handlers: 添加路径遍历防护 (validatePath 函数) - preload: openExternal 仅允许 http/https 协议 - window-manager: 启用 sandbox: true - preload: removeAllListeners 改为精确取消订阅 (返回 Unsubscribe 函数) ## 状态持久化 - activeTabId 持久化到 IndexedDB (刷新后恢复正确标签) - sidebar 状态持久化到 IndexedDB (isVisible/sidebarWidth) ## 代码优化 - preload 使用 IPC_CHANNELS 常量替代硬编码字符串 - ipc-handlers _event 类型改为 IpcMainInvokeEvent - settingsRepository 删除 splitRatio 字段
This commit is contained in:
@@ -4,21 +4,17 @@ import type { ViewMode } from '../types/settings'
|
||||
interface EditorState {
|
||||
viewMode: ViewMode
|
||||
darkMode: boolean
|
||||
splitRatio: number
|
||||
|
||||
setViewMode: (mode: ViewMode) => void
|
||||
setDarkMode: (dark: boolean) => void
|
||||
setSplitRatio: (ratio: number) => void
|
||||
toggleDarkMode: () => void
|
||||
}
|
||||
|
||||
export const useEditorStore = create<EditorState>((set) => ({
|
||||
viewMode: 'split',
|
||||
viewMode: 'editor',
|
||||
darkMode: false,
|
||||
splitRatio: 50,
|
||||
|
||||
setViewMode: (mode) => set({ viewMode: mode }),
|
||||
setDarkMode: (dark) => set({ darkMode: dark }),
|
||||
setSplitRatio: (ratio) => set({ splitRatio: ratio }),
|
||||
toggleDarkMode: () => set(state => ({ darkMode: !state.darkMode }))
|
||||
}))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { create } from 'zustand'
|
||||
import type { FileNode } from '../types/file'
|
||||
import { settingsRepository } from '../db/settingsRepository'
|
||||
|
||||
interface SidebarState {
|
||||
isVisible: boolean
|
||||
@@ -7,6 +8,7 @@ interface SidebarState {
|
||||
tree: FileNode[]
|
||||
expandedDirs: Set<string>
|
||||
sidebarWidth: number
|
||||
_loaded: boolean
|
||||
|
||||
setVisible: (visible: boolean) => void
|
||||
setRootPath: (path: string | null) => void
|
||||
@@ -14,16 +16,37 @@ interface SidebarState {
|
||||
toggleDir: (path: string) => void
|
||||
expandDirs: (paths: string[]) => void
|
||||
setSidebarWidth: (width: number) => void
|
||||
loadFromDB: () => Promise<void>
|
||||
}
|
||||
|
||||
export const useSidebarStore = create<SidebarState>((set) => ({
|
||||
export const useSidebarStore = create<SidebarState>((set, get) => ({
|
||||
isVisible: true,
|
||||
rootPath: null,
|
||||
tree: [],
|
||||
expandedDirs: new Set<string>(),
|
||||
sidebarWidth: 240,
|
||||
_loaded: false,
|
||||
|
||||
setVisible: (visible) => set({ isVisible: visible }),
|
||||
// 从 IndexedDB 加载 sidebar 设置
|
||||
loadFromDB: async () => {
|
||||
if (get()._loaded) return
|
||||
try {
|
||||
const settings = await settingsRepository.load()
|
||||
set({
|
||||
isVisible: !settings.sidebarCollapsed,
|
||||
sidebarWidth: settings.sidebarWidth,
|
||||
_loaded: true
|
||||
})
|
||||
} catch {
|
||||
set({ _loaded: true })
|
||||
}
|
||||
},
|
||||
|
||||
setVisible: (visible) => {
|
||||
set({ isVisible: visible })
|
||||
// 异步保存到 DB
|
||||
settingsRepository.save({ sidebarCollapsed: !visible })
|
||||
},
|
||||
setRootPath: (path) => set({ rootPath: path }),
|
||||
setTree: (tree) => set({ tree }),
|
||||
toggleDir: (path) =>
|
||||
@@ -48,5 +71,9 @@ export const useSidebarStore = create<SidebarState>((set) => ({
|
||||
}
|
||||
return changed ? { expandedDirs: newSet } : state
|
||||
}),
|
||||
setSidebarWidth: (width) => set({ sidebarWidth: width })
|
||||
setSidebarWidth: (width) => {
|
||||
set({ sidebarWidth: width })
|
||||
// 异步保存到 DB
|
||||
settingsRepository.save({ sidebarWidth: width })
|
||||
}
|
||||
}))
|
||||
|
||||
@@ -18,8 +18,7 @@ interface TabState {
|
||||
updateTabContent: (tabId: string, content: string) => void
|
||||
setModified: (tabId: string, modified: boolean) => void
|
||||
getActiveTab: () => Tab | null
|
||||
getTabIndex: (tabId: string) => number
|
||||
updateTabScroll: (tabId: string, scroll: Partial<Pick<Tab, 'scrollTop' | 'scrollLeft' | 'selectionStart' | 'selectionEnd' | 'previewScrollTop'>>) => void
|
||||
updateTabScroll: (tabId: string, scroll: Partial<Pick<Tab, 'scrollTop' | 'selectionStart' | 'selectionEnd'>>) => void
|
||||
loadFromDB: () => Promise<void>
|
||||
saveToDB: () => Promise<void>
|
||||
}
|
||||
@@ -34,7 +33,10 @@ export const useTabStore = create<TabState>((set, get) => ({
|
||||
loadFromDB: async () => {
|
||||
if (get()._loaded) return
|
||||
try {
|
||||
const snapshots = await tabRepository.loadAll()
|
||||
const [snapshots, savedActiveTabId] = await Promise.all([
|
||||
tabRepository.loadAll(),
|
||||
tabRepository.loadActiveTabId()
|
||||
])
|
||||
if (snapshots.length > 0) {
|
||||
const tabs: Tab[] = snapshots.map(s => ({
|
||||
id: s.id,
|
||||
@@ -42,14 +44,16 @@ export const useTabStore = create<TabState>((set, get) => ({
|
||||
content: s.content,
|
||||
isModified: s.isModified,
|
||||
scrollTop: s.scrollTop,
|
||||
scrollLeft: s.scrollLeft,
|
||||
selectionStart: s.selectionStart,
|
||||
selectionEnd: s.selectionEnd,
|
||||
previewScrollTop: s.previewScrollTop
|
||||
selectionEnd: s.selectionEnd
|
||||
}))
|
||||
// 使用保存的 activeTabId,如果不存在则取最后一个
|
||||
const activeTabId = (savedActiveTabId && tabs.find(t => t.id === savedActiveTabId))
|
||||
? savedActiveTabId
|
||||
: tabs[tabs.length - 1].id
|
||||
set({
|
||||
tabs,
|
||||
activeTabId: tabs[tabs.length - 1].id,
|
||||
activeTabId,
|
||||
_loaded: true
|
||||
})
|
||||
} else {
|
||||
@@ -73,13 +77,12 @@ export const useTabStore = create<TabState>((set, get) => ({
|
||||
content: t.content,
|
||||
isModified: t.isModified,
|
||||
scrollTop: t.scrollTop,
|
||||
scrollLeft: t.scrollLeft,
|
||||
selectionStart: t.selectionStart,
|
||||
selectionEnd: t.selectionEnd,
|
||||
previewScrollTop: t.previewScrollTop,
|
||||
updatedAt: Date.now()
|
||||
}))
|
||||
await tabRepository.saveAll(snapshots)
|
||||
await tabRepository.saveActiveTabId(get().activeTabId)
|
||||
},
|
||||
|
||||
createTab: (filePath = null, content = '') => {
|
||||
@@ -97,10 +100,8 @@ export const useTabStore = create<TabState>((set, get) => ({
|
||||
content,
|
||||
isModified: false,
|
||||
scrollTop: 0,
|
||||
scrollLeft: 0,
|
||||
selectionStart: 0,
|
||||
selectionEnd: 0,
|
||||
previewScrollTop: 0
|
||||
selectionEnd: 0
|
||||
}
|
||||
|
||||
set(state => ({
|
||||
@@ -187,6 +188,8 @@ export const useTabStore = create<TabState>((set, get) => ({
|
||||
: state.mruStack
|
||||
return { activeTabId: tabId, mruStack: newMru }
|
||||
})
|
||||
// 异步保存 activeTabId
|
||||
setTimeout(() => tabRepository.saveActiveTabId(tabId), 0)
|
||||
},
|
||||
|
||||
updateTabContent: (tabId, content) => {
|
||||
@@ -210,10 +213,6 @@ export const useTabStore = create<TabState>((set, get) => ({
|
||||
return tabs.find(t => t.id === activeTabId) ?? null
|
||||
},
|
||||
|
||||
getTabIndex: (tabId) => {
|
||||
return get().tabs.findIndex(t => t.id === tabId)
|
||||
},
|
||||
|
||||
updateTabScroll: (tabId, scroll) => {
|
||||
set(state => ({
|
||||
tabs: state.tabs.map(t =>
|
||||
|
||||
Reference in New Issue
Block a user