Skip to content

Commit 7e64582

Browse files
committed
feat: add skin palette customization and update settings UI
1 parent 2f95572 commit 7e64582

8 files changed

Lines changed: 122 additions & 3 deletions

File tree

src/App.tsx

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import MainApp from "./MainApp";
77
import WidgetWindow from "./widgets/WidgetWindow";
88
import { useSettingsStore } from "./stores/settingsStore";
99
import { AnnouncerProvider } from "@/components/Announcer";
10+
import { getSkinPalette } from "@/utils/skinPalettes";
1011

1112
/**
1213
* Root component. Decides whether to render the main dashboard or a widget,
@@ -25,6 +26,8 @@ export default function App() {
2526
const widgetBackgroundFit = useSettingsStore((s) => s.widgetBackgroundFit);
2627
const appBackgroundOverlay = useSettingsStore((s) => s.appBackgroundOverlay);
2728
const widgetBackgroundOverlay = useSettingsStore((s) => s.widgetBackgroundOverlay);
29+
const skinPalette = useSettingsStore((s) => s.skinPalette);
30+
const [activePalette, setActivePalette] = useState(skinPalette);
2831
const reducedMotion = useSettingsStore((s) => s.reducedMotion);
2932
const compactWidgets = useSettingsStore((s) => s.compactWidgets);
3033
const [skin, setSkin] = useState({
@@ -36,6 +39,10 @@ export default function App() {
3639
widgetOverlay: widgetBackgroundOverlay,
3740
});
3841

42+
useEffect(() => {
43+
setActivePalette(skinPalette);
44+
}, [skinPalette]);
45+
3946
useEffect(() => {
4047
try {
4148
const label = getCurrentWebviewWindow().label;
@@ -93,7 +100,7 @@ export default function App() {
93100

94101
useEffect(() => {
95102
let unlisten: (() => void) | undefined;
96-
listen<Partial<typeof skin> & { appBackgroundImage?: string; widgetBackgroundImage?: string }>("timelens-skin-changed", (event) => {
103+
listen<Partial<typeof skin> & { appBackgroundImage?: string; widgetBackgroundImage?: string; skinPalette?: typeof skinPalette }>("timelens-skin-changed", (event) => {
97104
setSkin((previous) => ({
98105
app: event.payload?.app ?? event.payload?.appBackgroundImage ?? previous.app,
99106
widget: event.payload?.widget ?? event.payload?.widgetBackgroundImage ?? previous.widget,
@@ -102,6 +109,9 @@ export default function App() {
102109
appOverlay: event.payload?.appOverlay ?? previous.appOverlay,
103110
widgetOverlay: event.payload?.widgetOverlay ?? previous.widgetOverlay,
104111
}));
112+
if (event.payload?.skinPalette) {
113+
setActivePalette(event.payload.skinPalette);
114+
}
105115
}).then((cleanup) => { unlisten = cleanup; }).catch(() => {});
106116
return () => unlisten?.();
107117
}, []);
@@ -116,7 +126,19 @@ export default function App() {
116126
root.style.setProperty("--timelens-widget-background-fit", skin.widgetFit === "stretch" ? "100% 100%" : skin.widgetFit);
117127
root.style.setProperty("--timelens-app-overlay", skin.app ? String(skin.appOverlay / 100) : "0");
118128
root.style.setProperty("--timelens-widget-overlay", skin.widget ? String(skin.widgetOverlay / 100) : "0");
119-
}, [skin]);
129+
const palette = getSkinPalette(activePalette);
130+
const paletteVars: Record<string, string> = {
131+
"--app-bg": palette.appBg, "--surface": palette.surface, "--surface-light": palette.surfaceLight,
132+
"--surface-card": palette.surfaceCard, "--surface-hover": palette.surfaceHover, "--surface-border": palette.surfaceBorder,
133+
"--text-primary": palette.textPrimary, "--text-secondary": palette.textSecondary, "--text-muted": palette.textMuted,
134+
"--accent-blue": palette.accentBlue, "--accent-purple": palette.accentPurple, "--accent-teal": palette.accentTeal,
135+
"--accent-green": palette.accentGreen, "--accent-red": palette.accentRed, "--accent-orange": palette.accentOrange,
136+
};
137+
Object.entries(paletteVars).forEach(([name, value]) => {
138+
if (activePalette === "default") root.style.removeProperty(name);
139+
else root.style.setProperty(name, value);
140+
});
141+
}, [activePalette, skin]);
120142

121143
if (windowLabel !== "main") {
122144
return <WidgetWindow widgetId={windowLabel} />;

src/i18n/locales/en/settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
"system": "System"
3838
},
3939
"skin": {
40+
"palette": "Color palette",
41+
"palettes": { "default": "Default", "ocean": "Ocean", "forest": "Forest", "sunset": "Sunset", "monochrome": "Monochrome" },
4042
"appBackground": "TimeLens background",
4143
"widgetBackground": "Widget background",
4244
"choose": "Choose image",

src/i18n/locales/zh-CN/settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
"system": "跟随系统"
3838
},
3939
"skin": {
40+
"palette": "调色板",
41+
"palettes": { "default": "默认", "ocean": "海洋", "forest": "森林", "sunset": "夕阳", "monochrome": "黑白" },
4042
"appBackground": "TimeLens 背景图片",
4143
"widgetBackground": "小组件背景图片",
4244
"choose": "选择图片",

src/i18n/locales/zh-TW/settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
"system": "跟隨系統"
3838
},
3939
"skin": {
40+
"palette": "調色盤",
41+
"palettes": { "default": "預設", "ocean": "海洋", "forest": "森林", "sunset": "夕陽", "monochrome": "黑白" },
4042
"appBackground": "TimeLens 背景圖片",
4143
"widgetBackground": "小元件背景圖片",
4244
"choose": "選擇圖片",

src/pages/Settings/index.tsx

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { convertFileSrc } from "@tauri-apps/api/core";
55
import { open as openFileDialog } from "@tauri-apps/plugin-dialog";
66
import { useSettingsStore } from "@/stores/settingsStore";
77
import LanguageSwitcher from "@/components/LanguageSwitcher";
8-
import { Moon, Sun, Activity, Database, Info, Rocket, Keyboard, PanelsTopLeft, ArrowLeft, Search, Lock, Copy, RotateCw, User, Shield, Droplet, Sparkles, Image } from "lucide-react";
8+
import { Moon, Sun, Activity, Database, Info, Rocket, Keyboard, PanelsTopLeft, ArrowLeft, Search, Lock, Copy, RotateCw, User, Shield, Droplet, Sparkles, Image, Palette } from "lucide-react";
99
import clsx from "clsx";
1010
import * as api from "@/services/tauriApi";
1111
import { APP_VERSION } from "../../version";
@@ -39,6 +39,7 @@ import BackupSection from "./BackupSection";
3939
import LlmSettings from "./LlmSettings";
4040

4141
import SettingsCard from "./SettingsCard";
42+
import { SKIN_PALETTES, type SkinPaletteId } from "@/utils/skinPalettes";
4243

4344
function SettingsRow({ label, children }: { label: string; children: React.ReactNode }) {
4445
return (
@@ -185,12 +186,14 @@ export default function Settings() {
185186
widgetBackgroundFit,
186187
appBackgroundOverlay,
187188
widgetBackgroundOverlay,
189+
skinPalette,
188190
setAppBackgroundImage,
189191
setWidgetBackgroundImage,
190192
setAppBackgroundFit,
191193
setWidgetBackgroundFit,
192194
setAppBackgroundOverlay,
193195
setWidgetBackgroundOverlay,
196+
setSkinPalette,
194197
monitoringActive,
195198
setMonitoringActive,
196199
samplingIntervalMs,
@@ -261,6 +264,11 @@ export default function Settings() {
261264
});
262265
};
263266

267+
const updatePalette = (palette: SkinPaletteId) => {
268+
setSkinPalette(palette);
269+
void emit("timelens-skin-changed", { skinPalette: palette });
270+
};
271+
264272
const chooseSkinImage = async (kind: "app" | "widget") => {
265273
try {
266274
const selected = await openFileDialog({
@@ -931,6 +939,28 @@ export default function Settings() {
931939
))}
932940
</div>
933941
</SettingsRow>
942+
<SettingsRow label={t("skin.palette")}>
943+
<div className="flex flex-wrap justify-end gap-2">
944+
{SKIN_PALETTES.map((palette) => (
945+
<button
946+
key={palette.id}
947+
onClick={() => updatePalette(palette.id)}
948+
title={t(`skin.palettes.${palette.id}`)}
949+
aria-label={t(`skin.palettes.${palette.id}`)}
950+
className={clsx(
951+
"flex items-center gap-1.5 rounded-lg border px-2 py-1.5 text-xs transition-colors",
952+
skinPalette === palette.id ? "border-accent-blue bg-accent-blue/15 text-accent-blue" : "border-surface-border text-text-muted hover:text-text-secondary"
953+
)}
954+
>
955+
<Palette size={12} />
956+
<span>{t(`skin.palettes.${palette.id}`)}</span>
957+
<span className="flex gap-0.5" aria-hidden="true">
958+
{[palette.accentBlue, palette.accentTeal, palette.accentOrange].map((color) => <span key={color} className="h-2.5 w-2.5 rounded-full" style={{ backgroundColor: color }} />)}
959+
</span>
960+
</button>
961+
))}
962+
</div>
963+
</SettingsRow>
934964
<SettingsRow label={t("accessibility.reducedMotion")}>
935965
<input type="checkbox" className="ui-checkbox" checked={reducedMotion} onChange={(event) => setReducedMotion(event.target.checked)} aria-label={t("accessibility.reducedMotion")} />
936966
</SettingsRow>

src/stores/settingsStore.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { create } from "zustand";
22
import { createJSONStorage, persist } from "zustand/middleware";
33
import i18n from "@/i18n/config";
44
import * as api from "@/services/tauriApi";
5+
import type { SkinPaletteId } from "@/utils/skinPalettes";
56

67
const safeSettingsStorage = createJSONStorage(() => ({
78
getItem: (name: string) => {
@@ -28,6 +29,7 @@ interface SettingsState {
2829
widgetBackgroundFit: "cover" | "contain" | "stretch";
2930
appBackgroundOverlay: number;
3031
widgetBackgroundOverlay: number;
32+
skinPalette: SkinPaletteId;
3133
updateMode: "off" | "notify" | "auto";
3234
monitoringActive: boolean;
3335
samplingIntervalMs: number;
@@ -52,6 +54,7 @@ interface SettingsState {
5254
setWidgetBackgroundFit: (fit: "cover" | "contain" | "stretch") => void;
5355
setAppBackgroundOverlay: (value: number) => void;
5456
setWidgetBackgroundOverlay: (value: number) => void;
57+
setSkinPalette: (palette: SkinPaletteId) => void;
5558
setUpdateMode: (mode: "off" | "notify" | "auto") => void;
5659
setMonitoringActive: (active: boolean) => Promise<void>;
5760
setSamplingInterval: (ms: number) => void;
@@ -81,6 +84,7 @@ export const useSettingsStore = create<SettingsState>()(
8184
widgetBackgroundFit: "cover",
8285
appBackgroundOverlay: 62,
8386
widgetBackgroundOverlay: 62,
87+
skinPalette: "default",
8488
updateMode: "notify",
8589
monitoringActive: true,
8690
samplingIntervalMs: 1000,
@@ -118,6 +122,8 @@ export const useSettingsStore = create<SettingsState>()(
118122

119123
setWidgetBackgroundOverlay: (value) => set({ widgetBackgroundOverlay: Math.max(0, Math.min(90, value)) }),
120124

125+
setSkinPalette: (skinPalette) => set({ skinPalette }),
126+
121127
setUpdateMode: (updateMode) => set({ updateMode }),
122128

123129
setMonitoringActive: (monitoringActive) => {

src/styles/globals.css

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,19 @@ html.compact-widgets .widget-root {
216216
color: var(--text-muted) !important;
217217
}
218218

219+
.text-accent-blue { color: var(--accent-blue) !important; }
220+
.text-accent-purple { color: var(--accent-purple) !important; }
221+
.text-accent-teal { color: var(--accent-teal) !important; }
222+
.text-accent-green { color: var(--accent-green) !important; }
223+
.text-accent-red { color: var(--accent-red) !important; }
224+
.text-accent-orange { color: var(--accent-orange) !important; }
225+
.bg-accent-blue { background-color: var(--accent-blue) !important; }
226+
.bg-accent-green { background-color: var(--accent-green) !important; }
227+
.bg-accent-red { background-color: var(--accent-red) !important; }
228+
.border-accent-blue { border-color: var(--accent-blue) !important; }
229+
.border-accent-green { border-color: var(--accent-green) !important; }
230+
.border-accent-red { border-color: var(--accent-red) !important; }
231+
219232
.hover\:bg-surface-hover:hover {
220233
background-color: var(--surface-hover) !important;
221234
}

src/utils/skinPalettes.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
export type SkinPaletteId = "default" | "ocean" | "forest" | "sunset" | "monochrome";
2+
3+
export interface SkinPalette {
4+
id: SkinPaletteId;
5+
appBg: string;
6+
surface: string;
7+
surfaceLight: string;
8+
surfaceCard: string;
9+
surfaceHover: string;
10+
surfaceBorder: string;
11+
textPrimary: string;
12+
textSecondary: string;
13+
textMuted: string;
14+
accentBlue: string;
15+
accentPurple: string;
16+
accentTeal: string;
17+
accentGreen: string;
18+
accentRed: string;
19+
accentOrange: string;
20+
}
21+
22+
export const SKIN_PALETTES: SkinPalette[] = [
23+
{
24+
id: "default", appBg: "#141824", surface: "#1e2433", surfaceLight: "#262d3d", surfaceCard: "#283043", surfaceHover: "#323b52", surfaceBorder: "#3a445c", textPrimary: "#e8edf7", textSecondary: "#aeb8ca", textMuted: "#7e8aa1", accentBlue: "#6c8ebf", accentPurple: "#8b6cbf", accentTeal: "#4ea5a0", accentGreen: "#4caf7d", accentRed: "#e05555", accentOrange: "#e09050",
25+
},
26+
{
27+
id: "ocean", appBg: "#0c1d2a", surface: "#123047", surfaceLight: "#17415b", surfaceCard: "#1b4b66", surfaceHover: "#23617d", surfaceBorder: "#327b96", textPrimary: "#e5f7ff", textSecondary: "#a8d6e5", textMuted: "#78aabd", accentBlue: "#43b7d8", accentPurple: "#8b9de9", accentTeal: "#35c3b0", accentGreen: "#65d69a", accentRed: "#ef7777", accentOrange: "#e9a15c",
28+
},
29+
{
30+
id: "forest", appBg: "#132019", surface: "#1c3026", surfaceLight: "#294235", surfaceCard: "#31513f", surfaceHover: "#3c624b", surfaceBorder: "#527963", textPrimary: "#edf8ed", textSecondary: "#b9d3bd", textMuted: "#89a78f", accentBlue: "#70b8c5", accentPurple: "#b29ad2", accentTeal: "#55c39c", accentGreen: "#8bd05f", accentRed: "#e87575", accentOrange: "#e4a65f",
31+
},
32+
{
33+
id: "sunset", appBg: "#261a20", surface: "#39232b", surfaceLight: "#4b2b35", surfaceCard: "#5a3540", surfaceHover: "#6e414c", surfaceBorder: "#8a5960", textPrimary: "#fff1e8", textSecondary: "#e2bdaf", textMuted: "#bb8f86", accentBlue: "#79b6d5", accentPurple: "#c494d6", accentTeal: "#65c3b0", accentGreen: "#9ace76", accentRed: "#f07b72", accentOrange: "#f2a45c",
34+
},
35+
{
36+
id: "monochrome", appBg: "#151515", surface: "#232323", surfaceLight: "#303030", surfaceCard: "#3b3b3b", surfaceHover: "#4a4a4a", surfaceBorder: "#626262", textPrimary: "#f4f4f4", textSecondary: "#c7c7c7", textMuted: "#999999", accentBlue: "#d1d1d1", accentPurple: "#b8b8b8", accentTeal: "#ababab", accentGreen: "#c8c8c8", accentRed: "#ef8c8c", accentOrange: "#e0b27f",
37+
},
38+
];
39+
40+
export function getSkinPalette(id: SkinPaletteId): SkinPalette {
41+
return SKIN_PALETTES.find((palette) => palette.id === id) ?? SKIN_PALETTES[0];
42+
}

0 commit comments

Comments
 (0)