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:
KimiSwitch Dev committed 2026-07-28 16:01:11 +08:00
1 parent 14d48500bf
commit ebf2dd28d5
134 files changed
+2898 -326

No files matched your search

+62
View File
@@ -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 };
}
+58
View File
@@ -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 };
}