feat(settings): clock format setting (Auto / 24h / 12h) (#742)

* feat(settings): clock format setting (Auto / 24h / 12h)

Reported on the Psysonic Discord — the Queue side panel's ETA label
and the sleep-timer preview both render via `formatClockTime`, which
just calls `toLocaleTimeString` and so follows the user's system
locale. On en-US that means AM/PM, with no in-app way out.

Add a tri-state **Clock Format** setting under
**Settings → System → App Behavior**:

* `auto` (default) — keep the existing locale-driven behaviour, so
  bestehende installs are unaffected on first launch.
* `24h` — force 24-hour wall-clock output everywhere
  `formatClockTime` is used.
* `12h` — force AM/PM output.

Wired through `authStore` (`clockFormat`, `setClockFormat`), exposed
via `CustomSelect` in `SystemTab`, and threaded into the two
consumers (`QueueHeader`, `PlaybackDelayModal`) so they re-render on
change. `formatClockTime` itself stays a pure helper — it accepts the
setting as an optional second argument and maps it to `hour12`.

Locale coverage: all nine bundled locales (en, de, es, fr, nl, nb,
ru, zh, ro) get the four new settings strings. Pin tests added for
the `setClockFormat` setter and the `hour12` mapping in
`formatClockTime`.

* docs(changelog): clock format setting + contributors (PR #742)
This commit is contained in:
Frank Stellmacher
2026-05-17 01:26:40 +02:00
committed by GitHub
parent 02e23b5755
commit 606a150e01
20 changed files with 134 additions and 6 deletions
+1
View File
@@ -54,6 +54,7 @@ describe('trivial pass-through setters', () => {
['setShowArtistImages', 'showArtistImages', true],
['setShowTrayIcon', 'showTrayIcon', false],
['setMinimizeToTray', 'minimizeToTray', true],
['setClockFormat', 'clockFormat', '24h'],
['setShowOrbitTrigger', 'showOrbitTrigger', false],
['setDiscordRichPresence', 'discordRichPresence', true],
['setEnableBandsintown', 'enableBandsintown', true],
+1
View File
@@ -62,6 +62,7 @@ export const useAuthStore = create<AuthState>()(
libraryGridMaxColumns: DEFAULT_LIBRARY_GRID_MAX_COLUMNS,
showTrayIcon: true,
minimizeToTray: false,
clockFormat: 'auto',
showOrbitTrigger: true,
discordRichPresence: false,
discordCoverSource: 'server',
+7
View File
@@ -16,6 +16,11 @@ export type SeekbarStyle = 'truewave' | 'pseudowave' | 'linedot' | 'bar' | 'thic
/** Queue header duration chip: total duration / time left / ETA finish clock. */
export type DurationMode = 'total' | 'remaining' | 'eta';
export type LoggingMode = 'off' | 'normal' | 'debug';
/**
* Wall-clock format for ETA / sleep-timer labels. `'auto'` follows the user's
* system locale (existing behaviour); explicit `'24h'` / `'12h'` overrides it.
*/
export type ClockFormat = 'auto' | '24h' | '12h';
export type NormalizationEngine = 'off' | 'replaygain' | 'loudness';
export type DiscordCoverSource = 'none' | 'apple' | 'server';
@@ -89,6 +94,7 @@ export interface AuthState {
libraryGridMaxColumns: number;
showTrayIcon: boolean;
minimizeToTray: boolean;
clockFormat: ClockFormat;
/** Whether the "Orbit" topbar trigger is rendered. Users who never
* touch Orbit can hide it so the header stays uncluttered. */
showOrbitTrigger: boolean;
@@ -266,6 +272,7 @@ export interface AuthState {
setLibraryGridMaxColumns: (v: number) => void;
setShowTrayIcon: (v: boolean) => void;
setMinimizeToTray: (v: boolean) => void;
setClockFormat: (v: ClockFormat) => void;
setShowOrbitTrigger: (v: boolean) => void;
setDiscordRichPresence: (v: boolean) => void;
setDiscordCoverSource: (v: DiscordCoverSource) => void;
+2
View File
@@ -16,6 +16,7 @@ export function createUiAppearanceActions(set: SetState): Pick<
| 'setLibraryGridMaxColumns'
| 'setShowTrayIcon'
| 'setMinimizeToTray'
| 'setClockFormat'
| 'setShowOrbitTrigger'
| 'setUseCustomTitlebar'
| 'setPreloadMiniPlayer'
@@ -37,6 +38,7 @@ export function createUiAppearanceActions(set: SetState): Pick<
setLibraryGridMaxColumns: (v) => set({ libraryGridMaxColumns: clampLibraryGridMaxColumns(v) }),
setShowTrayIcon: (v) => set({ showTrayIcon: v }),
setMinimizeToTray: (v) => set({ minimizeToTray: v }),
setClockFormat: (v) => set({ clockFormat: v }),
setShowOrbitTrigger: (v) => set({ showOrbitTrigger: v }),
setUseCustomTitlebar: (v) => set({ useCustomTitlebar: v }),
setPreloadMiniPlayer: (v) => set({ preloadMiniPlayer: v }),