mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
d49424e95b
* feat(ui): prototype compact hero and toolbar buttons (Large/Small appearance setting) * feat(settings): rename action-button size toggle to "Compact buttons" Promote the hero-button prototype to a real, app-wide setting. - rename heroButtonSize → buttonSize, data-hero-buttons → data-button-size, hero-action-bar/hero-btn-label → compact-action-bar/compact-btn-label - relabel "Hero buttons" → "Compact buttons" and broaden the description (action + toolbar buttons across detail pages and browse views) in all 11 locales - move the feature CSS out of cover-lightbox.css into its own compact-buttons.css - add tests: themeStore buttonSize toggle, SelectionToggleButton * docs(changelog): compact buttons appearance toggle (#1189) * docs(credits): compact buttons appearance toggle (#1189)
131 lines
5.0 KiB
TypeScript
131 lines
5.0 KiB
TypeScript
import { create } from 'zustand';
|
|
import { persist } from 'zustand/middleware';
|
|
|
|
/** Themes that ship bundled with the app and can never be uninstalled. */
|
|
export type BuiltinTheme =
|
|
| 'mocha'
|
|
| 'latte'
|
|
| 'kanagawa-wave'
|
|
| 'stark-hud'
|
|
| 'vision-dark'
|
|
| 'vision-navy';
|
|
|
|
/**
|
|
* A theme id. Built-in ids get autocomplete; installed community themes apply
|
|
* any string id (the `& {}` keeps the literal hints without collapsing to a
|
|
* bare `string`). Non-core palettes now live in the community Theme Store and
|
|
* are applied by their string id once installed.
|
|
*/
|
|
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;
|
|
setThemeNight: (v: string) => void;
|
|
timeDayStart: string;
|
|
setTimeDayStart: (v: string) => void;
|
|
timeNightStart: string;
|
|
setTimeNightStart: (v: string) => void;
|
|
enableCoverArtBackground: boolean;
|
|
setEnableCoverArtBackground: (v: boolean) => void;
|
|
enablePlaylistCoverPhoto: boolean;
|
|
setEnablePlaylistCoverPhoto: (v: boolean) => void;
|
|
showBitrate: boolean;
|
|
setShowBitrate: (v: boolean) => void;
|
|
/** Compact (icon-only) vs. large action/toolbar buttons across detail pages and browse views. */
|
|
buttonSize: 'large' | 'small';
|
|
setButtonSize: (v: 'large' | 'small') => void;
|
|
showRemainingTime: boolean;
|
|
setShowRemainingTime: (v: boolean) => void;
|
|
expandReplayGain: boolean;
|
|
setExpandReplayGain: (v: boolean) => void;
|
|
floatingPlayerBar: boolean;
|
|
setFloatingPlayerBar: (v: boolean) => void;
|
|
/** Master toggle for external artist artwork (fanart.tv). Off by default (§20). */
|
|
externalArtworkEnabled: boolean;
|
|
setExternalArtworkEnabled: (v: boolean) => void;
|
|
/** Optional personal fanart.tv API key (BYOK) — sent in addition to the app key (§22). */
|
|
externalArtworkByok: string;
|
|
setExternalArtworkByok: (v: string) => void;
|
|
}
|
|
|
|
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);
|
|
const [nh, nm] = state.timeNightStart.split(':').map(Number);
|
|
const dayMins = dh * 60 + dm;
|
|
const nightMins = nh * 60 + nm;
|
|
const isDay = dayMins < nightMins
|
|
? nowMins >= dayMins && nowMins < nightMins
|
|
: nowMins >= dayMins || nowMins < nightMins;
|
|
return isDay ? state.themeDay : state.themeNight;
|
|
}
|
|
|
|
export const useThemeStore = create<ThemeState>()(
|
|
persist(
|
|
(set) => ({
|
|
theme: 'mocha',
|
|
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',
|
|
setThemeNight: (v) => set({ themeNight: v }),
|
|
timeDayStart: '07:00',
|
|
setTimeDayStart: (v) => set({ timeDayStart: v }),
|
|
timeNightStart: '19:00',
|
|
setTimeNightStart: (v) => set({ timeNightStart: v }),
|
|
enableCoverArtBackground: true,
|
|
setEnableCoverArtBackground: (v) => set({ enableCoverArtBackground: v }),
|
|
enablePlaylistCoverPhoto: true,
|
|
setEnablePlaylistCoverPhoto: (v) => set({ enablePlaylistCoverPhoto: v }),
|
|
showBitrate: true,
|
|
setShowBitrate: (v) => set({ showBitrate: v }),
|
|
buttonSize: 'large',
|
|
setButtonSize: (v) => set({ buttonSize: v }),
|
|
showRemainingTime: false,
|
|
setShowRemainingTime: (v) => set({ showRemainingTime: v }),
|
|
expandReplayGain: false,
|
|
setExpandReplayGain: (v) => set({ expandReplayGain: v }),
|
|
floatingPlayerBar: false,
|
|
setFloatingPlayerBar: (v) => set({ floatingPlayerBar: v }),
|
|
externalArtworkEnabled: false,
|
|
setExternalArtworkEnabled: (v) => set({ externalArtworkEnabled: v }),
|
|
externalArtworkByok: '',
|
|
setExternalArtworkByok: (v) => set({ externalArtworkByok: v }),
|
|
}),
|
|
{
|
|
name: 'psysonic_theme',
|
|
version: 1,
|
|
// Identity migrate: preserve persisted state from older versions as-is.
|
|
// Theme-id repair for removed / store-only themes now happens in the
|
|
// pre-React bootstrap migration (see utils/themes/themeMigration).
|
|
migrate: (persistedState) => persistedState,
|
|
}
|
|
)
|
|
);
|