mirror of
https://github.com/LHY0125/PathEditor.git
synced 2026-06-29 09:55:56 +08:00
be375ed3ad
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
297 lines
10 KiB
TypeScript
297 lines
10 KiB
TypeScript
import { create } from 'zustand';
|
|
import { invoke } from '@tauri-apps/api/core';
|
|
import i18n from '@/i18n';
|
|
import { UndoRedoManager, OperationType, TargetType } from '@/core/undo-redo';
|
|
import { pathClean } from '@/core/path-manager';
|
|
import appConfig from '@/config/default.json';
|
|
|
|
export type TabId = 'system' | 'user' | 'merged';
|
|
|
|
interface AppState {
|
|
sysPaths: string[];
|
|
userPaths: string[];
|
|
undoRedo: UndoRedoManager;
|
|
_savedSys: string[]; // 上次保存时的快照,用于 isModified 判断
|
|
_savedUser: string[];
|
|
|
|
activeTab: TabId;
|
|
searchQuery: string;
|
|
selectedIndices: number[];
|
|
isAdmin: boolean;
|
|
statusMessage: string;
|
|
isModified: boolean;
|
|
isLoading: boolean;
|
|
isSaving: boolean;
|
|
|
|
setActiveTab: (tab: TabId) => void;
|
|
setSearchQuery: (query: string) => void;
|
|
setSelectedIndices: (indices: number[]) => void;
|
|
setStatusMessage: (msg: string) => void;
|
|
|
|
addPath: (path: string, target: TargetType) => void;
|
|
editPath: (index: number, newPath: string, target: TargetType) => void;
|
|
deletePaths: (indices: number[], target: TargetType) => void;
|
|
moveUp: (index: number, target: TargetType) => void;
|
|
moveDown: (index: number, target: TargetType) => void;
|
|
cleanPaths: (target: TargetType, validateFn: (p: string) => boolean) => string[];
|
|
importPaths: (target: TargetType, importPaths: string[]) => void;
|
|
clearPaths: (target: TargetType) => void;
|
|
|
|
undo: () => void;
|
|
redo: () => void;
|
|
|
|
loadPaths: () => Promise<void>;
|
|
savePaths: () => Promise<void>;
|
|
initialize: () => Promise<void>;
|
|
|
|
_markDirty: () => void;
|
|
}
|
|
|
|
function arraysEqual(a: readonly string[], b: readonly string[]): boolean {
|
|
return a.length === b.length && a.every((v, i) => v === b[i]);
|
|
}
|
|
|
|
export const useAppStore = create<AppState>((set, get) => ({
|
|
sysPaths: [],
|
|
userPaths: [],
|
|
undoRedo: new UndoRedoManager(appConfig.undo.maxHistory),
|
|
_savedSys: [],
|
|
_savedUser: [],
|
|
|
|
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 }),
|
|
|
|
addPath: (path, target) => {
|
|
const state = get();
|
|
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
|
|
const newList = [...list, path];
|
|
state.undoRedo.push({
|
|
type: OperationType.ADD, target, index: newList.length - 1, count: 1,
|
|
oldPaths: [], newPaths: [path],
|
|
});
|
|
if (target === TargetType.SYSTEM) set({ sysPaths: newList });
|
|
else set({ userPaths: newList });
|
|
get()._markDirty();
|
|
},
|
|
|
|
editPath: (index, newPath, target) => {
|
|
const state = get();
|
|
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
|
|
const oldPath = list[index];
|
|
if (oldPath === undefined) return;
|
|
state.undoRedo.push({
|
|
type: OperationType.EDIT, target, index, count: 1,
|
|
oldPaths: [oldPath], newPaths: [newPath],
|
|
});
|
|
const newList = [...list];
|
|
newList[index] = newPath;
|
|
if (target === TargetType.SYSTEM) set({ sysPaths: newList });
|
|
else set({ userPaths: newList });
|
|
get()._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: [] });
|
|
get()._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] });
|
|
get()._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] });
|
|
get()._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: [] });
|
|
get()._markDirty();
|
|
}
|
|
|
|
return removed;
|
|
},
|
|
|
|
importPaths: (target, importPaths) => {
|
|
if (importPaths.length === 0) return;
|
|
const state = get();
|
|
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
|
|
|
|
state.undoRedo.push({
|
|
type: OperationType.IMPORT, target, index: 0, count: importPaths.length,
|
|
oldPaths: [...list], newPaths: [...importPaths],
|
|
});
|
|
|
|
if (target === TargetType.SYSTEM) set({ sysPaths: [...importPaths], selectedIndices: [] });
|
|
else set({ userPaths: [...importPaths], selectedIndices: [] });
|
|
get()._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: [] });
|
|
get()._markDirty();
|
|
},
|
|
|
|
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: !(arraysEqual(result[0], _savedSys) && arraysEqual(result[1], _savedUser)),
|
|
});
|
|
}
|
|
},
|
|
|
|
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: !(arraysEqual(result[0], _savedSys) && arraysEqual(result[1], _savedUser)),
|
|
});
|
|
}
|
|
},
|
|
|
|
_markDirty: () => {
|
|
const { _savedSys, _savedUser, sysPaths, userPaths } = get();
|
|
set({ isModified: !(arraysEqual(sysPaths, _savedSys) && arraysEqual(userPaths, _savedUser)) });
|
|
},
|
|
|
|
loadPaths: async () => {
|
|
try {
|
|
set({ isLoading: true });
|
|
const [sysArr, userArr] = await Promise.all([
|
|
invoke<string[]>('load_system_paths'),
|
|
invoke<string[]>('load_user_paths'),
|
|
]);
|
|
set({
|
|
sysPaths: sysArr, userPaths: userArr,
|
|
_savedSys: [...sysArr], _savedUser: [...userArr],
|
|
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 () => {
|
|
const state = get();
|
|
if (state.isSaving) return;
|
|
set({ isSaving: true, statusMessage: i18n.t('status.saving') });
|
|
|
|
const { sysPaths, userPaths } = state;
|
|
const sysJoined = sysPaths.join(';');
|
|
const userJoined = userPaths.join(';');
|
|
|
|
const { maxSystemLength, maxUserLength, maxCombinedLength } = appConfig.path;
|
|
if (sysJoined.length > maxSystemLength || userJoined.length > maxUserLength || (sysJoined + userJoined).length > maxCombinedLength) {
|
|
if (!window.confirm('PATH 长度超过建议值,是否继续保存?')) { set({ isSaving: false }); return; }
|
|
}
|
|
|
|
// 备份当前注册表(保存前备份旧值,失败仅警告不中断)
|
|
await invoke('backup_registry', { customDir: null })
|
|
.catch(() => set({ statusMessage: i18n.t('status.warning_backup') }));
|
|
|
|
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) {
|
|
invoke('broadcast_env_change').catch(() => {});
|
|
const savedSys = [...sysPaths], savedUser = [...userPaths];
|
|
set({ isModified: false, isSaving: false, statusMessage: i18n.t('status.saved'), _savedSys: savedSys, _savedUser: savedUser });
|
|
} else {
|
|
const reason = (!sysOk && sysResult.status === 'rejected') ? String(sysResult.reason) :
|
|
(!userOk && userResult.status === 'rejected') ? String(userResult.reason) : '';
|
|
const msg = sysOk ? '用户 PATH 保存失败' : userOk ? '系统 PATH 保存失败' : `保存失败: ${reason}`;
|
|
set({ isSaving: false, statusMessage: 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();
|
|
},
|
|
}));
|