Files
ctms/frontend/src/runtime/desktopUiPreferences.ts
T
Cheng Zhou b491b6a146
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled
Storage Persistence Guard / storage-persistence-audit (push) Has been cancelled
Client Quality Gates / Shared client and Web (pull_request) Has been cancelled
Client Quality Gates / macOS Desktop (pull_request) Has been cancelled
Storage Persistence Guard / storage-persistence-audit (pull_request) Has been cancelled
发布候选:整合桌面端界面与发布稳定化里程碑
2026-07-01 10:53:24 +08:00

114 lines
4.5 KiB
TypeScript

export const DESKTOP_RECENT_ROUTES_KEY = "ctms_desktop_recent_routes";
export const DESKTOP_FAVORITE_ROUTES_KEY = "ctms_desktop_favorite_routes";
export const DESKTOP_THEME_KEY = "ctms_desktop_theme";
export const DESKTOP_THEME_CHANGED_EVENT = "ctms:desktop-theme-changed";
export type DesktopThemePreference = "light" | "dark";
export interface DesktopRoutePreference {
path: string;
title: string;
group?: string;
updatedAt: string;
}
const MAX_RECENT_ROUTES = 8;
const MAX_FAVORITE_ROUTES = 12;
const DEFAULT_DESKTOP_THEME: DesktopThemePreference = "light";
const DESKTOP_THEME_ATTRIBUTE = "data-ctms-theme";
const isStorageAvailable = () => typeof window !== "undefined" && typeof window.localStorage !== "undefined";
const sanitizeRoutePreference = (value: Partial<DesktopRoutePreference> | null | undefined): DesktopRoutePreference | null => {
const path = typeof value?.path === "string" ? value.path.trim() : "";
const title = typeof value?.title === "string" ? value.title.trim() : "";
if (!path.startsWith("/") || !title) return null;
return {
path,
title: title.slice(0, 80),
group: typeof value?.group === "string" ? value.group.slice(0, 40) : undefined,
updatedAt: typeof value?.updatedAt === "string" ? value.updatedAt : new Date().toISOString(),
};
};
const readRoutePreferences = (key: string): DesktopRoutePreference[] => {
if (!isStorageAvailable()) return [];
try {
const raw = window.localStorage.getItem(key);
if (!raw) return [];
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed)) return [];
return parsed
.map((item) => sanitizeRoutePreference(item))
.filter(Boolean) as DesktopRoutePreference[];
} catch {
return [];
}
};
const writeRoutePreferences = (key: string, routes: DesktopRoutePreference[]) => {
if (!isStorageAvailable()) return;
window.localStorage.setItem(key, JSON.stringify(routes));
};
const sanitizeDesktopThemePreference = (value: unknown): DesktopThemePreference =>
value === "dark" ? "dark" : DEFAULT_DESKTOP_THEME;
export const readDesktopThemePreference = (): DesktopThemePreference => {
if (!isStorageAvailable()) return DEFAULT_DESKTOP_THEME;
return sanitizeDesktopThemePreference(window.localStorage.getItem(DESKTOP_THEME_KEY));
};
export const applyDesktopThemePreference = (theme: DesktopThemePreference = readDesktopThemePreference()): DesktopThemePreference => {
const nextTheme = sanitizeDesktopThemePreference(theme);
if (typeof document !== "undefined") {
document.documentElement.setAttribute(DESKTOP_THEME_ATTRIBUTE, nextTheme);
document.documentElement.style.colorScheme = nextTheme;
}
return nextTheme;
};
export const setDesktopThemePreference = (theme: DesktopThemePreference): DesktopThemePreference => {
const nextTheme = sanitizeDesktopThemePreference(theme);
if (isStorageAvailable()) {
window.localStorage.setItem(DESKTOP_THEME_KEY, nextTheme);
}
applyDesktopThemePreference(nextTheme);
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent(DESKTOP_THEME_CHANGED_EVENT, { detail: nextTheme }));
}
return nextTheme;
};
export const readDesktopRecentRoutes = (): DesktopRoutePreference[] => readRoutePreferences(DESKTOP_RECENT_ROUTES_KEY);
export const readDesktopFavoriteRoutes = (): DesktopRoutePreference[] => readRoutePreferences(DESKTOP_FAVORITE_ROUTES_KEY);
export const recordDesktopRecentRoute = (route: Pick<DesktopRoutePreference, "path" | "title" | "group">): DesktopRoutePreference[] => {
const item = sanitizeRoutePreference({ ...route, updatedAt: new Date().toISOString() });
if (!item) return readDesktopRecentRoutes();
const next = [
item,
...readDesktopRecentRoutes().filter((existing) => existing.path !== item.path),
].slice(0, MAX_RECENT_ROUTES);
writeRoutePreferences(DESKTOP_RECENT_ROUTES_KEY, next);
return next;
};
export const isDesktopFavoriteRoute = (path: string): boolean =>
readDesktopFavoriteRoutes().some((item) => item.path === path);
export const toggleDesktopFavoriteRoute = (
route: Pick<DesktopRoutePreference, "path" | "title" | "group">,
): DesktopRoutePreference[] => {
const item = sanitizeRoutePreference({ ...route, updatedAt: new Date().toISOString() });
if (!item) return readDesktopFavoriteRoutes();
const current = readDesktopFavoriteRoutes();
const exists = current.some((existing) => existing.path === item.path);
const next = exists
? current.filter((existing) => existing.path !== item.path)
: [item, ...current].slice(0, MAX_FAVORITE_ROUTES);
writeRoutePreferences(DESKTOP_FAVORITE_ROUTES_KEY, next);
return next;
};