mirror of
https://github.com/LHY0125/PathEditor.git
synced 2026-06-29 09:55:56 +08:00
chore: 同步 v5.0 基础设施完善到 v5.1
从 v5.0 cherry-pick 的开源项目基础设施改进: 新增配置文件: - .editorconfig, .gitattributes, .prettierrc, .markdownlint.json - commitlint.config.js 新增 GitHub 社区文件: - .github/dependabot.yml — 依赖自动更新 - .github/CODEOWNERS — 自动 PR 审查分配 - .github/FUNDING.yml — 开源赞助入口 新增文档: - ROADMAP.md — 路线图 - SUPPORT.md — 帮助指南 - docs/screenshots/ — 应用截图 新增 Git Hooks: - .husky/pre-commit — lint-staged 自动格式化+修复 - .husky/commit-msg — commitlint 校验 CI 强化: - 新增 Prettier 格式检查 - 新增 Vitest 覆盖率 + Codecov 上报 - 保留 v5.1 已有的 rust-cache + jsdom 全局环境 修复: - index.html 标题 v4.0 → v5.1 - PathEditDialog set-state-in-effect 改用 useRef prevOpen 守卫 - merge-preview.test.tsx no-explicit-any 修复 - 所有 TS/TSX 文件 Prettier 格式化统一 v5.1 保留特性: - @tanstack/react-virtual 虚拟滚动 - jsdom 全局测试环境 - Swatinem/rust-cache CI 加速 - 105 测试全部通过
This commit is contained in:
+389
-318
@@ -54,11 +54,12 @@ interface AppState {
|
||||
loadPaths: () => Promise<void>;
|
||||
savePaths: (force?: boolean) => Promise<SaveResult>;
|
||||
initialize: () => Promise<void>;
|
||||
|
||||
}
|
||||
|
||||
function arraysEqual(a: readonly PathEntry[], b: readonly PathEntry[]): boolean {
|
||||
return a.length === b.length && a.every((v, i) => v.path === b[i].path && v.enabled === b[i].enabled);
|
||||
return (
|
||||
a.length === b.length && a.every((v, i) => v.path === b[i].path && v.enabled === b[i].enabled)
|
||||
);
|
||||
}
|
||||
|
||||
export const useAppStore = create<AppState>((set, get) => {
|
||||
@@ -69,335 +70,405 @@ export const useAppStore = create<AppState>((set, get) => {
|
||||
|
||||
return {
|
||||
sysPaths: [],
|
||||
userPaths: [],
|
||||
undoRedo: new UndoRedoManager(appConfig.undo.maxHistory),
|
||||
_savedSys: [],
|
||||
_savedUser: [],
|
||||
userPaths: [],
|
||||
undoRedo: new UndoRedoManager(appConfig.undo.maxHistory),
|
||||
_savedSys: [],
|
||||
_savedUser: [],
|
||||
|
||||
activeTab: 'system',
|
||||
searchQuery: '',
|
||||
selectedIndices: [],
|
||||
isAdmin: false,
|
||||
statusMessage: '',
|
||||
isModified: false,
|
||||
isLoading: true,
|
||||
isSaving: false,
|
||||
activeTab: 'system',
|
||||
searchQuery: '',
|
||||
selectedIndices: [],
|
||||
isAdmin: false,
|
||||
statusMessage: '',
|
||||
isModified: false,
|
||||
isLoading: true,
|
||||
isSaving: false,
|
||||
|
||||
setActiveTab: (tab) => set({ activeTab: tab }),
|
||||
setSearchQuery: (query) => set({ searchQuery: query }),
|
||||
setSelectedIndices: (indices) => set({ selectedIndices: indices }),
|
||||
setStatusMessage: (msg) => set({ statusMessage: msg }),
|
||||
setActiveTab: (tab) => set({ activeTab: tab }),
|
||||
setSearchQuery: (query) => set({ searchQuery: query }),
|
||||
setSelectedIndices: (indices) => set({ selectedIndices: indices }),
|
||||
setStatusMessage: (msg) => set({ statusMessage: msg }),
|
||||
|
||||
addPath: (path, target) => {
|
||||
const state = get();
|
||||
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
|
||||
const entry: PathEntry = { path, enabled: true };
|
||||
const newList = [...list, entry];
|
||||
state.undoRedo.push({
|
||||
type: OperationType.ADD, target, index: newList.length - 1, count: 1,
|
||||
oldPaths: [], newPaths: [entry],
|
||||
});
|
||||
if (target === TargetType.SYSTEM) set({ sysPaths: newList });
|
||||
else set({ userPaths: newList });
|
||||
markDirty();
|
||||
},
|
||||
|
||||
editPath: (index, newPath, target) => {
|
||||
const state = get();
|
||||
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
|
||||
const oldEntry = list[index];
|
||||
if (!oldEntry) return;
|
||||
const newEntry: PathEntry = { path: newPath, enabled: oldEntry.enabled };
|
||||
state.undoRedo.push({
|
||||
type: OperationType.EDIT, target, index, count: 1,
|
||||
oldPaths: [oldEntry], newPaths: [newEntry],
|
||||
});
|
||||
const newList = [...list];
|
||||
newList[index] = newEntry;
|
||||
if (target === TargetType.SYSTEM) set({ sysPaths: newList });
|
||||
else set({ userPaths: newList });
|
||||
markDirty();
|
||||
},
|
||||
|
||||
deletePaths: (indices, target) => {
|
||||
if (indices.length === 0) return;
|
||||
const state = get();
|
||||
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
|
||||
const sortedDesc = [...indices].sort((a, b) => b - a);
|
||||
const sortedAsc = [...indices].sort((a, b) => a - b);
|
||||
const oldPaths = sortedAsc.map((i) => list[i]);
|
||||
|
||||
state.undoRedo.push({
|
||||
type: OperationType.DELETE, target,
|
||||
index: sortedAsc[0], count: sortedAsc.length,
|
||||
oldPaths, newPaths: [],
|
||||
indices: sortedAsc,
|
||||
});
|
||||
|
||||
const toRemove = new Set(sortedDesc);
|
||||
const newList = list.filter((_, i) => !toRemove.has(i));
|
||||
if (target === TargetType.SYSTEM) set({ sysPaths: newList, selectedIndices: [] });
|
||||
else set({ userPaths: newList, selectedIndices: [] });
|
||||
markDirty();
|
||||
},
|
||||
|
||||
moveUp: (index, target) => {
|
||||
if (index <= 0) return;
|
||||
const state = get();
|
||||
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
|
||||
state.undoRedo.push({
|
||||
type: OperationType.MOVE_UP, target, index, count: 1, oldPaths: [], newPaths: [],
|
||||
});
|
||||
const newList = [...list];
|
||||
[newList[index - 1], newList[index]] = [newList[index], newList[index - 1]];
|
||||
if (target === TargetType.SYSTEM) set({ sysPaths: newList, selectedIndices: [index - 1] });
|
||||
else set({ userPaths: newList, selectedIndices: [index - 1] });
|
||||
markDirty();
|
||||
},
|
||||
|
||||
moveDown: (index, target) => {
|
||||
const state = get();
|
||||
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
|
||||
if (index >= list.length - 1) return;
|
||||
state.undoRedo.push({
|
||||
type: OperationType.MOVE_DOWN, target, index, count: 1, oldPaths: [], newPaths: [],
|
||||
});
|
||||
const newList = [...list];
|
||||
[newList[index], newList[index + 1]] = [newList[index + 1], newList[index]];
|
||||
if (target === TargetType.SYSTEM) set({ sysPaths: newList, selectedIndices: [index + 1] });
|
||||
else set({ userPaths: newList, selectedIndices: [index + 1] });
|
||||
markDirty();
|
||||
},
|
||||
|
||||
cleanPaths: (target, validateFn) => {
|
||||
const state = get();
|
||||
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
|
||||
const [kept, removed] = pathClean(list, validateFn);
|
||||
|
||||
if (removed.length > 0) {
|
||||
addPath: (path, target) => {
|
||||
const state = get();
|
||||
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
|
||||
const entry: PathEntry = { path, enabled: true };
|
||||
const newList = [...list, entry];
|
||||
state.undoRedo.push({
|
||||
type: OperationType.CLEAN, target, index: 0, count: removed.length,
|
||||
oldPaths: [...list], newPaths: kept,
|
||||
type: OperationType.ADD,
|
||||
target,
|
||||
index: newList.length - 1,
|
||||
count: 1,
|
||||
oldPaths: [],
|
||||
newPaths: [entry],
|
||||
});
|
||||
if (target === TargetType.SYSTEM) set({ sysPaths: kept, selectedIndices: [] });
|
||||
else set({ userPaths: kept, selectedIndices: [] });
|
||||
if (target === TargetType.SYSTEM) set({ sysPaths: newList });
|
||||
else set({ userPaths: newList });
|
||||
markDirty();
|
||||
}
|
||||
},
|
||||
|
||||
return removed.map(e => e.path);
|
||||
},
|
||||
|
||||
replacePaths: (target, newPaths) => {
|
||||
if (newPaths.length === 0) return;
|
||||
const state = get();
|
||||
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
|
||||
const entries: PathEntry[] = newPaths.map(p => ({ path: p, enabled: true }));
|
||||
|
||||
state.undoRedo.push({
|
||||
type: OperationType.IMPORT, target, index: 0, count: entries.length,
|
||||
oldPaths: [...list], newPaths: [...entries],
|
||||
});
|
||||
|
||||
if (target === TargetType.SYSTEM) set({ sysPaths: [...entries], selectedIndices: [] });
|
||||
else set({ userPaths: [...entries], selectedIndices: [] });
|
||||
markDirty();
|
||||
},
|
||||
|
||||
replaceBothPaths: (sysPaths, userPaths) => {
|
||||
const state = get();
|
||||
const sysEntries: PathEntry[] = sysPaths.map(p => ({ path: p, enabled: true }));
|
||||
const usrEntries: PathEntry[] = userPaths.map(p => ({ path: p, enabled: true }));
|
||||
state.undoRedo.push({
|
||||
type: OperationType.IMPORT_BOTH, target: TargetType.SYSTEM, index: 0,
|
||||
count: sysEntries.length + usrEntries.length,
|
||||
oldPaths: [...state.sysPaths], newPaths: [...sysEntries],
|
||||
oldPathsOther: [...state.userPaths], newPathsOther: [...usrEntries],
|
||||
});
|
||||
set({ sysPaths: [...sysEntries], userPaths: [...usrEntries], selectedIndices: [] });
|
||||
markDirty();
|
||||
},
|
||||
|
||||
clearPaths: (target) => {
|
||||
const state = get();
|
||||
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
|
||||
if (list.length === 0) return;
|
||||
|
||||
state.undoRedo.push({
|
||||
type: OperationType.CLEAR, target, index: 0, count: list.length,
|
||||
oldPaths: [...list], newPaths: [],
|
||||
});
|
||||
|
||||
if (target === TargetType.SYSTEM) set({ sysPaths: [] });
|
||||
else set({ userPaths: [] });
|
||||
markDirty();
|
||||
},
|
||||
|
||||
togglePath: (index, target) => {
|
||||
const state = get();
|
||||
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
|
||||
const oldEntry = list[index];
|
||||
if (!oldEntry) return;
|
||||
const newEntry: PathEntry = { path: oldEntry.path, enabled: !oldEntry.enabled };
|
||||
|
||||
state.undoRedo.push({
|
||||
type: OperationType.TOGGLE, target, index, count: 1,
|
||||
oldPaths: [oldEntry], newPaths: [newEntry],
|
||||
});
|
||||
|
||||
const newList = [...list];
|
||||
newList[index] = newEntry;
|
||||
if (target === TargetType.SYSTEM) set({ sysPaths: newList });
|
||||
else set({ userPaths: newList });
|
||||
markDirty();
|
||||
|
||||
// 即时保存禁用状态
|
||||
const { sysPaths: sys, userPaths: usr } = get();
|
||||
const sysDisabled = sys.filter(e => !e.enabled).map(e => e.path);
|
||||
const usrDisabled = usr.filter(e => !e.enabled).map(e => e.path);
|
||||
invoke('save_disabled_state', { system: sysDisabled, user: usrDisabled })
|
||||
.catch((e) => console.warn('保存禁用状态失败:', e));
|
||||
},
|
||||
|
||||
undo: () => {
|
||||
const { undoRedo, sysPaths, userPaths, _savedSys, _savedUser } = get();
|
||||
const result = undoRedo.undo(sysPaths, userPaths);
|
||||
if (result) {
|
||||
set({
|
||||
sysPaths: result[0], userPaths: result[1], selectedIndices: [],
|
||||
// 内联计算 isModified 而非调用 markDirty(),避免两次 set() 导致额外渲染
|
||||
isModified: !(arraysEqual(result[0], _savedSys) && arraysEqual(result[1], _savedUser)),
|
||||
editPath: (index, newPath, target) => {
|
||||
const state = get();
|
||||
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
|
||||
const oldEntry = list[index];
|
||||
if (!oldEntry) return;
|
||||
const newEntry: PathEntry = { path: newPath, enabled: oldEntry.enabled };
|
||||
state.undoRedo.push({
|
||||
type: OperationType.EDIT,
|
||||
target,
|
||||
index,
|
||||
count: 1,
|
||||
oldPaths: [oldEntry],
|
||||
newPaths: [newEntry],
|
||||
});
|
||||
// 同步持久化 disabled 状态,与 togglePath 保持一致
|
||||
invoke('save_disabled_state', {
|
||||
system: result[0].filter(e => !e.enabled).map(e => e.path),
|
||||
user: result[1].filter(e => !e.enabled).map(e => e.path),
|
||||
}).catch((e) => console.warn('保存禁用状态失败:', e));
|
||||
}
|
||||
},
|
||||
const newList = [...list];
|
||||
newList[index] = newEntry;
|
||||
if (target === TargetType.SYSTEM) set({ sysPaths: newList });
|
||||
else set({ userPaths: newList });
|
||||
markDirty();
|
||||
},
|
||||
|
||||
redo: () => {
|
||||
const { undoRedo, sysPaths, userPaths, _savedSys, _savedUser } = get();
|
||||
const result = undoRedo.redo(sysPaths, userPaths);
|
||||
if (result) {
|
||||
set({
|
||||
sysPaths: result[0], userPaths: result[1], selectedIndices: [],
|
||||
// 内联计算 isModified 而非调用 markDirty(),避免两次 set() 导致额外渲染
|
||||
isModified: !(arraysEqual(result[0], _savedSys) && arraysEqual(result[1], _savedUser)),
|
||||
deletePaths: (indices, target) => {
|
||||
if (indices.length === 0) return;
|
||||
const state = get();
|
||||
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
|
||||
const sortedDesc = [...indices].sort((a, b) => b - a);
|
||||
const sortedAsc = [...indices].sort((a, b) => a - b);
|
||||
const oldPaths = sortedAsc.map((i) => list[i]);
|
||||
|
||||
state.undoRedo.push({
|
||||
type: OperationType.DELETE,
|
||||
target,
|
||||
index: sortedAsc[0],
|
||||
count: sortedAsc.length,
|
||||
oldPaths,
|
||||
newPaths: [],
|
||||
indices: sortedAsc,
|
||||
});
|
||||
// 同步持久化 disabled 状态,与 togglePath 保持一致
|
||||
invoke('save_disabled_state', {
|
||||
system: result[0].filter(e => !e.enabled).map(e => e.path),
|
||||
user: result[1].filter(e => !e.enabled).map(e => e.path),
|
||||
}).catch((e) => console.warn('保存禁用状态失败:', e));
|
||||
}
|
||||
},
|
||||
|
||||
loadPaths: async () => {
|
||||
try {
|
||||
set({ isLoading: true });
|
||||
const [sysArr, userArr] = await Promise.all([
|
||||
invoke<string[]>('load_system_paths'),
|
||||
invoke<string[]>('load_user_paths'),
|
||||
const toRemove = new Set(sortedDesc);
|
||||
const newList = list.filter((_, i) => !toRemove.has(i));
|
||||
if (target === TargetType.SYSTEM) set({ sysPaths: newList, selectedIndices: [] });
|
||||
else set({ userPaths: newList, selectedIndices: [] });
|
||||
markDirty();
|
||||
},
|
||||
|
||||
moveUp: (index, target) => {
|
||||
if (index <= 0) return;
|
||||
const state = get();
|
||||
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
|
||||
state.undoRedo.push({
|
||||
type: OperationType.MOVE_UP,
|
||||
target,
|
||||
index,
|
||||
count: 1,
|
||||
oldPaths: [],
|
||||
newPaths: [],
|
||||
});
|
||||
const newList = [...list];
|
||||
[newList[index - 1], newList[index]] = [newList[index], newList[index - 1]];
|
||||
if (target === TargetType.SYSTEM) set({ sysPaths: newList, selectedIndices: [index - 1] });
|
||||
else set({ userPaths: newList, selectedIndices: [index - 1] });
|
||||
markDirty();
|
||||
},
|
||||
|
||||
moveDown: (index, target) => {
|
||||
const state = get();
|
||||
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
|
||||
if (index >= list.length - 1) return;
|
||||
state.undoRedo.push({
|
||||
type: OperationType.MOVE_DOWN,
|
||||
target,
|
||||
index,
|
||||
count: 1,
|
||||
oldPaths: [],
|
||||
newPaths: [],
|
||||
});
|
||||
const newList = [...list];
|
||||
[newList[index], newList[index + 1]] = [newList[index + 1], newList[index]];
|
||||
if (target === TargetType.SYSTEM) set({ sysPaths: newList, selectedIndices: [index + 1] });
|
||||
else set({ userPaths: newList, selectedIndices: [index + 1] });
|
||||
markDirty();
|
||||
},
|
||||
|
||||
cleanPaths: (target, validateFn) => {
|
||||
const state = get();
|
||||
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
|
||||
const [kept, removed] = pathClean(list, validateFn);
|
||||
|
||||
if (removed.length > 0) {
|
||||
state.undoRedo.push({
|
||||
type: OperationType.CLEAN,
|
||||
target,
|
||||
index: 0,
|
||||
count: removed.length,
|
||||
oldPaths: [...list],
|
||||
newPaths: kept,
|
||||
});
|
||||
if (target === TargetType.SYSTEM) set({ sysPaths: kept, selectedIndices: [] });
|
||||
else set({ userPaths: kept, selectedIndices: [] });
|
||||
markDirty();
|
||||
}
|
||||
|
||||
return removed.map((e) => e.path);
|
||||
},
|
||||
|
||||
replacePaths: (target, newPaths) => {
|
||||
if (newPaths.length === 0) return;
|
||||
const state = get();
|
||||
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
|
||||
const entries: PathEntry[] = newPaths.map((p) => ({ path: p, enabled: true }));
|
||||
|
||||
state.undoRedo.push({
|
||||
type: OperationType.IMPORT,
|
||||
target,
|
||||
index: 0,
|
||||
count: entries.length,
|
||||
oldPaths: [...list],
|
||||
newPaths: [...entries],
|
||||
});
|
||||
|
||||
if (target === TargetType.SYSTEM) set({ sysPaths: [...entries], selectedIndices: [] });
|
||||
else set({ userPaths: [...entries], selectedIndices: [] });
|
||||
markDirty();
|
||||
},
|
||||
|
||||
replaceBothPaths: (sysPaths, userPaths) => {
|
||||
const state = get();
|
||||
const sysEntries: PathEntry[] = sysPaths.map((p) => ({ path: p, enabled: true }));
|
||||
const usrEntries: PathEntry[] = userPaths.map((p) => ({ path: p, enabled: true }));
|
||||
state.undoRedo.push({
|
||||
type: OperationType.IMPORT_BOTH,
|
||||
target: TargetType.SYSTEM,
|
||||
index: 0,
|
||||
count: sysEntries.length + usrEntries.length,
|
||||
oldPaths: [...state.sysPaths],
|
||||
newPaths: [...sysEntries],
|
||||
oldPathsOther: [...state.userPaths],
|
||||
newPathsOther: [...usrEntries],
|
||||
});
|
||||
set({ sysPaths: [...sysEntries], userPaths: [...usrEntries], selectedIndices: [] });
|
||||
markDirty();
|
||||
},
|
||||
|
||||
clearPaths: (target) => {
|
||||
const state = get();
|
||||
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
|
||||
if (list.length === 0) return;
|
||||
|
||||
state.undoRedo.push({
|
||||
type: OperationType.CLEAR,
|
||||
target,
|
||||
index: 0,
|
||||
count: list.length,
|
||||
oldPaths: [...list],
|
||||
newPaths: [],
|
||||
});
|
||||
|
||||
if (target === TargetType.SYSTEM) set({ sysPaths: [] });
|
||||
else set({ userPaths: [] });
|
||||
markDirty();
|
||||
},
|
||||
|
||||
togglePath: (index, target) => {
|
||||
const state = get();
|
||||
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
|
||||
const oldEntry = list[index];
|
||||
if (!oldEntry) return;
|
||||
const newEntry: PathEntry = { path: oldEntry.path, enabled: !oldEntry.enabled };
|
||||
|
||||
state.undoRedo.push({
|
||||
type: OperationType.TOGGLE,
|
||||
target,
|
||||
index,
|
||||
count: 1,
|
||||
oldPaths: [oldEntry],
|
||||
newPaths: [newEntry],
|
||||
});
|
||||
|
||||
const newList = [...list];
|
||||
newList[index] = newEntry;
|
||||
if (target === TargetType.SYSTEM) set({ sysPaths: newList });
|
||||
else set({ userPaths: newList });
|
||||
markDirty();
|
||||
|
||||
// 即时保存禁用状态
|
||||
const { sysPaths: sys, userPaths: usr } = get();
|
||||
const sysDisabled = sys.filter((e) => !e.enabled).map((e) => e.path);
|
||||
const usrDisabled = usr.filter((e) => !e.enabled).map((e) => e.path);
|
||||
invoke('save_disabled_state', { system: sysDisabled, user: usrDisabled }).catch((e) =>
|
||||
console.warn('保存禁用状态失败:', e),
|
||||
);
|
||||
},
|
||||
|
||||
undo: () => {
|
||||
const { undoRedo, sysPaths, userPaths, _savedSys, _savedUser } = get();
|
||||
const result = undoRedo.undo(sysPaths, userPaths);
|
||||
if (result) {
|
||||
set({
|
||||
sysPaths: result[0],
|
||||
userPaths: result[1],
|
||||
selectedIndices: [],
|
||||
// 内联计算 isModified 而非调用 markDirty(),避免两次 set() 导致额外渲染
|
||||
isModified: !(arraysEqual(result[0], _savedSys) && arraysEqual(result[1], _savedUser)),
|
||||
});
|
||||
// 同步持久化 disabled 状态,与 togglePath 保持一致
|
||||
invoke('save_disabled_state', {
|
||||
system: result[0].filter((e) => !e.enabled).map((e) => e.path),
|
||||
user: result[1].filter((e) => !e.enabled).map((e) => e.path),
|
||||
}).catch((e) => console.warn('保存禁用状态失败:', e));
|
||||
}
|
||||
},
|
||||
|
||||
redo: () => {
|
||||
const { undoRedo, sysPaths, userPaths, _savedSys, _savedUser } = get();
|
||||
const result = undoRedo.redo(sysPaths, userPaths);
|
||||
if (result) {
|
||||
set({
|
||||
sysPaths: result[0],
|
||||
userPaths: result[1],
|
||||
selectedIndices: [],
|
||||
// 内联计算 isModified 而非调用 markDirty(),避免两次 set() 导致额外渲染
|
||||
isModified: !(arraysEqual(result[0], _savedSys) && arraysEqual(result[1], _savedUser)),
|
||||
});
|
||||
// 同步持久化 disabled 状态,与 togglePath 保持一致
|
||||
invoke('save_disabled_state', {
|
||||
system: result[0].filter((e) => !e.enabled).map((e) => e.path),
|
||||
user: result[1].filter((e) => !e.enabled).map((e) => e.path),
|
||||
}).catch((e) => console.warn('保存禁用状态失败:', e));
|
||||
}
|
||||
},
|
||||
|
||||
loadPaths: async () => {
|
||||
try {
|
||||
set({ isLoading: true });
|
||||
const [sysArr, userArr] = await Promise.all([
|
||||
invoke<string[]>('load_system_paths'),
|
||||
invoke<string[]>('load_user_paths'),
|
||||
]);
|
||||
|
||||
// 加载禁用状态(文件不存在时返回空)
|
||||
let sysDisabled: string[] = [];
|
||||
let usrDisabled: string[] = [];
|
||||
try {
|
||||
const result = await invoke<[string[], string[]]>('load_disabled_state');
|
||||
sysDisabled = result[0];
|
||||
usrDisabled = result[1];
|
||||
} catch {
|
||||
// 文件不存在或损坏,忽略
|
||||
}
|
||||
|
||||
const sysSet = new Set(sysDisabled);
|
||||
const usrSet = new Set(usrDisabled);
|
||||
|
||||
const sysEntries: PathEntry[] = sysArr.map((p) => ({ path: p, enabled: !sysSet.has(p) }));
|
||||
const usrEntries: PathEntry[] = userArr.map((p) => ({ path: p, enabled: !usrSet.has(p) }));
|
||||
|
||||
set({
|
||||
sysPaths: sysEntries,
|
||||
userPaths: usrEntries,
|
||||
_savedSys: [...sysEntries],
|
||||
_savedUser: [...usrEntries],
|
||||
undoRedo: new UndoRedoManager(appConfig.undo.maxHistory),
|
||||
isLoading: false,
|
||||
isModified: false,
|
||||
statusMessage: i18n.t('status.loaded', {
|
||||
sysCount: sysArr.length,
|
||||
userCount: userArr.length,
|
||||
}),
|
||||
});
|
||||
} catch (e) {
|
||||
set({ isLoading: false, statusMessage: `${i18n.t('status.error')}: ${String(e)}` });
|
||||
}
|
||||
},
|
||||
|
||||
savePaths: async (force?: boolean) => {
|
||||
const state = get();
|
||||
if (state.isSaving) return { kind: 'blocked' };
|
||||
set({ isSaving: true, statusMessage: i18n.t('status.saving') });
|
||||
|
||||
// 只保存 enabled 的路径到注册表
|
||||
const sysPaths = state.sysPaths.filter((e) => e.enabled).map((e) => e.path);
|
||||
const userPaths = state.userPaths.filter((e) => e.enabled).map((e) => e.path);
|
||||
const sysJoined = sysPaths.join(';');
|
||||
const userJoined = userPaths.join(';');
|
||||
|
||||
// 长度检查:非强制模式下返回警告,由 UI 层确认
|
||||
const { maxSystemLength, maxUserLength, maxCombinedLength } = appConfig.path;
|
||||
if (
|
||||
!force &&
|
||||
(sysJoined.length > maxSystemLength ||
|
||||
userJoined.length > maxUserLength ||
|
||||
(sysJoined + userJoined).length > maxCombinedLength)
|
||||
) {
|
||||
set({ isSaving: false, statusMessage: i18n.t('status.saveWarningLongPaths') });
|
||||
return { kind: 'warning', reason: 'lengthExceeded' };
|
||||
}
|
||||
|
||||
// 备份当前注册表(保存前备份旧值,失败仅警告不中断)
|
||||
let backupFailed = false;
|
||||
await invoke('backup_registry', { customDir: null }).catch(() => {
|
||||
backupFailed = true;
|
||||
});
|
||||
|
||||
const origSys = state._savedSys.filter((e) => e.enabled).map((e) => e.path);
|
||||
const origUser = state._savedUser.filter((e) => e.enabled).map((e) => e.path);
|
||||
|
||||
const [sysResult, userResult] = await Promise.allSettled([
|
||||
invoke('save_system_paths', { paths: sysPaths, original: origSys }),
|
||||
invoke('save_user_paths', { paths: userPaths, original: origUser }),
|
||||
]);
|
||||
|
||||
// 加载禁用状态(文件不存在时返回空)
|
||||
let sysDisabled: string[] = [];
|
||||
let usrDisabled: string[] = [];
|
||||
try {
|
||||
const result = await invoke<[string[], string[]]>('load_disabled_state');
|
||||
sysDisabled = result[0];
|
||||
usrDisabled = result[1];
|
||||
} catch {
|
||||
// 文件不存在或损坏,忽略
|
||||
}
|
||||
const sysOk = sysResult.status === 'fulfilled';
|
||||
const userOk = userResult.status === 'fulfilled';
|
||||
|
||||
const sysSet = new Set(sysDisabled);
|
||||
const usrSet = new Set(usrDisabled);
|
||||
|
||||
const sysEntries: PathEntry[] = sysArr.map(p => ({ path: p, enabled: !sysSet.has(p) }));
|
||||
const usrEntries: PathEntry[] = userArr.map(p => ({ path: p, enabled: !usrSet.has(p) }));
|
||||
|
||||
set({
|
||||
sysPaths: sysEntries, userPaths: usrEntries,
|
||||
_savedSys: [...sysEntries], _savedUser: [...usrEntries],
|
||||
undoRedo: new UndoRedoManager(appConfig.undo.maxHistory),
|
||||
isLoading: false, isModified: false,
|
||||
statusMessage: i18n.t('status.loaded', { sysCount: sysArr.length, userCount: userArr.length }),
|
||||
});
|
||||
} catch (e) {
|
||||
set({ isLoading: false, statusMessage: `${i18n.t('status.error')}: ${String(e)}` });
|
||||
}
|
||||
},
|
||||
|
||||
savePaths: async (force?: boolean) => {
|
||||
const state = get();
|
||||
if (state.isSaving) return { kind: 'blocked' };
|
||||
set({ isSaving: true, statusMessage: i18n.t('status.saving') });
|
||||
|
||||
// 只保存 enabled 的路径到注册表
|
||||
const sysPaths = state.sysPaths.filter(e => e.enabled).map(e => e.path);
|
||||
const userPaths = state.userPaths.filter(e => e.enabled).map(e => e.path);
|
||||
const sysJoined = sysPaths.join(';');
|
||||
const userJoined = userPaths.join(';');
|
||||
|
||||
// 长度检查:非强制模式下返回警告,由 UI 层确认
|
||||
const { maxSystemLength, maxUserLength, maxCombinedLength } = appConfig.path;
|
||||
if (!force && (sysJoined.length > maxSystemLength || userJoined.length > maxUserLength || (sysJoined + userJoined).length > maxCombinedLength)) {
|
||||
set({ isSaving: false, statusMessage: i18n.t('status.saveWarningLongPaths') });
|
||||
return { kind: 'warning', reason: 'lengthExceeded' };
|
||||
}
|
||||
|
||||
// 备份当前注册表(保存前备份旧值,失败仅警告不中断)
|
||||
let backupFailed = false;
|
||||
await invoke('backup_registry', { customDir: null })
|
||||
.catch(() => { backupFailed = true; });
|
||||
|
||||
const origSys = state._savedSys.filter(e => e.enabled).map(e => e.path);
|
||||
const origUser = state._savedUser.filter(e => e.enabled).map(e => e.path);
|
||||
|
||||
const [sysResult, userResult] = await Promise.allSettled([
|
||||
invoke('save_system_paths', { paths: sysPaths, original: origSys }),
|
||||
invoke('save_user_paths', { paths: userPaths, original: origUser }),
|
||||
]);
|
||||
|
||||
const sysOk = sysResult.status === 'fulfilled';
|
||||
const userOk = userResult.status === 'fulfilled';
|
||||
|
||||
if (sysOk && userOk) {
|
||||
invoke('broadcast_env_change').catch(() => {});
|
||||
const savedSys = [...state.sysPaths], savedUser = [...state.userPaths];
|
||||
set({ isModified: false, isSaving: false,
|
||||
statusMessage: backupFailed ? i18n.t('status.saved_without_backup') : i18n.t('status.saved'),
|
||||
_savedSys: savedSys, _savedUser: savedUser });
|
||||
return { kind: 'success' };
|
||||
} else {
|
||||
const sysErr = (!sysOk && sysResult.status === 'rejected') ? String(sysResult.reason) : '';
|
||||
const usrErr = (!userOk && userResult.status === 'rejected') ? String(userResult.reason) : '';
|
||||
const parts = [sysErr, usrErr].filter(Boolean);
|
||||
|
||||
const msg = sysOk ? `用户 PATH 保存失败: ${usrErr}` : userOk ? `系统 PATH 保存失败: ${sysErr}` : `保存失败: ${parts.join('; ')}`;
|
||||
|
||||
if (sysOk || userOk) {
|
||||
// partial success
|
||||
set({ isSaving: false });
|
||||
await get().loadPaths(); // reload to avoid state drift
|
||||
set({ statusMessage: msg }); // restore the error message overwritten by loadPaths
|
||||
return { kind: 'partial', message: msg };
|
||||
if (sysOk && userOk) {
|
||||
invoke('broadcast_env_change').catch(() => {});
|
||||
const savedSys = [...state.sysPaths],
|
||||
savedUser = [...state.userPaths];
|
||||
set({
|
||||
isModified: false,
|
||||
isSaving: false,
|
||||
statusMessage: backupFailed
|
||||
? i18n.t('status.saved_without_backup')
|
||||
: i18n.t('status.saved'),
|
||||
_savedSys: savedSys,
|
||||
_savedUser: savedUser,
|
||||
});
|
||||
return { kind: 'success' };
|
||||
} else {
|
||||
set({ isSaving: false, statusMessage: msg });
|
||||
return { kind: 'failure', message: msg };
|
||||
}
|
||||
}
|
||||
},
|
||||
const sysErr = !sysOk && sysResult.status === 'rejected' ? String(sysResult.reason) : '';
|
||||
const usrErr = !userOk && userResult.status === 'rejected' ? String(userResult.reason) : '';
|
||||
const parts = [sysErr, usrErr].filter(Boolean);
|
||||
|
||||
initialize: async () => {
|
||||
try {
|
||||
const isAdmin: boolean = await invoke('check_admin');
|
||||
set({ isAdmin });
|
||||
if (!isAdmin) set({ statusMessage: i18n.t('status.readonly') });
|
||||
} catch {
|
||||
set({ isAdmin: false, statusMessage: i18n.t('status.readonly') });
|
||||
}
|
||||
await get().loadPaths();
|
||||
},
|
||||
};});
|
||||
const msg = sysOk
|
||||
? `用户 PATH 保存失败: ${usrErr}`
|
||||
: userOk
|
||||
? `系统 PATH 保存失败: ${sysErr}`
|
||||
: `保存失败: ${parts.join('; ')}`;
|
||||
|
||||
if (sysOk || userOk) {
|
||||
// partial success
|
||||
set({ isSaving: false });
|
||||
await get().loadPaths(); // reload to avoid state drift
|
||||
set({ statusMessage: msg }); // restore the error message overwritten by loadPaths
|
||||
return { kind: 'partial', message: msg };
|
||||
} else {
|
||||
set({ isSaving: false, statusMessage: msg });
|
||||
return { kind: 'failure', message: msg };
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
initialize: async () => {
|
||||
try {
|
||||
const isAdmin: boolean = await invoke('check_admin');
|
||||
set({ isAdmin });
|
||||
if (!isAdmin) set({ statusMessage: i18n.t('status.readonly') });
|
||||
} catch {
|
||||
set({ isAdmin: false, statusMessage: i18n.t('status.readonly') });
|
||||
}
|
||||
await get().loadPaths();
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user