feat: v0.4.0 - provider icon system, settings modal & polish
- Add provider icon system with extracted icon library (cc-switch based) - Persist icon and icon_color in SQLite and config.toml - Add Settings modal (theme / language / update check / silent download) - Add useTheme and useUpdateCheck hooks - Add provider icon picker (IconPicker) with search and grid - Fix icon frame 2-layer color difference in ProviderEdit - Dashboard heatmap and daily usage improvements - Sessions page polish and adaptive layout - Bump version to 0.4.0
This commit is contained in:
1 parent
14d48500bf
commit
ebf2dd28d5
134 files changed
+2898
-326
No files matched your search
@@ -0,0 +1,62 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export type Theme = "light" | "dark" | "auto";
|
||||
export type ResolvedTheme = "light" | "dark";
|
||||
|
||||
const STORAGE_KEY = "kimi-switch-theme";
|
||||
|
||||
function getSystemTheme(): ResolvedTheme {
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||
}
|
||||
|
||||
function getInitialTheme(): Theme {
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY) as Theme | null;
|
||||
if (stored === "light" || stored === "dark" || stored === "auto") return stored;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return "auto";
|
||||
}
|
||||
|
||||
function applyTheme(theme: Theme): ResolvedTheme {
|
||||
const resolved: ResolvedTheme = theme === "auto" ? getSystemTheme() : theme;
|
||||
const root = document.documentElement;
|
||||
root.classList.toggle("dark", resolved === "dark");
|
||||
root.classList.toggle("light", resolved === "light");
|
||||
return resolved;
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
const [theme, setThemeState] = useState<Theme>(getInitialTheme);
|
||||
const [resolvedTheme, setResolvedTheme] = useState<ResolvedTheme>(() => applyTheme(getInitialTheme()));
|
||||
|
||||
// Apply theme on change
|
||||
useEffect(() => {
|
||||
const resolved = applyTheme(theme);
|
||||
setResolvedTheme(resolved);
|
||||
}, [theme]);
|
||||
|
||||
// Listen to system theme changes when in "auto" mode
|
||||
useEffect(() => {
|
||||
if (theme !== "auto") return;
|
||||
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
const handler = () => {
|
||||
const resolved = applyTheme("auto");
|
||||
setResolvedTheme(resolved);
|
||||
};
|
||||
media.addEventListener("change", handler);
|
||||
return () => media.removeEventListener("change", handler);
|
||||
}, [theme]);
|
||||
|
||||
const setTheme = (next: Theme) => {
|
||||
setThemeState(next);
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, next);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
return { theme, resolvedTheme, setTheme };
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
export interface UpdateInfo {
|
||||
current: string;
|
||||
latest: string;
|
||||
updateAvailable: boolean;
|
||||
releaseUrl: string;
|
||||
downloadUrl: string | null;
|
||||
}
|
||||
|
||||
const THROTTLE_KEY = "kimi-switch-last-update-check";
|
||||
const THROTTLE_MS = 24 * 60 * 60 * 1000; // 24 hours
|
||||
|
||||
export function useUpdateCheck() {
|
||||
const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null);
|
||||
const [checking, setChecking] = useState(false);
|
||||
const [lastChecked, setLastChecked] = useState<number | null>(() => {
|
||||
try {
|
||||
const v = localStorage.getItem(THROTTLE_KEY);
|
||||
return v ? Number(v) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
const doCheck = useCallback(async (force: boolean) => {
|
||||
if (!force) {
|
||||
if (lastChecked && Date.now() - lastChecked < THROTTLE_MS) return;
|
||||
}
|
||||
setChecking(true);
|
||||
try {
|
||||
const info = await invoke<UpdateInfo>("check_for_update");
|
||||
setUpdateInfo(info);
|
||||
const now = Date.now();
|
||||
setLastChecked(now);
|
||||
try {
|
||||
localStorage.setItem(THROTTLE_KEY, String(now));
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
} catch {
|
||||
// silently fail — user can retry from Settings
|
||||
} finally {
|
||||
setChecking(false);
|
||||
}
|
||||
}, [lastChecked]);
|
||||
|
||||
// On mount: throttled auto-check
|
||||
useEffect(() => {
|
||||
doCheck(false);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const checkNow = useCallback(() => doCheck(true), [doCheck]);
|
||||
|
||||
return { updateInfo, checking, checkNow, lastChecked };
|
||||
}
|
||||
Reference in new issue
Block a user