mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
feat(titlebar): selectable window button styles + minimize toggle (#1083)
* feat(titlebar): selectable window button styles + minimize toggle Custom title bar (Linux) gains a window-button style picker, mirroring the seekbar style picker pattern. Six form-named styles: dots, dotsGlyph, flat, pill, outline, glyph. All buttons now carry minimize/maximize/close glyphs for clear, colour-blind-friendly iconography; dots reveal glyphs on hover, dotsGlyph always shows them. - New authStore state windowButtonStyle (default dots) + showMinimizeButton, with rehydrate validation falling back to dots on unknown values. - WindowButtonPreview reuses the real .titlebar-btn classes for WYSIWYG tiles. - Picker + minimize toggle render under the Custom title bar setting, gated on the toggle being on. - Monochrome styles use --text-primary glyphs and stronger borders for contrast on dark themes. - Dev-build grey marker scoped to the real title bar so previews show true colours. - i18n keys in all 9 locales; setter and rehydrate tests. * docs(changelog): window button styles (#1083)
This commit is contained in:
@@ -131,6 +131,28 @@ describe('onRehydrate migrations', () => {
|
||||
expect(useAuthStore.getState().seekbarStyle).toBe('neon');
|
||||
});
|
||||
|
||||
it('falls back an invalid windowButtonStyle to `dots`', async () => {
|
||||
writePersistedState({
|
||||
servers: [],
|
||||
activeServerId: null,
|
||||
windowButtonStyle: 'bogus', // not in VALID_WINDOW_BUTTON_STYLES
|
||||
});
|
||||
|
||||
await useAuthStore.persist.rehydrate();
|
||||
expect(useAuthStore.getState().windowButtonStyle).toBe('dots');
|
||||
});
|
||||
|
||||
it('keeps a valid windowButtonStyle unchanged', async () => {
|
||||
writePersistedState({
|
||||
servers: [],
|
||||
activeServerId: null,
|
||||
windowButtonStyle: 'glyph',
|
||||
});
|
||||
|
||||
await useAuthStore.persist.rehydrate();
|
||||
expect(useAuthStore.getState().windowButtonStyle).toBe('glyph');
|
||||
});
|
||||
|
||||
it('strips the removed `animationMode` and `reducedAnimations` legacy fields', async () => {
|
||||
writePersistedState({
|
||||
servers: [],
|
||||
|
||||
@@ -58,6 +58,8 @@ describe('trivial pass-through setters', () => {
|
||||
['setDiscordRichPresence', 'discordRichPresence', true],
|
||||
['setEnableBandsintown', 'enableBandsintown', true],
|
||||
['setUseCustomTitlebar', 'useCustomTitlebar', true],
|
||||
['setWindowButtonStyle', 'windowButtonStyle', 'flat'],
|
||||
['setShowMinimizeButton', 'showMinimizeButton', false],
|
||||
['setPreloadMiniPlayer', 'preloadMiniPlayer', true],
|
||||
['setLinuxWebkitKineticScroll', 'linuxWebkitKineticScroll', false],
|
||||
['setLinuxWaylandTextRenderProfile', 'linuxWaylandTextRenderProfile', 'gpu'],
|
||||
|
||||
@@ -74,6 +74,8 @@ export const useAuthStore = create<AuthState>()(
|
||||
discordTemplateLargeText: '{album}',
|
||||
discordTemplateName: '{title}',
|
||||
useCustomTitlebar: false,
|
||||
windowButtonStyle: 'dots',
|
||||
showMinimizeButton: true,
|
||||
preloadMiniPlayer: false,
|
||||
linuxWebkitKineticScroll: true,
|
||||
linuxWaylandTextRenderProfile: 'sharp',
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
LyricsSourceConfig,
|
||||
QueueDisplayMode,
|
||||
SeekbarStyle,
|
||||
WindowButtonStyle,
|
||||
} from './authStoreTypes';
|
||||
import { migrateLegacyLastfm, sanitizeAccounts } from '../music-network';
|
||||
|
||||
@@ -93,6 +94,17 @@ export function computeAuthStoreRehydration(state: AuthState): Partial<AuthState
|
||||
? {}
|
||||
: { seekbarStyle: 'truewave' as SeekbarStyle };
|
||||
|
||||
// Unknown / missing / tampered window-button style falls back to the
|
||||
// default 'dots' so the title bar never renders an unstyled data-attr.
|
||||
const VALID_WINDOW_BUTTON_STYLES = new Set<string>([
|
||||
'dots', 'dotsGlyph', 'flat', 'pill', 'outline', 'glyph',
|
||||
]);
|
||||
const windowButtonStyleMigrated = VALID_WINDOW_BUTTON_STYLES.has(
|
||||
(state as { windowButtonStyle?: unknown }).windowButtonStyle as string,
|
||||
)
|
||||
? {}
|
||||
: { windowButtonStyle: 'dots' as WindowButtonStyle };
|
||||
|
||||
// Garbage / null / undefined / missing key from a legacy or tampered persist
|
||||
// payload maps back to 'total' so the duration chip never receives an
|
||||
// unknown mode (would render an empty label).
|
||||
@@ -236,6 +248,7 @@ export function computeAuthStoreRehydration(state: AuthState): Partial<AuthState
|
||||
...youLyPlusMigrated,
|
||||
...wheelSmoothOneTime,
|
||||
...seekbarStyleMigrated,
|
||||
...windowButtonStyleMigrated,
|
||||
...queueDurationDisplayModeMigrated,
|
||||
...queueDisplayModeMigrated,
|
||||
...linuxWaylandTextRenderProfileMigrated,
|
||||
|
||||
@@ -33,6 +33,17 @@ export interface ServerProfile {
|
||||
}
|
||||
|
||||
export type SeekbarStyle = 'truewave' | 'pseudowave' | 'linedot' | 'bar' | 'thick' | 'segmented' | 'neon' | 'pulsewave' | 'particletrail' | 'liquidfill' | 'retrotape';
|
||||
/**
|
||||
* Look of the custom-title-bar window buttons (minimize/maximize/close).
|
||||
* Form-descriptive names, not OS brands:
|
||||
* - `dots`: coloured traffic-light circles, glyphs appear on hover (default).
|
||||
* - `dotsGlyph`: traffic-light circles with always-visible glyphs (colour + shape).
|
||||
* - `flat`: full-height rectangular buttons with line glyphs, red close hover.
|
||||
* - `pill`: soft circular monochrome buttons with glyphs.
|
||||
* - `outline`: square bordered buttons with thin glyphs, accent hover.
|
||||
* - `glyph`: themed monochrome glyphs only, no background — blends with the app.
|
||||
*/
|
||||
export type WindowButtonStyle = 'dots' | 'dotsGlyph' | 'flat' | 'pill' | 'outline' | 'glyph';
|
||||
/** Queue header duration chip: total duration / time left / ETA finish clock. */
|
||||
export type DurationMode = 'total' | 'remaining' | 'eta';
|
||||
|
||||
@@ -143,6 +154,10 @@ export interface AuthState {
|
||||
* Empty string falls back to "Psysonic". */
|
||||
discordTemplateName: string;
|
||||
useCustomTitlebar: boolean;
|
||||
/** Look of the custom-title-bar window buttons (Linux custom title bar only). */
|
||||
windowButtonStyle: WindowButtonStyle;
|
||||
/** Show the minimize button in the custom title bar. Off = only maximize + close. */
|
||||
showMinimizeButton: boolean;
|
||||
/** Pre-build the mini-player webview at app start on Linux/macOS so content is available instantly
|
||||
* on first open. Ignored on Windows — that platform always pre-creates as a hang workaround. */
|
||||
preloadMiniPlayer: boolean;
|
||||
@@ -344,6 +359,8 @@ export interface AuthState {
|
||||
setDiscordTemplateLargeText: (v: string) => void;
|
||||
setDiscordTemplateName: (v: string) => void;
|
||||
setUseCustomTitlebar: (v: boolean) => void;
|
||||
setWindowButtonStyle: (v: WindowButtonStyle) => void;
|
||||
setShowMinimizeButton: (v: boolean) => void;
|
||||
setPreloadMiniPlayer: (v: boolean) => void;
|
||||
setLinuxWebkitKineticScroll: (v: boolean) => void;
|
||||
setLinuxWaylandTextRenderProfile: (v: LinuxWaylandTextRenderProfile) => void;
|
||||
|
||||
@@ -19,6 +19,8 @@ export function createUiAppearanceActions(set: SetState): Pick<
|
||||
| 'setClockFormat'
|
||||
| 'setShowOrbitTrigger'
|
||||
| 'setUseCustomTitlebar'
|
||||
| 'setWindowButtonStyle'
|
||||
| 'setShowMinimizeButton'
|
||||
| 'setPreloadMiniPlayer'
|
||||
| 'setLinuxWebkitKineticScroll'
|
||||
| 'setLinuxWaylandTextRenderProfile'
|
||||
@@ -41,6 +43,8 @@ export function createUiAppearanceActions(set: SetState): Pick<
|
||||
setClockFormat: (v) => set({ clockFormat: v }),
|
||||
setShowOrbitTrigger: (v) => set({ showOrbitTrigger: v }),
|
||||
setUseCustomTitlebar: (v) => set({ useCustomTitlebar: v }),
|
||||
setWindowButtonStyle: (v) => set({ windowButtonStyle: v }),
|
||||
setShowMinimizeButton: (v) => set({ showMinimizeButton: v }),
|
||||
setPreloadMiniPlayer: (v) => set({ preloadMiniPlayer: v }),
|
||||
setLinuxWebkitKineticScroll: (v) => set({ linuxWebkitKineticScroll: v }),
|
||||
setLinuxWaylandTextRenderProfile: (v) => set({ linuxWaylandTextRenderProfile: v }),
|
||||
|
||||
Reference in New Issue
Block a user