feat: v1.13.0 — SVG Logo, Marquee, Player UX, Global Shortcuts fix

### Added
- SVG logo with theme-adaptive gradient in sidebar (full wordmark + P-icon for collapsed state)
- Player bar: song title/artist marquee scroll on overflow
- Player bar: live volume percentage tooltip on slider hover

### Changed
- Sidebar collapse button moved to right-edge hover tab
- Player bar: fixed 320px track info width, increased waveform margins
- Settings: Server tab opens by default
- Crossfade: experimental badge removed (stable)
- Help page: Lyrics/Keybindings/Font entries added, theme count corrected

### Fixed
- Global shortcuts double-fire (Rust-side ShortcutMap idempotency fix)
- W98 theme: comprehensive contrast fixes for navy hover backgrounds
- Help page: removed orphaned translation key under Playback

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Psychotoxical
2026-03-22 20:24:14 +01:00
parent d927ef2082
commit 5516d95b52
20 changed files with 781 additions and 97 deletions
+103
View File
@@ -0,0 +1,103 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { invoke } from '@tauri-apps/api/core';
import { formatKeyCode } from './keybindingsStore';
export type GlobalAction = 'play-pause' | 'next' | 'prev' | 'volume-up' | 'volume-down';
const MODIFIER_CODES = [
'ControlLeft', 'ControlRight', 'AltLeft', 'AltRight',
'ShiftLeft', 'ShiftRight', 'MetaLeft', 'MetaRight', 'OSLeft', 'OSRight',
];
/** Build a Tauri-compatible shortcut string from a KeyboardEvent, or null if invalid. */
export function buildGlobalShortcut(e: KeyboardEvent): string | null {
if (MODIFIER_CODES.includes(e.code)) return null;
// Require at least Ctrl, Alt, or Meta — Shift alone is too invasive
if (!e.ctrlKey && !e.altKey && !e.metaKey) return null;
const mods: string[] = [];
if (e.ctrlKey) mods.push('ctrl');
if (e.altKey) mods.push('alt');
if (e.shiftKey) mods.push('shift');
if (e.metaKey) mods.push('super');
return [...mods, e.code].join('+');
}
/** Human-readable label for a stored shortcut string, e.g. "ctrl+alt+ArrowRight" → "Ctrl+Alt+→". */
export function formatGlobalShortcut(shortcut: string): string {
return shortcut.split('+').map(part => {
if (part === 'ctrl') return 'Ctrl';
if (part === 'alt') return 'Alt';
if (part === 'shift') return 'Shift';
if (part === 'super' || part === 'meta') return 'Super';
return formatKeyCode(part);
}).join('+');
}
// Module-level guard — prevents double-registration from React StrictMode's
// intentional double-invocation of effects in development.
let _registerAllCalled = false;
interface GlobalShortcutsState {
shortcuts: Partial<Record<GlobalAction, string>>;
setShortcut: (action: GlobalAction, shortcut: string | null) => Promise<void>;
registerAll: () => Promise<void>;
resetAll: () => Promise<void>;
}
export const useGlobalShortcutsStore = create<GlobalShortcutsState>()(
persist(
(set, get) => ({
shortcuts: {},
setShortcut: async (action, shortcut) => {
const prev = get().shortcuts[action];
if (prev) {
try { await invoke('unregister_global_shortcut', { shortcut: prev }); } catch {}
}
if (shortcut) {
try {
await invoke('register_global_shortcut', { shortcut, action });
set(s => ({ shortcuts: { ...s.shortcuts, [action]: shortcut } }));
} catch (e) {
console.warn('[GlobalShortcuts] Failed to register:', shortcut, e);
}
} else {
set(s => {
const next = { ...s.shortcuts };
delete next[action];
return { shortcuts: next };
});
}
},
registerAll: async () => {
if (_registerAllCalled) return;
_registerAllCalled = true;
const { shortcuts } = get();
for (const [action, shortcut] of Object.entries(shortcuts)) {
if (shortcut) {
try {
await invoke('register_global_shortcut', { shortcut, action });
} catch (e) {
console.warn('[GlobalShortcuts] Failed to re-register:', shortcut, e);
}
}
}
},
resetAll: async () => {
const { shortcuts } = get();
for (const shortcut of Object.values(shortcuts)) {
if (shortcut) {
try { await invoke('unregister_global_shortcut', { shortcut }); } catch {}
}
}
set({ shortcuts: {} });
},
}),
{ name: 'psysonic_global_shortcuts' }
)
);