mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-21 14:05:41 +00:00
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.
This commit is contained in:
@@ -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.
|
* 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.
|
* 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
|
## Changed
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
- 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
|
## Fixed
|
||||||
|
|
||||||
### Playback and audio
|
### Playback and audio
|
||||||
|
|||||||
+17
-8
@@ -69,14 +69,23 @@ export function applyThemeAtStartup(): void {
|
|||||||
const s = parsed.state;
|
const s = parsed.state;
|
||||||
if (!s) return;
|
if (!s) return;
|
||||||
syncInjectedThemes(readInstalledThemes());
|
syncInjectedThemes(readInstalledThemes());
|
||||||
const effective = getScheduledTheme({
|
// First-frame best effort for the "follow system" mode: the Web media query
|
||||||
enableThemeScheduler: !!s.enableThemeScheduler,
|
// is sync here (the native Tauri theme resolves only after mount, when the
|
||||||
theme: String(s.theme ?? 'mocha'),
|
// App effect re-applies the effective theme). Unreliable on Linux WebKitGTK,
|
||||||
themeDay: String(s.themeDay ?? 'latte'),
|
// but only affects this initial paint before the effect corrects it.
|
||||||
themeNight: String(s.themeNight ?? 'mocha'),
|
const systemPrefersDark = window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false;
|
||||||
timeDayStart: String(s.timeDayStart ?? '07:00'),
|
const effective = getScheduledTheme(
|
||||||
timeNightStart: String(s.timeNightStart ?? '19:00'),
|
{
|
||||||
});
|
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);
|
if (effective) document.documentElement.setAttribute('data-theme', effective);
|
||||||
} catch {
|
} catch {
|
||||||
// Non-fatal — App's effects apply the theme after mount.
|
// Non-fatal — App's effects apply the theme after mount.
|
||||||
|
|||||||
@@ -92,31 +92,66 @@ export function ThemesTab() {
|
|||||||
const dayM = theme.timeDayStart.split(':')[1];
|
const dayM = theme.timeDayStart.split(':')[1];
|
||||||
const nightH = theme.timeNightStart.split(':')[0];
|
const nightH = theme.timeNightStart.split(':')[0];
|
||||||
const nightM = theme.timeNightStart.split(':')[1];
|
const nightM = theme.timeNightStart.split(':')[1];
|
||||||
|
const isSystem = theme.schedulerMode === 'system';
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))', gap: '1rem', marginTop: '1rem' }}>
|
<div style={{ marginTop: '1rem' }}>
|
||||||
<div className="form-group">
|
<div className="form-group" style={{ marginBottom: '1rem' }}>
|
||||||
<label className="settings-label" style={{ marginBottom: 6 }}>{t('settings.themeSchedulerDayTheme')}</label>
|
<label className="settings-label" style={{ marginBottom: 6 }}>{t('settings.themeSchedulerModeLabel')}</label>
|
||||||
<CustomSelect value={theme.themeDay} onChange={theme.setThemeDay} options={themeOptions} />
|
<div className="settings-segmented">
|
||||||
</div>
|
<button
|
||||||
<div className="form-group">
|
type="button"
|
||||||
<label className="settings-label" style={{ marginBottom: 6 }}>{t('settings.themeSchedulerDayStart')}</label>
|
className={`btn ${!isSystem ? 'btn-primary' : 'btn-ghost'}`}
|
||||||
<div style={{ display: 'flex', gap: '6px', alignItems: 'center' }}>
|
onClick={() => theme.setSchedulerMode('time')}
|
||||||
<CustomSelect value={dayH} onChange={v => theme.setTimeDayStart(`${v}:${dayM}`)} options={hourOptions} />
|
>
|
||||||
<span style={{ color: 'var(--text-muted)', fontWeight: 600 }}>:</span>
|
{t('settings.themeSchedulerModeTime')}
|
||||||
<CustomSelect value={dayM} onChange={v => theme.setTimeDayStart(`${dayH}:${v}`)} options={minuteOptions} />
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`btn ${isSystem ? 'btn-primary' : 'btn-ghost'}`}
|
||||||
|
onClick={() => theme.setSchedulerMode('system')}
|
||||||
|
>
|
||||||
|
{t('settings.themeSchedulerModeSystem')}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
{isSystem && (
|
||||||
<label className="settings-label" style={{ marginBottom: 6 }}>{t('settings.themeSchedulerNightTheme')}</label>
|
<div className="settings-hint settings-hint-info" style={{ marginBottom: '1rem' }}>
|
||||||
<CustomSelect value={theme.themeNight} onChange={theme.setThemeNight} options={themeOptions} />
|
{t('settings.themeSchedulerSystemRestartHint')}
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<label className="settings-label" style={{ marginBottom: 6 }}>{t('settings.themeSchedulerNightStart')}</label>
|
|
||||||
<div style={{ display: 'flex', gap: '6px', alignItems: 'center' }}>
|
|
||||||
<CustomSelect value={nightH} onChange={v => theme.setTimeNightStart(`${v}:${nightM}`)} options={hourOptions} />
|
|
||||||
<span style={{ color: 'var(--text-muted)', fontWeight: 600 }}>:</span>
|
|
||||||
<CustomSelect value={nightM} onChange={v => theme.setTimeNightStart(`${nightH}:${v}`)} options={minuteOptions} />
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))', gap: '1rem' }}>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="settings-label" style={{ marginBottom: 6 }}>
|
||||||
|
{isSystem ? t('settings.themeSchedulerLightTheme') : t('settings.themeSchedulerDayTheme')}
|
||||||
|
</label>
|
||||||
|
<CustomSelect value={theme.themeDay} onChange={theme.setThemeDay} options={themeOptions} />
|
||||||
|
</div>
|
||||||
|
{!isSystem && (
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="settings-label" style={{ marginBottom: 6 }}>{t('settings.themeSchedulerDayStart')}</label>
|
||||||
|
<div style={{ display: 'flex', gap: '6px', alignItems: 'center' }}>
|
||||||
|
<CustomSelect value={dayH} onChange={v => theme.setTimeDayStart(`${v}:${dayM}`)} options={hourOptions} />
|
||||||
|
<span style={{ color: 'var(--text-muted)', fontWeight: 600 }}>:</span>
|
||||||
|
<CustomSelect value={dayM} onChange={v => theme.setTimeDayStart(`${dayH}:${v}`)} options={minuteOptions} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="settings-label" style={{ marginBottom: 6 }}>
|
||||||
|
{isSystem ? t('settings.themeSchedulerDarkTheme') : t('settings.themeSchedulerNightTheme')}
|
||||||
|
</label>
|
||||||
|
<CustomSelect value={theme.themeNight} onChange={theme.setThemeNight} options={themeOptions} />
|
||||||
|
</div>
|
||||||
|
{!isSystem && (
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="settings-label" style={{ marginBottom: 6 }}>{t('settings.themeSchedulerNightStart')}</label>
|
||||||
|
<div style={{ display: 'flex', gap: '6px', alignItems: 'center' }}>
|
||||||
|
<CustomSelect value={nightH} onChange={v => theme.setTimeNightStart(`${v}:${nightM}`)} options={hourOptions} />
|
||||||
|
<span style={{ color: 'var(--text-muted)', fontWeight: 600 }}>:</span>
|
||||||
|
<CustomSelect value={nightM} onChange={v => theme.setTimeNightStart(`${nightH}:${v}`)} options={minuteOptions} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -383,6 +383,7 @@ const CONTRIBUTOR_ENTRIES = [
|
|||||||
'Artist artwork from fanart.tv — opt-in fullscreen background + artist-header banner, off by default (PR #1137)',
|
'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)',
|
'Per-device equalizer profiles — opt-in, remembers and restores the EQ for each audio output device (PR #1146)',
|
||||||
'Hungarian (Magyar) translation (PR #1149)',
|
'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)',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -1,34 +1,46 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useThemeStore, getScheduledTheme } from '../store/themeStore';
|
import { useThemeStore, getScheduledTheme } from '../store/themeStore';
|
||||||
|
import { useSystemPrefersDark } from './useSystemPrefersDark';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Effective theme id for `data-theme` — scheduler-aware when enabled.
|
* Effective theme id for `data-theme` — scheduler-aware when enabled.
|
||||||
* Derived synchronously from the store so `App.tsx` never paints a stale id
|
* 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
|
* (the previous useState+mirror lag could leave `data-theme` on Mocha for a
|
||||||
* commit cycle in production React).
|
* 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 {
|
export function useThemeScheduler(): string {
|
||||||
const enableScheduler = useThemeStore(s => s.enableThemeScheduler);
|
const enableScheduler = useThemeStore(s => s.enableThemeScheduler);
|
||||||
|
const schedulerMode = useThemeStore(s => s.schedulerMode);
|
||||||
const theme = useThemeStore(s => s.theme);
|
const theme = useThemeStore(s => s.theme);
|
||||||
const themeDay = useThemeStore(s => s.themeDay);
|
const themeDay = useThemeStore(s => s.themeDay);
|
||||||
const themeNight = useThemeStore(s => s.themeNight);
|
const themeNight = useThemeStore(s => s.themeNight);
|
||||||
const timeDayStart = useThemeStore(s => s.timeDayStart);
|
const timeDayStart = useThemeStore(s => s.timeDayStart);
|
||||||
const timeNightStart = useThemeStore(s => s.timeNightStart);
|
const timeNightStart = useThemeStore(s => s.timeNightStart);
|
||||||
|
const systemPrefersDark = useSystemPrefersDark();
|
||||||
const [tick, setTick] = useState(0);
|
const [tick, setTick] = useState(0);
|
||||||
|
|
||||||
useEffect(() => {
|
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);
|
const id = setInterval(() => setTick(n => n + 1), 60_000);
|
||||||
return () => clearInterval(id);
|
return () => clearInterval(id);
|
||||||
}, [enableScheduler, themeDay, themeNight, timeDayStart, timeNightStart]);
|
}, [enableScheduler, schedulerMode, themeDay, themeNight, timeDayStart, timeNightStart]);
|
||||||
|
|
||||||
void tick;
|
void tick;
|
||||||
return getScheduledTheme({
|
return getScheduledTheme(
|
||||||
enableThemeScheduler: enableScheduler,
|
{
|
||||||
theme,
|
enableThemeScheduler: enableScheduler,
|
||||||
themeDay,
|
schedulerMode,
|
||||||
themeNight,
|
theme,
|
||||||
timeDayStart,
|
themeDay,
|
||||||
timeNightStart,
|
themeNight,
|
||||||
});
|
timeDayStart,
|
||||||
|
timeNightStart,
|
||||||
|
},
|
||||||
|
systemPrefersDark,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -650,12 +650,18 @@ export const settings = {
|
|||||||
seekbarRetrotape: 'Retro-Band',
|
seekbarRetrotape: 'Retro-Band',
|
||||||
themeSchedulerTitle: 'Theme-Zeitplan',
|
themeSchedulerTitle: 'Theme-Zeitplan',
|
||||||
themeSchedulerEnable: 'Theme-Zeitplan aktivieren',
|
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',
|
themeSchedulerDayTheme: 'Tages-Theme',
|
||||||
themeSchedulerDayStart: 'Tag beginnt um',
|
themeSchedulerDayStart: 'Tag beginnt um',
|
||||||
themeSchedulerNightTheme: 'Nacht-Theme',
|
themeSchedulerNightTheme: 'Nacht-Theme',
|
||||||
themeSchedulerNightStart: 'Nacht beginnt um',
|
themeSchedulerNightStart: 'Nacht beginnt um',
|
||||||
themeSchedulerActiveHint: 'Theme-Zeitplan ist aktiv - Themes werden automatisch gewechselt.',
|
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',
|
visualOptionsTitle: 'Visuelle Optionen',
|
||||||
coverArtBackground: 'Cover-Hintergrund',
|
coverArtBackground: 'Cover-Hintergrund',
|
||||||
coverArtBackgroundSub: 'Zeigt verschwommenes Cover als Hintergrund in Album/Playlist-Kopfzeilen',
|
coverArtBackgroundSub: 'Zeigt verschwommenes Cover als Hintergrund in Album/Playlist-Kopfzeilen',
|
||||||
|
|||||||
@@ -717,12 +717,18 @@ export const settings = {
|
|||||||
seekbarRetrotape: 'Retro Tape',
|
seekbarRetrotape: 'Retro Tape',
|
||||||
themeSchedulerTitle: 'Auto-Switch Theme',
|
themeSchedulerTitle: 'Auto-Switch Theme',
|
||||||
themeSchedulerEnable: 'Enable Theme Scheduler',
|
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',
|
themeSchedulerDayTheme: 'Day Theme',
|
||||||
themeSchedulerDayStart: 'Day Starts At',
|
themeSchedulerDayStart: 'Day Starts At',
|
||||||
themeSchedulerNightTheme: 'Night Theme',
|
themeSchedulerNightTheme: 'Night Theme',
|
||||||
themeSchedulerNightStart: 'Night Starts At',
|
themeSchedulerNightStart: 'Night Starts At',
|
||||||
themeSchedulerActiveHint: 'Theme Scheduler is active - theme changes are managed automatically.',
|
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',
|
visualOptionsTitle: 'Visual Options',
|
||||||
coverArtBackground: 'Cover Art Background',
|
coverArtBackground: 'Cover Art Background',
|
||||||
coverArtBackgroundSub: 'Show blurred cover art as background in album/playlist headers',
|
coverArtBackgroundSub: 'Show blurred cover art as background in album/playlist headers',
|
||||||
|
|||||||
@@ -649,12 +649,18 @@ export const settings = {
|
|||||||
seekbarRetrotape: 'Cinta Retro',
|
seekbarRetrotape: 'Cinta Retro',
|
||||||
themeSchedulerTitle: 'Cambio Automático de Tema',
|
themeSchedulerTitle: 'Cambio Automático de Tema',
|
||||||
themeSchedulerEnable: 'Habilitar Programador de Temas',
|
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',
|
themeSchedulerDayTheme: 'Tema de Día',
|
||||||
themeSchedulerDayStart: 'Comienza de Día A las',
|
themeSchedulerDayStart: 'Comienza de Día A las',
|
||||||
themeSchedulerNightTheme: 'Tema de Noche',
|
themeSchedulerNightTheme: 'Tema de Noche',
|
||||||
themeSchedulerNightStart: 'Comienza de Noche A las',
|
themeSchedulerNightStart: 'Comienza de Noche A las',
|
||||||
themeSchedulerActiveHint: 'El Programador de Temas está activo — los cambios de tema se manejan automáticamente.',
|
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',
|
visualOptionsTitle: 'Opciones Visuales',
|
||||||
coverArtBackground: 'Fondo de Portada',
|
coverArtBackground: 'Fondo de Portada',
|
||||||
coverArtBackgroundSub: 'Mostrar portada desenfocada como fondo en encabezados de álbumes y listas de reproducción',
|
coverArtBackgroundSub: 'Mostrar portada desenfocada como fondo en encabezados de álbumes y listas de reproducción',
|
||||||
|
|||||||
@@ -637,12 +637,18 @@ export const settings = {
|
|||||||
seekbarRetrotape: 'Bande rétro',
|
seekbarRetrotape: 'Bande rétro',
|
||||||
themeSchedulerTitle: 'Planificateur de thème',
|
themeSchedulerTitle: 'Planificateur de thème',
|
||||||
themeSchedulerEnable: 'Activer le 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',
|
themeSchedulerDayTheme: 'Thème de jour',
|
||||||
themeSchedulerDayStart: 'Début du jour',
|
themeSchedulerDayStart: 'Début du jour',
|
||||||
themeSchedulerNightTheme: 'Thème de nuit',
|
themeSchedulerNightTheme: 'Thème de nuit',
|
||||||
themeSchedulerNightStart: 'Début de la nuit',
|
themeSchedulerNightStart: 'Début de la nuit',
|
||||||
themeSchedulerActiveHint: 'Le planificateur de thème est actif - les thèmes changent automatiquement.',
|
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',
|
visualOptionsTitle: 'Options Visuelles',
|
||||||
coverArtBackground: "Fond d'Art de Poche",
|
coverArtBackground: "Fond d'Art de Poche",
|
||||||
coverArtBackgroundSub: "Afficher la pochette floutée comme fond dans les en-têtes d'albums et de playlists",
|
coverArtBackgroundSub: "Afficher la pochette floutée comme fond dans les en-têtes d'albums et de playlists",
|
||||||
|
|||||||
@@ -717,12 +717,18 @@ export const settings = {
|
|||||||
seekbarRetrotape: 'Retro szalag',
|
seekbarRetrotape: 'Retro szalag',
|
||||||
themeSchedulerTitle: 'Automatikus témaváltás',
|
themeSchedulerTitle: 'Automatikus témaváltás',
|
||||||
themeSchedulerEnable: 'Témaütemező engedélyezése',
|
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',
|
themeSchedulerDayTheme: 'Nappali téma',
|
||||||
themeSchedulerDayStart: 'A nappal kezdődik',
|
themeSchedulerDayStart: 'A nappal kezdődik',
|
||||||
themeSchedulerNightTheme: 'Éjszakai téma',
|
themeSchedulerNightTheme: 'Éjszakai téma',
|
||||||
themeSchedulerNightStart: 'Az éjszaka kezdődik',
|
themeSchedulerNightStart: 'Az éjszaka kezdődik',
|
||||||
themeSchedulerActiveHint: 'A témaütemező aktív – a témaváltások automatikusan kezeltek.',
|
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',
|
visualOptionsTitle: 'Vizuális beállítások',
|
||||||
coverArtBackground: 'Borító háttér',
|
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',
|
coverArtBackgroundSub: 'Elmosott borító megjelenítése háttérként az album/lejátszási lista fejlécekben',
|
||||||
|
|||||||
@@ -711,12 +711,18 @@ export const settings = {
|
|||||||
seekbarRetrotape: 'Retro Tape',
|
seekbarRetrotape: 'Retro Tape',
|
||||||
themeSchedulerTitle: 'テーマ自動切り替え',
|
themeSchedulerTitle: 'テーマ自動切り替え',
|
||||||
themeSchedulerEnable: 'テーマスケジューラーを有効化',
|
themeSchedulerEnable: 'テーマスケジューラーを有効化',
|
||||||
themeSchedulerEnableSub: '時刻に基づいて 2 つのテーマを自動で切り替えます',
|
themeSchedulerEnableSub: '時刻またはシステムテーマに基づいて 2 つのテーマを自動で切り替えます',
|
||||||
themeSchedulerDayTheme: '昼テーマ',
|
themeSchedulerDayTheme: '昼テーマ',
|
||||||
themeSchedulerDayStart: '昼の開始時刻',
|
themeSchedulerDayStart: '昼の開始時刻',
|
||||||
themeSchedulerNightTheme: '夜テーマ',
|
themeSchedulerNightTheme: '夜テーマ',
|
||||||
themeSchedulerNightStart: '夜の開始時刻',
|
themeSchedulerNightStart: '夜の開始時刻',
|
||||||
themeSchedulerActiveHint: 'テーマスケジューラーが有効です。テーマ変更は自動で管理されます。',
|
themeSchedulerActiveHint: 'テーマスケジューラーが有効です。テーマ変更は自動で管理されます。',
|
||||||
|
themeSchedulerModeLabel: '切り替え基準',
|
||||||
|
themeSchedulerModeTime: '時刻',
|
||||||
|
themeSchedulerModeSystem: 'システムテーマ',
|
||||||
|
themeSchedulerLightTheme: 'ライトテーマ',
|
||||||
|
themeSchedulerDarkTheme: 'ダークテーマ',
|
||||||
|
themeSchedulerSystemRestartHint: 'システムのライト/ダーク設定の変更は、アプリの再起動後に反映されます。',
|
||||||
visualOptionsTitle: '表示オプション',
|
visualOptionsTitle: '表示オプション',
|
||||||
coverArtBackground: 'カバーアート背景',
|
coverArtBackground: 'カバーアート背景',
|
||||||
coverArtBackgroundSub: 'アルバム/プレイリストヘッダーの背景にぼかしたカバーアートを表示',
|
coverArtBackgroundSub: 'アルバム/プレイリストヘッダーの背景にぼかしたカバーアートを表示',
|
||||||
|
|||||||
@@ -636,12 +636,18 @@ export const settings = {
|
|||||||
seekbarRetrotape: 'Retrotape',
|
seekbarRetrotape: 'Retrotape',
|
||||||
themeSchedulerTitle: 'Tidsplanlagt tema',
|
themeSchedulerTitle: 'Tidsplanlagt tema',
|
||||||
themeSchedulerEnable: 'Aktiver temaplanlegger',
|
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',
|
themeSchedulerDayTheme: 'Dagtema',
|
||||||
themeSchedulerDayStart: 'Dag starter kl.',
|
themeSchedulerDayStart: 'Dag starter kl.',
|
||||||
themeSchedulerNightTheme: 'Natttema',
|
themeSchedulerNightTheme: 'Natttema',
|
||||||
themeSchedulerNightStart: 'Natt starter kl.',
|
themeSchedulerNightStart: 'Natt starter kl.',
|
||||||
themeSchedulerActiveHint: 'Temaplanlegger er aktiv - temaer byttes automatisk.',
|
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',
|
visualOptionsTitle: 'Visuelle Alternativer',
|
||||||
coverArtBackground: 'Cover-bakgrunn',
|
coverArtBackground: 'Cover-bakgrunn',
|
||||||
coverArtBackgroundSub: 'Vis uskarpt cover som bakgrunn i album/playlist-overskrifter',
|
coverArtBackgroundSub: 'Vis uskarpt cover som bakgrunn i album/playlist-overskrifter',
|
||||||
|
|||||||
@@ -637,12 +637,18 @@ export const settings = {
|
|||||||
seekbarRetrotape: 'Retrotape',
|
seekbarRetrotape: 'Retrotape',
|
||||||
themeSchedulerTitle: 'Thema-planner',
|
themeSchedulerTitle: 'Thema-planner',
|
||||||
themeSchedulerEnable: 'Thema-planner inschakelen',
|
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',
|
themeSchedulerDayTheme: 'Dagthema',
|
||||||
themeSchedulerDayStart: 'Dag begint om',
|
themeSchedulerDayStart: 'Dag begint om',
|
||||||
themeSchedulerNightTheme: 'Nachtthema',
|
themeSchedulerNightTheme: 'Nachtthema',
|
||||||
themeSchedulerNightStart: 'Nacht begint om',
|
themeSchedulerNightStart: 'Nacht begint om',
|
||||||
themeSchedulerActiveHint: "Thema-planner is actief - thema's worden automatisch gewisseld.",
|
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',
|
visualOptionsTitle: 'Visuele Opties',
|
||||||
coverArtBackground: 'Cover Achtergrond',
|
coverArtBackground: 'Cover Achtergrond',
|
||||||
coverArtBackgroundSub: 'Toon vervaagde cover als achtergrond in album/playlist headers',
|
coverArtBackgroundSub: 'Toon vervaagde cover als achtergrond in album/playlist headers',
|
||||||
|
|||||||
@@ -652,12 +652,18 @@ export const settings = {
|
|||||||
seekbarRetrotape: 'Bandă Retro',
|
seekbarRetrotape: 'Bandă Retro',
|
||||||
themeSchedulerTitle: 'Schimbă Tema automat',
|
themeSchedulerTitle: 'Schimbă Tema automat',
|
||||||
themeSchedulerEnable: 'Pornește temele programate',
|
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',
|
themeSchedulerDayTheme: 'Temă de zi',
|
||||||
themeSchedulerDayStart: 'Ziua începe la',
|
themeSchedulerDayStart: 'Ziua începe la',
|
||||||
themeSchedulerNightTheme: 'Temă de noapte',
|
themeSchedulerNightTheme: 'Temă de noapte',
|
||||||
themeSchedulerNightStart: 'Noaptea începe la',
|
themeSchedulerNightStart: 'Noaptea începe la',
|
||||||
themeSchedulerActiveHint: 'Temele programate sunt active - aceste schimbări de teme sunt gestionate automat.',
|
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',
|
visualOptionsTitle: 'Opțiuni vizuale',
|
||||||
coverArtBackground: 'Arta de fundal a copertei',
|
coverArtBackground: 'Arta de fundal a copertei',
|
||||||
coverArtBackgroundSub: 'Arată arta de copertă blurată ca fundal în antetele albumelor/playlisturilor',
|
coverArtBackgroundSub: 'Arată arta de copertă blurată ca fundal în antetele albumelor/playlisturilor',
|
||||||
|
|||||||
@@ -737,12 +737,18 @@ export const settings = {
|
|||||||
seekbarRetrotape: 'Ретро-лента',
|
seekbarRetrotape: 'Ретро-лента',
|
||||||
themeSchedulerTitle: 'Расписание тем',
|
themeSchedulerTitle: 'Расписание тем',
|
||||||
themeSchedulerEnable: 'Включить расписание тем',
|
themeSchedulerEnable: 'Включить расписание тем',
|
||||||
themeSchedulerEnableSub: 'Автоматически переключается между двумя темами в зависимости от времени суток',
|
themeSchedulerEnableSub: 'Автоматически переключается между двумя темами в зависимости от времени суток или системной темы',
|
||||||
themeSchedulerDayTheme: 'Дневная тема',
|
themeSchedulerDayTheme: 'Дневная тема',
|
||||||
themeSchedulerDayStart: 'День начинается в',
|
themeSchedulerDayStart: 'День начинается в',
|
||||||
themeSchedulerNightTheme: 'Ночная тема',
|
themeSchedulerNightTheme: 'Ночная тема',
|
||||||
themeSchedulerNightStart: 'Ночь начинается в',
|
themeSchedulerNightStart: 'Ночь начинается в',
|
||||||
themeSchedulerActiveHint: 'Расписание тем активно - темы переключаются автоматически.',
|
themeSchedulerActiveHint: 'Расписание тем активно - темы переключаются автоматически.',
|
||||||
|
themeSchedulerModeLabel: 'Переключать по',
|
||||||
|
themeSchedulerModeTime: 'Времени суток',
|
||||||
|
themeSchedulerModeSystem: 'Системной теме',
|
||||||
|
themeSchedulerLightTheme: 'Светлая тема',
|
||||||
|
themeSchedulerDarkTheme: 'Тёмная тема',
|
||||||
|
themeSchedulerSystemRestartHint: 'Изменение светлой/тёмной настройки системы вступит в силу после перезапуска приложения.',
|
||||||
visualOptionsTitle: 'Визуальные Настройки',
|
visualOptionsTitle: 'Визуальные Настройки',
|
||||||
coverArtBackground: 'Фон Обложки',
|
coverArtBackground: 'Фон Обложки',
|
||||||
coverArtBackgroundSub: 'Показывать размытую обложку как фон в заголовках альбомов и плейлистов',
|
coverArtBackgroundSub: 'Показывать размытую обложку как фон в заголовках альбомов и плейлистов',
|
||||||
|
|||||||
@@ -636,12 +636,18 @@ export const settings = {
|
|||||||
seekbarRetrotape: '复古磁带',
|
seekbarRetrotape: '复古磁带',
|
||||||
themeSchedulerTitle: '主题定时切换',
|
themeSchedulerTitle: '主题定时切换',
|
||||||
themeSchedulerEnable: '启用主题定时器',
|
themeSchedulerEnable: '启用主题定时器',
|
||||||
themeSchedulerEnableSub: '根据一天中的时间自动在两个主题之间切换',
|
themeSchedulerEnableSub: '根据一天中的时间或系统主题自动在两个主题之间切换',
|
||||||
themeSchedulerDayTheme: '白天主题',
|
themeSchedulerDayTheme: '白天主题',
|
||||||
themeSchedulerDayStart: '白天开始时间',
|
themeSchedulerDayStart: '白天开始时间',
|
||||||
themeSchedulerNightTheme: '夜晚主题',
|
themeSchedulerNightTheme: '夜晚主题',
|
||||||
themeSchedulerNightStart: '夜晚开始时间',
|
themeSchedulerNightStart: '夜晚开始时间',
|
||||||
themeSchedulerActiveHint: '主题定时器已启用 - 主题将自动切换。',
|
themeSchedulerActiveHint: '主题定时器已启用 - 主题将自动切换。',
|
||||||
|
themeSchedulerModeLabel: '切换依据',
|
||||||
|
themeSchedulerModeTime: '时间',
|
||||||
|
themeSchedulerModeSystem: '系统主题',
|
||||||
|
themeSchedulerLightTheme: '浅色主题',
|
||||||
|
themeSchedulerDarkTheme: '深色主题',
|
||||||
|
themeSchedulerSystemRestartHint: '更改系统的浅色/深色设置需重启应用后生效。',
|
||||||
visualOptionsTitle: '视觉选项',
|
visualOptionsTitle: '视觉选项',
|
||||||
coverArtBackground: '封面背景',
|
coverArtBackground: '封面背景',
|
||||||
coverArtBackgroundSub: '在专辑/播放列表标题中显示模糊的封面作为背景',
|
coverArtBackgroundSub: '在专辑/播放列表标题中显示模糊的封面作为背景',
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { getScheduledTheme } from './themeStore';
|
||||||
|
|
||||||
|
type SchedState = Parameters<typeof getScheduledTheme>[0];
|
||||||
|
|
||||||
|
function state(over: Partial<SchedState> = {}): 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
+17
-1
@@ -18,11 +18,17 @@ export type BuiltinTheme =
|
|||||||
*/
|
*/
|
||||||
export type Theme = BuiltinTheme | (string & {});
|
export type Theme = BuiltinTheme | (string & {});
|
||||||
|
|
||||||
|
/** Trigger for the day/night theme switch. */
|
||||||
|
export type ThemeSchedulerMode = 'time' | 'system';
|
||||||
|
|
||||||
interface ThemeState {
|
interface ThemeState {
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
setTheme: (theme: Theme) => void;
|
setTheme: (theme: Theme) => void;
|
||||||
enableThemeScheduler: boolean;
|
enableThemeScheduler: boolean;
|
||||||
setEnableThemeScheduler: (v: boolean) => void;
|
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;
|
themeDay: string;
|
||||||
setThemeDay: (v: string) => void;
|
setThemeDay: (v: string) => void;
|
||||||
themeNight: string;
|
themeNight: string;
|
||||||
@@ -51,8 +57,16 @@ interface ThemeState {
|
|||||||
setExternalArtworkByok: (v: string) => void;
|
setExternalArtworkByok: (v: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getScheduledTheme(state: Pick<ThemeState, 'enableThemeScheduler' | 'theme' | 'themeDay' | 'themeNight' | 'timeDayStart' | 'timeNightStart'>): string {
|
export function getScheduledTheme(
|
||||||
|
state: Pick<
|
||||||
|
ThemeState,
|
||||||
|
'enableThemeScheduler' | 'schedulerMode' | 'theme' | 'themeDay' | 'themeNight' | 'timeDayStart' | 'timeNightStart'
|
||||||
|
>,
|
||||||
|
systemPrefersDark = false,
|
||||||
|
): string {
|
||||||
if (!state.enableThemeScheduler) return state.theme;
|
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 now = new Date();
|
||||||
const nowMins = now.getHours() * 60 + now.getMinutes();
|
const nowMins = now.getHours() * 60 + now.getMinutes();
|
||||||
const [dh, dm] = state.timeDayStart.split(':').map(Number);
|
const [dh, dm] = state.timeDayStart.split(':').map(Number);
|
||||||
@@ -72,6 +86,8 @@ export const useThemeStore = create<ThemeState>()(
|
|||||||
setTheme: (theme) => set({ theme }),
|
setTheme: (theme) => set({ theme }),
|
||||||
enableThemeScheduler: false,
|
enableThemeScheduler: false,
|
||||||
setEnableThemeScheduler: (v) => set({ enableThemeScheduler: v }),
|
setEnableThemeScheduler: (v) => set({ enableThemeScheduler: v }),
|
||||||
|
schedulerMode: 'time',
|
||||||
|
setSchedulerMode: (v) => set({ schedulerMode: v }),
|
||||||
themeDay: 'latte',
|
themeDay: 'latte',
|
||||||
setThemeDay: (v) => set({ themeDay: v }),
|
setThemeDay: (v) => set({ themeDay: v }),
|
||||||
themeNight: 'mocha',
|
themeNight: 'mocha',
|
||||||
|
|||||||
Reference in New Issue
Block a user