From c7d76af790a1ce0e62b09f1d3c34c5b3abb0dae4 Mon Sep 17 00:00:00 2001
From: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com>
Date: Tue, 23 Jun 2026 12:13:26 +0200
Subject: [PATCH] feat(themes): add follow-system mode to the theme scheduler
(#1163)
* feat(themes): add follow-system mode to the theme scheduler
The theme scheduler can now switch the day/night theme pair by the OS
light/dark preference instead of a clock schedule. A segmented control
picks the trigger; in system mode the time inputs are hidden and the two
theme pickers read as Light/Dark.
The OS theme is read via the native Tauri window theme API (theme() +
onThemeChanged) rather than the Web prefers-color-scheme media query,
which is unreliable through WebKitGTK on Linux. Live updates land
instantly where the platform forwards them; on Linux setups that don't,
a hint notes the change applies after an app restart.
* docs(changelog): add follow-system theme mode entry (#1163)
CHANGELOG Added entry, contributor credit and What's New highlight for the
theme scheduler's new system-theme mode.
---
CHANGELOG.md | 6 ++
WHATS_NEW.md | 4 ++
src/app/bootstrap.ts | 25 ++++++---
src/components/settings/ThemesTab.tsx | 77 +++++++++++++++++++-------
src/config/settingsCredits.ts | 1 +
src/hooks/useSystemPrefersDark.ts | 77 ++++++++++++++++++++++++++
src/hooks/useThemeScheduler.ts | 32 +++++++----
src/locales/de/settings.ts | 8 ++-
src/locales/en/settings.ts | 8 ++-
src/locales/es/settings.ts | 8 ++-
src/locales/fr/settings.ts | 8 ++-
src/locales/hu/settings.ts | 8 ++-
src/locales/ja/settings.ts | 8 ++-
src/locales/nb/settings.ts | 8 ++-
src/locales/nl/settings.ts | 8 ++-
src/locales/ro/settings.ts | 8 ++-
src/locales/ru/settings.ts | 8 ++-
src/locales/zh/settings.ts | 8 ++-
src/store/themeStore.test.ts | 79 +++++++++++++++++++++++++++
src/store/themeStore.ts | 18 +++++-
20 files changed, 356 insertions(+), 51 deletions(-)
create mode 100644 src/hooks/useSystemPrefersDark.ts
create mode 100644 src/store/themeStore.test.ts
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ea575e95..6a549fc6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -89,6 +89,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* In an Orbit session the host's track-transition settings — crossfade, gapless or AutoDJ, including the crossfade length and smooth-skip — now apply to everyone, so guests blend between tracks the same way the host does instead of each person using their own. Your own settings are restored when you leave.
* While you are a guest in a session, the transition controls in Settings → Audio and the queue toolbar are shown as host-controlled.
+### Theme scheduler — follow the system theme
+
+**By [@Psychotoxical](https://github.com/Psychotoxical), PR [#1163](https://github.com/Psychotoxical/psysonic/pull/1163)**, suggested by [@mokazemi](https://github.com/mokazemi)
+
+* The theme scheduler can now switch your day/night theme pair based on your operating system's light/dark setting, in addition to the existing time-of-day schedule. Pick the trigger with a new Time of Day / System Theme switch; in system mode the two pickers read as Light and Dark theme. On Linux setups where the OS does not signal the change live, a hint notes it applies after restarting the app.
+
## Changed
diff --git a/WHATS_NEW.md b/WHATS_NEW.md
index e80a8744..20e2ee0e 100644
--- a/WHATS_NEW.md
+++ b/WHATS_NEW.md
@@ -42,6 +42,10 @@ Within each section, order by **user impact** (most noticeable first) — not PR
- Turn on **Remember EQ per device** under **Settings → Audio** and Psysonic keeps a separate equalizer setup for each output — speakers, headphones, a USB DAC — and switches to the right one automatically when you change devices. Off by default.
+### Themes — follow your system's light and dark mode
+
+- The theme scheduler can now match your **system's light/dark setting** instead of a fixed clock: pick a light theme and a dark one, and Psysonic switches along with your OS. Choose **Time of Day** or **System Theme** under **Settings → Themes** — the existing time-based schedule is still there.
+
## Fixed
### Playback and audio
diff --git a/src/app/bootstrap.ts b/src/app/bootstrap.ts
index ef439772..3db7cded 100644
--- a/src/app/bootstrap.ts
+++ b/src/app/bootstrap.ts
@@ -69,14 +69,23 @@ export function applyThemeAtStartup(): void {
const s = parsed.state;
if (!s) return;
syncInjectedThemes(readInstalledThemes());
- const effective = getScheduledTheme({
- enableThemeScheduler: !!s.enableThemeScheduler,
- theme: String(s.theme ?? 'mocha'),
- themeDay: String(s.themeDay ?? 'latte'),
- themeNight: String(s.themeNight ?? 'mocha'),
- timeDayStart: String(s.timeDayStart ?? '07:00'),
- timeNightStart: String(s.timeNightStart ?? '19:00'),
- });
+ // First-frame best effort for the "follow system" mode: the Web media query
+ // is sync here (the native Tauri theme resolves only after mount, when the
+ // App effect re-applies the effective theme). Unreliable on Linux WebKitGTK,
+ // but only affects this initial paint before the effect corrects it.
+ const systemPrefersDark = window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false;
+ const effective = getScheduledTheme(
+ {
+ enableThemeScheduler: !!s.enableThemeScheduler,
+ schedulerMode: s.schedulerMode === 'system' ? 'system' : 'time',
+ theme: String(s.theme ?? 'mocha'),
+ themeDay: String(s.themeDay ?? 'latte'),
+ themeNight: String(s.themeNight ?? 'mocha'),
+ timeDayStart: String(s.timeDayStart ?? '07:00'),
+ timeNightStart: String(s.timeNightStart ?? '19:00'),
+ },
+ systemPrefersDark,
+ );
if (effective) document.documentElement.setAttribute('data-theme', effective);
} catch {
// Non-fatal — App's effects apply the theme after mount.
diff --git a/src/components/settings/ThemesTab.tsx b/src/components/settings/ThemesTab.tsx
index 83a7deeb..f3303074 100644
--- a/src/components/settings/ThemesTab.tsx
+++ b/src/components/settings/ThemesTab.tsx
@@ -92,31 +92,66 @@ export function ThemesTab() {
const dayM = theme.timeDayStart.split(':')[1];
const nightH = theme.timeNightStart.split(':')[0];
const nightM = theme.timeNightStart.split(':')[1];
+ const isSystem = theme.schedulerMode === 'system';
return (
-