-
{t('settings.themeSchedulerNightStart')}
-
-
theme.setTimeNightStart(`${v}:${nightM}`)} options={hourOptions} />
- :
- theme.setTimeNightStart(`${nightH}:${v}`)} options={minuteOptions} />
+ {isSystem && (
+
+ {t('settings.themeSchedulerSystemRestartHint')}
+ )}
+
+
+
+ {isSystem ? t('settings.themeSchedulerLightTheme') : t('settings.themeSchedulerDayTheme')}
+
+
+
+ {!isSystem && (
+
+
{t('settings.themeSchedulerDayStart')}
+
+ theme.setTimeDayStart(`${v}:${dayM}`)} options={hourOptions} />
+ :
+ theme.setTimeDayStart(`${dayH}:${v}`)} options={minuteOptions} />
+
+
+ )}
+
+
+ {isSystem ? t('settings.themeSchedulerDarkTheme') : t('settings.themeSchedulerNightTheme')}
+
+
+
+ {!isSystem && (
+
+
{t('settings.themeSchedulerNightStart')}
+
+ theme.setTimeNightStart(`${v}:${nightM}`)} options={hourOptions} />
+ :
+ theme.setTimeNightStart(`${nightH}:${v}`)} options={minuteOptions} />
+
+
+ )}
);
diff --git a/src/config/settingsCredits.ts b/src/config/settingsCredits.ts
index 7d09e382..c90abb8b 100644
--- a/src/config/settingsCredits.ts
+++ b/src/config/settingsCredits.ts
@@ -383,6 +383,7 @@ const CONTRIBUTOR_ENTRIES = [
'Artist artwork from fanart.tv — opt-in fullscreen background + artist-header banner, off by default (PR #1137)',
'Per-device equalizer profiles — opt-in, remembers and restores the EQ for each audio output device (PR #1146)',
'Hungarian (Magyar) translation (PR #1149)',
+ 'Theme scheduler — follow the OS light/dark setting as an alternative to the time-based day/night schedule (PR #1163)',
],
},
{
diff --git a/src/hooks/useSystemPrefersDark.ts b/src/hooks/useSystemPrefersDark.ts
new file mode 100644
index 00000000..ec361d09
--- /dev/null
+++ b/src/hooks/useSystemPrefersDark.ts
@@ -0,0 +1,77 @@
+import { useSyncExternalStore } from 'react';
+import { getCurrentWindow } from '@tauri-apps/api/window';
+
+/**
+ * Tracks whether the OS prefers a dark color scheme, for the "follow system"
+ * theme scheduler mode.
+ *
+ * The native Tauri window theme API is the primary source: on Linux the Web
+ * `prefers-color-scheme` media query is unreliable through WebKitGTK (Tauri
+ * does not reliably forward the system preference — it tends to report `light`
+ * regardless), whereas `theme()` / `onThemeChanged` read the OS theme natively
+ * (via the XDG portal on Linux). `matchMedia` is only a fallback for the
+ * frontend-only dev shell where the Tauri IPC is absent.
+ *
+ * `theme()` resolves the OS theme correctly at startup; `onThemeChanged` gives
+ * an instant live update where the platform forwards it (macOS / Windows). On
+ * some Linux setups (tao does not forward the portal change live) the event
+ * never fires, so a light/dark flip is only reflected after an app restart —
+ * the UI surfaces that note in the system scheduler mode.
+ */
+
+let isDark = false;
+let started = false;
+const listeners = new Set<() => void>();
+
+function emit(): void {
+ for (const l of listeners) l();
+}
+
+function setDark(next: boolean): void {
+ if (next === isDark) return;
+ isDark = next;
+ emit();
+}
+
+/** Begin watching once the first subscriber mounts (idempotent). */
+function start(): void {
+ if (started) return;
+ started = true;
+
+ void (async () => {
+ try {
+ const win = getCurrentWindow();
+ setDark((await win.theme()) === 'dark');
+ // Instant updates where the platform forwards them (macOS / Windows).
+ await win.onThemeChanged(({ payload }) => setDark(payload === 'dark'));
+ } catch {
+ // No Tauri IPC (frontend-only dev) — best-effort Web fallback.
+ try {
+ const mql = window.matchMedia('(prefers-color-scheme: dark)');
+ setDark(mql.matches);
+ mql.addEventListener('change', (e) => setDark(e.matches));
+ } catch {
+ /* default: light */
+ }
+ }
+ })();
+}
+
+function subscribe(cb: () => void): () => void {
+ listeners.add(cb);
+ start();
+ return () => listeners.delete(cb);
+}
+
+function getSnapshot(): boolean {
+ return isDark;
+}
+
+function getServerSnapshot(): boolean {
+ return false;
+}
+
+/** `true` when the OS theme is dark. Updates live on OS theme changes. */
+export function useSystemPrefersDark(): boolean {
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
+}
diff --git a/src/hooks/useThemeScheduler.ts b/src/hooks/useThemeScheduler.ts
index 0287bd4f..568d6398 100644
--- a/src/hooks/useThemeScheduler.ts
+++ b/src/hooks/useThemeScheduler.ts
@@ -1,34 +1,46 @@
import { useEffect, useState } from 'react';
import { useThemeStore, getScheduledTheme } from '../store/themeStore';
+import { useSystemPrefersDark } from './useSystemPrefersDark';
/**
* Effective theme id for `data-theme` — scheduler-aware when enabled.
* Derived synchronously from the store so `App.tsx` never paints a stale id
* (the previous useState+mirror lag could leave `data-theme` on Mocha for a
* commit cycle in production React).
+ *
+ * Two trigger modes: a clock schedule (re-evaluated each minute) or the OS
+ * theme (re-evaluated whenever the system flips light/dark via
+ * `useSystemPrefersDark`).
*/
export function useThemeScheduler(): string {
const enableScheduler = useThemeStore(s => s.enableThemeScheduler);
+ const schedulerMode = useThemeStore(s => s.schedulerMode);
const theme = useThemeStore(s => s.theme);
const themeDay = useThemeStore(s => s.themeDay);
const themeNight = useThemeStore(s => s.themeNight);
const timeDayStart = useThemeStore(s => s.timeDayStart);
const timeNightStart = useThemeStore(s => s.timeNightStart);
+ const systemPrefersDark = useSystemPrefersDark();
const [tick, setTick] = useState(0);
useEffect(() => {
- if (!enableScheduler) return;
+ // Only the clock mode needs polling; the system mode is event-driven.
+ if (!enableScheduler || schedulerMode !== 'time') return;
const id = setInterval(() => setTick(n => n + 1), 60_000);
return () => clearInterval(id);
- }, [enableScheduler, themeDay, themeNight, timeDayStart, timeNightStart]);
+ }, [enableScheduler, schedulerMode, themeDay, themeNight, timeDayStart, timeNightStart]);
void tick;
- return getScheduledTheme({
- enableThemeScheduler: enableScheduler,
- theme,
- themeDay,
- themeNight,
- timeDayStart,
- timeNightStart,
- });
+ return getScheduledTheme(
+ {
+ enableThemeScheduler: enableScheduler,
+ schedulerMode,
+ theme,
+ themeDay,
+ themeNight,
+ timeDayStart,
+ timeNightStart,
+ },
+ systemPrefersDark,
+ );
}
diff --git a/src/locales/de/settings.ts b/src/locales/de/settings.ts
index 6e50e427..e81e3d63 100644
--- a/src/locales/de/settings.ts
+++ b/src/locales/de/settings.ts
@@ -650,12 +650,18 @@ export const settings = {
seekbarRetrotape: 'Retro-Band',
themeSchedulerTitle: 'Theme-Zeitplan',
themeSchedulerEnable: 'Theme-Zeitplan aktivieren',
- themeSchedulerEnableSub: 'Wechselt automatisch zwischen zwei Themes basierend auf der Uhrzeit',
+ themeSchedulerEnableSub: 'Wechselt automatisch zwischen zwei Themes – nach Uhrzeit oder System-Theme',
themeSchedulerDayTheme: 'Tages-Theme',
themeSchedulerDayStart: 'Tag beginnt um',
themeSchedulerNightTheme: 'Nacht-Theme',
themeSchedulerNightStart: 'Nacht beginnt um',
themeSchedulerActiveHint: 'Theme-Zeitplan ist aktiv - Themes werden automatisch gewechselt.',
+ themeSchedulerModeLabel: 'Wechseln nach',
+ themeSchedulerModeTime: 'Uhrzeit',
+ themeSchedulerModeSystem: 'System-Theme',
+ themeSchedulerLightTheme: 'Helles Theme',
+ themeSchedulerDarkTheme: 'Dunkles Theme',
+ themeSchedulerSystemRestartHint: 'Ein Wechsel des System-Designs (hell/dunkel) wird erst nach einem Neustart der App übernommen.',
visualOptionsTitle: 'Visuelle Optionen',
coverArtBackground: 'Cover-Hintergrund',
coverArtBackgroundSub: 'Zeigt verschwommenes Cover als Hintergrund in Album/Playlist-Kopfzeilen',
diff --git a/src/locales/en/settings.ts b/src/locales/en/settings.ts
index fc03575d..256b6e31 100644
--- a/src/locales/en/settings.ts
+++ b/src/locales/en/settings.ts
@@ -717,12 +717,18 @@ export const settings = {
seekbarRetrotape: 'Retro Tape',
themeSchedulerTitle: 'Auto-Switch Theme',
themeSchedulerEnable: 'Enable Theme Scheduler',
- themeSchedulerEnableSub: 'Automatically switch between two themes based on the time of day',
+ themeSchedulerEnableSub: 'Automatically switch between two themes based on the time of day or your system theme',
themeSchedulerDayTheme: 'Day Theme',
themeSchedulerDayStart: 'Day Starts At',
themeSchedulerNightTheme: 'Night Theme',
themeSchedulerNightStart: 'Night Starts At',
themeSchedulerActiveHint: 'Theme Scheduler is active - theme changes are managed automatically.',
+ themeSchedulerModeLabel: 'Switch Based On',
+ themeSchedulerModeTime: 'Time of Day',
+ themeSchedulerModeSystem: 'System Theme',
+ themeSchedulerLightTheme: 'Light Theme',
+ themeSchedulerDarkTheme: 'Dark Theme',
+ themeSchedulerSystemRestartHint: 'Changing your system light/dark setting takes effect after restarting the app.',
visualOptionsTitle: 'Visual Options',
coverArtBackground: 'Cover Art Background',
coverArtBackgroundSub: 'Show blurred cover art as background in album/playlist headers',
diff --git a/src/locales/es/settings.ts b/src/locales/es/settings.ts
index 7b2589c1..a5ac29e1 100644
--- a/src/locales/es/settings.ts
+++ b/src/locales/es/settings.ts
@@ -649,12 +649,18 @@ export const settings = {
seekbarRetrotape: 'Cinta Retro',
themeSchedulerTitle: 'Cambio Automático de Tema',
themeSchedulerEnable: 'Habilitar Programador de Temas',
- themeSchedulerEnableSub: 'Cambia automáticamente entre dos temas según la hora del día',
+ themeSchedulerEnableSub: 'Cambia automáticamente entre dos temas según la hora del día o el tema del sistema',
themeSchedulerDayTheme: 'Tema de Día',
themeSchedulerDayStart: 'Comienza de Día A las',
themeSchedulerNightTheme: 'Tema de Noche',
themeSchedulerNightStart: 'Comienza de Noche A las',
themeSchedulerActiveHint: 'El Programador de Temas está activo — los cambios de tema se manejan automáticamente.',
+ themeSchedulerModeLabel: 'Cambiar según',
+ themeSchedulerModeTime: 'Hora del día',
+ themeSchedulerModeSystem: 'Tema del sistema',
+ themeSchedulerLightTheme: 'Tema claro',
+ themeSchedulerDarkTheme: 'Tema oscuro',
+ themeSchedulerSystemRestartHint: 'Cambiar el ajuste claro/oscuro del sistema se aplica tras reiniciar la aplicación.',
visualOptionsTitle: 'Opciones Visuales',
coverArtBackground: 'Fondo de Portada',
coverArtBackgroundSub: 'Mostrar portada desenfocada como fondo en encabezados de álbumes y listas de reproducción',
diff --git a/src/locales/fr/settings.ts b/src/locales/fr/settings.ts
index 6a8d38e8..958482d2 100644
--- a/src/locales/fr/settings.ts
+++ b/src/locales/fr/settings.ts
@@ -637,12 +637,18 @@ export const settings = {
seekbarRetrotape: 'Bande rétro',
themeSchedulerTitle: 'Planificateur de thème',
themeSchedulerEnable: 'Activer le planificateur de thème',
- themeSchedulerEnableSub: 'Bascule automatiquement entre deux thèmes selon l\'heure',
+ themeSchedulerEnableSub: 'Bascule automatiquement entre deux thèmes selon l\'heure ou le thème du système',
themeSchedulerDayTheme: 'Thème de jour',
themeSchedulerDayStart: 'Début du jour',
themeSchedulerNightTheme: 'Thème de nuit',
themeSchedulerNightStart: 'Début de la nuit',
themeSchedulerActiveHint: 'Le planificateur de thème est actif - les thèmes changent automatiquement.',
+ themeSchedulerModeLabel: 'Basculer selon',
+ themeSchedulerModeTime: 'Heure',
+ themeSchedulerModeSystem: 'Thème du système',
+ themeSchedulerLightTheme: 'Thème clair',
+ themeSchedulerDarkTheme: 'Thème sombre',
+ themeSchedulerSystemRestartHint: 'Le changement du thème clair/sombre du système prend effet après le redémarrage de l\'application.',
visualOptionsTitle: 'Options Visuelles',
coverArtBackground: "Fond d'Art de Poche",
coverArtBackgroundSub: "Afficher la pochette floutée comme fond dans les en-têtes d'albums et de playlists",
diff --git a/src/locales/hu/settings.ts b/src/locales/hu/settings.ts
index eb438536..ec15269f 100644
--- a/src/locales/hu/settings.ts
+++ b/src/locales/hu/settings.ts
@@ -717,12 +717,18 @@ export const settings = {
seekbarRetrotape: 'Retro szalag',
themeSchedulerTitle: 'Automatikus témaváltás',
themeSchedulerEnable: 'Témaütemező engedélyezése',
- themeSchedulerEnableSub: 'Automatikus váltás két téma között a napszak alapján',
+ themeSchedulerEnableSub: 'Automatikus váltás két téma között a napszak vagy a rendszertéma alapján',
themeSchedulerDayTheme: 'Nappali téma',
themeSchedulerDayStart: 'A nappal kezdődik',
themeSchedulerNightTheme: 'Éjszakai téma',
themeSchedulerNightStart: 'Az éjszaka kezdődik',
themeSchedulerActiveHint: 'A témaütemező aktív – a témaváltások automatikusan kezeltek.',
+ themeSchedulerModeLabel: 'Váltás alapja',
+ themeSchedulerModeTime: 'Napszak',
+ themeSchedulerModeSystem: 'Rendszertéma',
+ themeSchedulerLightTheme: 'Világos téma',
+ themeSchedulerDarkTheme: 'Sötét téma',
+ themeSchedulerSystemRestartHint: 'A rendszer világos/sötét beállításának módosítása az alkalmazás újraindítása után lép életbe.',
visualOptionsTitle: 'Vizuális beállítások',
coverArtBackground: 'Borító háttér',
coverArtBackgroundSub: 'Elmosott borító megjelenítése háttérként az album/lejátszási lista fejlécekben',
diff --git a/src/locales/ja/settings.ts b/src/locales/ja/settings.ts
index ef679926..9a8a3f7f 100644
--- a/src/locales/ja/settings.ts
+++ b/src/locales/ja/settings.ts
@@ -711,12 +711,18 @@ export const settings = {
seekbarRetrotape: 'Retro Tape',
themeSchedulerTitle: 'テーマ自動切り替え',
themeSchedulerEnable: 'テーマスケジューラーを有効化',
- themeSchedulerEnableSub: '時刻に基づいて 2 つのテーマを自動で切り替えます',
+ themeSchedulerEnableSub: '時刻またはシステムテーマに基づいて 2 つのテーマを自動で切り替えます',
themeSchedulerDayTheme: '昼テーマ',
themeSchedulerDayStart: '昼の開始時刻',
themeSchedulerNightTheme: '夜テーマ',
themeSchedulerNightStart: '夜の開始時刻',
themeSchedulerActiveHint: 'テーマスケジューラーが有効です。テーマ変更は自動で管理されます。',
+ themeSchedulerModeLabel: '切り替え基準',
+ themeSchedulerModeTime: '時刻',
+ themeSchedulerModeSystem: 'システムテーマ',
+ themeSchedulerLightTheme: 'ライトテーマ',
+ themeSchedulerDarkTheme: 'ダークテーマ',
+ themeSchedulerSystemRestartHint: 'システムのライト/ダーク設定の変更は、アプリの再起動後に反映されます。',
visualOptionsTitle: '表示オプション',
coverArtBackground: 'カバーアート背景',
coverArtBackgroundSub: 'アルバム/プレイリストヘッダーの背景にぼかしたカバーアートを表示',
diff --git a/src/locales/nb/settings.ts b/src/locales/nb/settings.ts
index 17b0577a..14665fc1 100644
--- a/src/locales/nb/settings.ts
+++ b/src/locales/nb/settings.ts
@@ -636,12 +636,18 @@ export const settings = {
seekbarRetrotape: 'Retrotape',
themeSchedulerTitle: 'Tidsplanlagt tema',
themeSchedulerEnable: 'Aktiver temaplanlegger',
- themeSchedulerEnableSub: 'Bytter automatisk mellom to temaer basert på tidspunkt',
+ themeSchedulerEnableSub: 'Bytter automatisk mellom to temaer basert på tidspunkt eller systemtemaet',
themeSchedulerDayTheme: 'Dagtema',
themeSchedulerDayStart: 'Dag starter kl.',
themeSchedulerNightTheme: 'Natttema',
themeSchedulerNightStart: 'Natt starter kl.',
themeSchedulerActiveHint: 'Temaplanlegger er aktiv - temaer byttes automatisk.',
+ themeSchedulerModeLabel: 'Bytt basert på',
+ themeSchedulerModeTime: 'Tidspunkt',
+ themeSchedulerModeSystem: 'Systemtema',
+ themeSchedulerLightTheme: 'Lyst tema',
+ themeSchedulerDarkTheme: 'Mørkt tema',
+ themeSchedulerSystemRestartHint: 'Endring av systemets lys/mørk-innstilling trer i kraft etter omstart av appen.',
visualOptionsTitle: 'Visuelle Alternativer',
coverArtBackground: 'Cover-bakgrunn',
coverArtBackgroundSub: 'Vis uskarpt cover som bakgrunn i album/playlist-overskrifter',
diff --git a/src/locales/nl/settings.ts b/src/locales/nl/settings.ts
index 0206b4d1..6a628f36 100644
--- a/src/locales/nl/settings.ts
+++ b/src/locales/nl/settings.ts
@@ -637,12 +637,18 @@ export const settings = {
seekbarRetrotape: 'Retrotape',
themeSchedulerTitle: 'Thema-planner',
themeSchedulerEnable: 'Thema-planner inschakelen',
- themeSchedulerEnableSub: 'Schakelt automatisch tussen twee thema\'s op basis van de tijd',
+ themeSchedulerEnableSub: 'Schakelt automatisch tussen twee thema\'s op basis van de tijd of je systeemthema',
themeSchedulerDayTheme: 'Dagthema',
themeSchedulerDayStart: 'Dag begint om',
themeSchedulerNightTheme: 'Nachtthema',
themeSchedulerNightStart: 'Nacht begint om',
themeSchedulerActiveHint: "Thema-planner is actief - thema's worden automatisch gewisseld.",
+ themeSchedulerModeLabel: 'Wisselen op basis van',
+ themeSchedulerModeTime: 'Tijd van de dag',
+ themeSchedulerModeSystem: 'Systeemthema',
+ themeSchedulerLightTheme: 'Licht thema',
+ themeSchedulerDarkTheme: 'Donker thema',
+ themeSchedulerSystemRestartHint: 'Het wijzigen van de licht/donker-instelling van je systeem wordt pas na het herstarten van de app toegepast.',
visualOptionsTitle: 'Visuele Opties',
coverArtBackground: 'Cover Achtergrond',
coverArtBackgroundSub: 'Toon vervaagde cover als achtergrond in album/playlist headers',
diff --git a/src/locales/ro/settings.ts b/src/locales/ro/settings.ts
index ef6197bc..4a0dac29 100644
--- a/src/locales/ro/settings.ts
+++ b/src/locales/ro/settings.ts
@@ -652,12 +652,18 @@ export const settings = {
seekbarRetrotape: 'Bandă Retro',
themeSchedulerTitle: 'Schimbă Tema automat',
themeSchedulerEnable: 'Pornește temele programate',
- themeSchedulerEnableSub: 'Schimbă automat între două teme pe baza orei din zi',
+ themeSchedulerEnableSub: 'Schimbă automat între două teme în funcție de ora din zi sau tema sistemului',
themeSchedulerDayTheme: 'Temă de zi',
themeSchedulerDayStart: 'Ziua începe la',
themeSchedulerNightTheme: 'Temă de noapte',
themeSchedulerNightStart: 'Noaptea începe la',
themeSchedulerActiveHint: 'Temele programate sunt active - aceste schimbări de teme sunt gestionate automat.',
+ themeSchedulerModeLabel: 'Comută în funcție de',
+ themeSchedulerModeTime: 'Ora din zi',
+ themeSchedulerModeSystem: 'Tema sistemului',
+ themeSchedulerLightTheme: 'Temă luminoasă',
+ themeSchedulerDarkTheme: 'Temă întunecată',
+ themeSchedulerSystemRestartHint: 'Schimbarea setării luminos/întunecat a sistemului are efect după repornirea aplicației.',
visualOptionsTitle: 'Opțiuni vizuale',
coverArtBackground: 'Arta de fundal a copertei',
coverArtBackgroundSub: 'Arată arta de copertă blurată ca fundal în antetele albumelor/playlisturilor',
diff --git a/src/locales/ru/settings.ts b/src/locales/ru/settings.ts
index 9679dff5..ed36e8a3 100644
--- a/src/locales/ru/settings.ts
+++ b/src/locales/ru/settings.ts
@@ -737,12 +737,18 @@ export const settings = {
seekbarRetrotape: 'Ретро-лента',
themeSchedulerTitle: 'Расписание тем',
themeSchedulerEnable: 'Включить расписание тем',
- themeSchedulerEnableSub: 'Автоматически переключается между двумя темами в зависимости от времени суток',
+ themeSchedulerEnableSub: 'Автоматически переключается между двумя темами в зависимости от времени суток или системной темы',
themeSchedulerDayTheme: 'Дневная тема',
themeSchedulerDayStart: 'День начинается в',
themeSchedulerNightTheme: 'Ночная тема',
themeSchedulerNightStart: 'Ночь начинается в',
themeSchedulerActiveHint: 'Расписание тем активно - темы переключаются автоматически.',
+ themeSchedulerModeLabel: 'Переключать по',
+ themeSchedulerModeTime: 'Времени суток',
+ themeSchedulerModeSystem: 'Системной теме',
+ themeSchedulerLightTheme: 'Светлая тема',
+ themeSchedulerDarkTheme: 'Тёмная тема',
+ themeSchedulerSystemRestartHint: 'Изменение светлой/тёмной настройки системы вступит в силу после перезапуска приложения.',
visualOptionsTitle: 'Визуальные Настройки',
coverArtBackground: 'Фон Обложки',
coverArtBackgroundSub: 'Показывать размытую обложку как фон в заголовках альбомов и плейлистов',
diff --git a/src/locales/zh/settings.ts b/src/locales/zh/settings.ts
index eb65dce6..c190eaf4 100644
--- a/src/locales/zh/settings.ts
+++ b/src/locales/zh/settings.ts
@@ -636,12 +636,18 @@ export const settings = {
seekbarRetrotape: '复古磁带',
themeSchedulerTitle: '主题定时切换',
themeSchedulerEnable: '启用主题定时器',
- themeSchedulerEnableSub: '根据一天中的时间自动在两个主题之间切换',
+ themeSchedulerEnableSub: '根据一天中的时间或系统主题自动在两个主题之间切换',
themeSchedulerDayTheme: '白天主题',
themeSchedulerDayStart: '白天开始时间',
themeSchedulerNightTheme: '夜晚主题',
themeSchedulerNightStart: '夜晚开始时间',
themeSchedulerActiveHint: '主题定时器已启用 - 主题将自动切换。',
+ themeSchedulerModeLabel: '切换依据',
+ themeSchedulerModeTime: '时间',
+ themeSchedulerModeSystem: '系统主题',
+ themeSchedulerLightTheme: '浅色主题',
+ themeSchedulerDarkTheme: '深色主题',
+ themeSchedulerSystemRestartHint: '更改系统的浅色/深色设置需重启应用后生效。',
visualOptionsTitle: '视觉选项',
coverArtBackground: '封面背景',
coverArtBackgroundSub: '在专辑/播放列表标题中显示模糊的封面作为背景',
diff --git a/src/store/themeStore.test.ts b/src/store/themeStore.test.ts
new file mode 100644
index 00000000..8cf4e376
--- /dev/null
+++ b/src/store/themeStore.test.ts
@@ -0,0 +1,79 @@
+import { afterEach, describe, expect, it, vi } from 'vitest';
+
+import { getScheduledTheme } from './themeStore';
+
+type SchedState = Parameters
[0];
+
+function state(over: Partial = {}): SchedState {
+ return {
+ enableThemeScheduler: true,
+ schedulerMode: 'time',
+ theme: 'mocha',
+ themeDay: 'latte',
+ themeNight: 'mocha',
+ timeDayStart: '07:00',
+ timeNightStart: '19:00',
+ ...over,
+ };
+}
+
+/** Pin the wall clock to a fixed `HH:MM` for the time-mode cases. */
+function atTime(hh: number, mm = 0): void {
+ vi.useFakeTimers();
+ const d = new Date(2026, 5, 23, hh, mm, 0);
+ vi.setSystemTime(d);
+}
+
+afterEach(() => {
+ vi.useRealTimers();
+});
+
+describe('getScheduledTheme', () => {
+ it('returns the plain theme when the scheduler is off', () => {
+ expect(getScheduledTheme(state({ enableThemeScheduler: false }))).toBe('mocha');
+ });
+
+ describe('time mode', () => {
+ it('picks the day theme during the day window', () => {
+ atTime(12);
+ expect(getScheduledTheme(state())).toBe('latte');
+ });
+
+ it('picks the night theme during the night window', () => {
+ atTime(22);
+ expect(getScheduledTheme(state())).toBe('mocha');
+ });
+
+ it('treats the day-start edge as day and the night-start edge as night', () => {
+ atTime(7, 0);
+ expect(getScheduledTheme(state())).toBe('latte');
+ atTime(19, 0);
+ expect(getScheduledTheme(state())).toBe('mocha');
+ });
+
+ it('handles a window that wraps past midnight (day 22:00 → 06:00)', () => {
+ const s = state({ timeDayStart: '22:00', timeNightStart: '06:00' });
+ atTime(23);
+ expect(getScheduledTheme(s)).toBe('latte'); // inside the wrapped day window
+ atTime(12);
+ expect(getScheduledTheme(s)).toBe('mocha'); // outside → night
+ });
+ });
+
+ describe('system mode', () => {
+ it('follows the OS theme, ignoring the clock', () => {
+ atTime(3); // would be night in time mode — must not matter here
+ const s = state({ schedulerMode: 'system' });
+ expect(getScheduledTheme(s, true)).toBe('mocha'); // dark → night theme
+ expect(getScheduledTheme(s, false)).toBe('latte'); // light → day theme
+ });
+
+ it('defaults to the day theme when the system preference is unknown', () => {
+ expect(getScheduledTheme(state({ schedulerMode: 'system' }))).toBe('latte');
+ });
+
+ it('still returns the plain theme when the scheduler is off', () => {
+ expect(getScheduledTheme(state({ enableThemeScheduler: false, schedulerMode: 'system' }), true)).toBe('mocha');
+ });
+ });
+});
diff --git a/src/store/themeStore.ts b/src/store/themeStore.ts
index 59f797cb..99375b11 100644
--- a/src/store/themeStore.ts
+++ b/src/store/themeStore.ts
@@ -18,11 +18,17 @@ export type BuiltinTheme =
*/
export type Theme = BuiltinTheme | (string & {});
+/** Trigger for the day/night theme switch. */
+export type ThemeSchedulerMode = 'time' | 'system';
+
interface ThemeState {
theme: Theme;
setTheme: (theme: Theme) => void;
enableThemeScheduler: boolean;
setEnableThemeScheduler: (v: boolean) => void;
+ /** What drives the day/night switch: a clock schedule or the OS theme. */
+ schedulerMode: ThemeSchedulerMode;
+ setSchedulerMode: (v: ThemeSchedulerMode) => void;
themeDay: string;
setThemeDay: (v: string) => void;
themeNight: string;
@@ -51,8 +57,16 @@ interface ThemeState {
setExternalArtworkByok: (v: string) => void;
}
-export function getScheduledTheme(state: Pick): string {
+export function getScheduledTheme(
+ state: Pick<
+ ThemeState,
+ 'enableThemeScheduler' | 'schedulerMode' | 'theme' | 'themeDay' | 'themeNight' | 'timeDayStart' | 'timeNightStart'
+ >,
+ systemPrefersDark = false,
+): string {
if (!state.enableThemeScheduler) return state.theme;
+ // Follow the OS theme: dark → night theme, light → day theme.
+ if (state.schedulerMode === 'system') return systemPrefersDark ? state.themeNight : state.themeDay;
const now = new Date();
const nowMins = now.getHours() * 60 + now.getMinutes();
const [dh, dm] = state.timeDayStart.split(':').map(Number);
@@ -72,6 +86,8 @@ export const useThemeStore = create()(
setTheme: (theme) => set({ theme }),
enableThemeScheduler: false,
setEnableThemeScheduler: (v) => set({ enableThemeScheduler: v }),
+ schedulerMode: 'time',
+ setSchedulerMode: (v) => set({ schedulerMode: v }),
themeDay: 'latte',
setThemeDay: (v) => set({ themeDay: v }),
themeNight: 'mocha',