Files
paseo/packages/app/src/hooks/use-settings.ts
T
Mohamed Boudra 2f77674c55 feat: add theme selector with multiple dark themes
Add theme dropdown in settings with Light, Dark, System plus
Zinc, Midnight, Claude, and Ghostty dark variants. Each theme
has its own accent color and surface tints. Desaturate terminal
ANSI colors across all dark themes. Add surfaceSidebarHover
semantic token so hover states go the right direction in both
light and dark modes. Self-heal if a stored theme no longer exists.
2026-04-10 16:21:05 +07:00

122 lines
3.8 KiB
TypeScript

import { useCallback } from "react";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { useQuery, useQueryClient } from "@tanstack/react-query";
export const APP_SETTINGS_KEY = "@paseo:app-settings";
const LEGACY_SETTINGS_KEY = "@paseo:settings";
const APP_SETTINGS_QUERY_KEY = ["app-settings"];
import { THEME_TO_UNISTYLES, type ThemeName } from "@/styles/theme";
export type SendBehavior = "interrupt" | "queue";
const VALID_THEMES = new Set<string>([...Object.keys(THEME_TO_UNISTYLES), "auto"]);
export interface AppSettings {
theme: ThemeName | "auto";
manageBuiltInDaemon: boolean;
sendBehavior: SendBehavior;
}
export const DEFAULT_APP_SETTINGS: AppSettings = {
theme: "auto",
manageBuiltInDaemon: true,
sendBehavior: "interrupt",
};
export interface UseAppSettingsReturn {
settings: AppSettings;
isLoading: boolean;
error: unknown | null;
updateSettings: (updates: Partial<AppSettings>) => Promise<void>;
resetSettings: () => Promise<void>;
}
export function useAppSettings(): UseAppSettingsReturn {
const queryClient = useQueryClient();
const { data, isPending, error } = useQuery({
queryKey: APP_SETTINGS_QUERY_KEY,
queryFn: loadSettingsFromStorage,
staleTime: Infinity,
gcTime: Infinity,
});
const updateSettings = useCallback(
async (updates: Partial<AppSettings>) => {
try {
const prev =
queryClient.getQueryData<AppSettings>(APP_SETTINGS_QUERY_KEY) ?? DEFAULT_APP_SETTINGS;
const next = { ...prev, ...updates };
queryClient.setQueryData<AppSettings>(APP_SETTINGS_QUERY_KEY, next);
await AsyncStorage.setItem(APP_SETTINGS_KEY, JSON.stringify(next));
} catch (err) {
console.error("[AppSettings] Failed to save settings:", err);
throw err;
}
},
[queryClient],
);
const resetSettings = useCallback(async () => {
try {
const next = { ...DEFAULT_APP_SETTINGS };
queryClient.setQueryData<AppSettings>(APP_SETTINGS_QUERY_KEY, next);
await AsyncStorage.setItem(APP_SETTINGS_KEY, JSON.stringify(next));
} catch (err) {
console.error("[AppSettings] Failed to reset settings:", err);
throw err;
}
}, [queryClient]);
return {
settings: data ?? DEFAULT_APP_SETTINGS,
isLoading: isPending,
error: error ?? null,
updateSettings,
resetSettings,
};
}
export async function loadSettingsFromStorage(): Promise<AppSettings> {
try {
const stored = await AsyncStorage.getItem(APP_SETTINGS_KEY);
if (stored) {
const parsed = JSON.parse(stored) as Partial<AppSettings>;
if (parsed.theme && !VALID_THEMES.has(parsed.theme)) {
parsed.theme = DEFAULT_APP_SETTINGS.theme;
}
return { ...DEFAULT_APP_SETTINGS, ...parsed };
}
const legacyStored = await AsyncStorage.getItem(LEGACY_SETTINGS_KEY);
if (legacyStored) {
const legacyParsed = JSON.parse(legacyStored) as Record<string, unknown>;
const next = {
...DEFAULT_APP_SETTINGS,
...pickAppSettingsFromLegacy(legacyParsed),
} satisfies AppSettings;
await AsyncStorage.setItem(APP_SETTINGS_KEY, JSON.stringify(next));
return next;
}
await AsyncStorage.setItem(APP_SETTINGS_KEY, JSON.stringify(DEFAULT_APP_SETTINGS));
return DEFAULT_APP_SETTINGS;
} catch (error) {
console.error("[AppSettings] Failed to load settings:", error);
throw error;
}
}
function pickAppSettingsFromLegacy(legacy: Record<string, unknown>): Partial<AppSettings> {
const result: Partial<AppSettings> = {};
if (legacy.theme === "dark" || legacy.theme === "light" || legacy.theme === "auto") {
result.theme = legacy.theme;
}
if (typeof legacy.manageBuiltInDaemon === "boolean") {
result.manageBuiltInDaemon = legacy.manageBuiltInDaemon;
}
return result;
}
export const useSettings = useAppSettings;