Files
Psychotoxical-psysonic/src/store/globalShortcutsStore.ts
T
cucadmuh 820f71c421 feat(dev): run dev alongside release with shared app data (#866)
* feat(dev): run dev alongside release with shared app data

Skip tauri-plugin-single-instance in debug builds so `tauri dev` can run
while an installed release instance is open. Keep the same bundle identifier
and data directory; label the dev window "Psysonic (Dev)".

* fix(dev): gate on_second_instance behind release cfg

Avoid dead_code warning in debug builds where single-instance is skipped.

* feat(dev): red sidebar brand and monochrome titlebar chrome

Tag the document in Vite dev and style the logo header with a red
background plus gray window controls so dev is obvious at a glance.

* feat(dev): skip OS hotkeys and add mobile DEV markers

Debug builds no longer register global shortcuts, MPRIS, or Windows
taskbar media controls so release keeps system input when both run.
Flip the tray icon horizontally in dev and show a fixed DEV badge on
narrow layouts.

* docs(changelog): note PR #866 parallel dev alongside release

* fix(dev): satisfy clippy needless_return in debug-only paths
2026-05-24 23:39:57 +03:00

105 lines
3.8 KiB
TypeScript

import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { invoke } from '@tauri-apps/api/core';
import { MODIFIER_KEY_CODES, formatBinding } from './keybindingsStore';
import { DEFAULT_GLOBAL_SHORTCUTS, isGlobalShortcutActionId, type GlobalAction } from '../config/shortcutActions';
/** Dev builds run alongside release — OS-level grabs stay on the release instance. */
const GLOBAL_SHORTCUTS_OS_ENABLED = !import.meta.env.DEV;
/** Build a Tauri-compatible shortcut string from a KeyboardEvent, or null if invalid. */
export function buildGlobalShortcut(e: KeyboardEvent): string | null {
if ((MODIFIER_KEY_CODES as readonly string[]).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 formatBinding(shortcut);
}
// 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: { ...DEFAULT_GLOBAL_SHORTCUTS },
setShortcut: async (action, shortcut) => {
const prev = get().shortcuts[action];
if (GLOBAL_SHORTCUTS_OS_ENABLED && prev) {
try { await invoke('unregister_global_shortcut', { shortcut: prev }); } catch {}
}
if (shortcut) {
if (GLOBAL_SHORTCUTS_OS_ENABLED) {
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 => ({ shortcuts: { ...s.shortcuts, [action]: shortcut } }));
}
} else {
set(s => {
const next = { ...s.shortcuts };
delete next[action];
return { shortcuts: next };
});
}
},
registerAll: async () => {
if (!GLOBAL_SHORTCUTS_OS_ENABLED) return;
if (_registerAllCalled) return;
_registerAllCalled = true;
const { shortcuts } = get();
for (const [action, shortcut] of Object.entries(shortcuts)) {
if (!isGlobalShortcutActionId(action)) continue;
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();
if (GLOBAL_SHORTCUTS_OS_ENABLED) {
for (const shortcut of Object.values(shortcuts)) {
if (shortcut) {
try { await invoke('unregister_global_shortcut', { shortcut }); } catch {}
}
}
}
set({ shortcuts: { ...DEFAULT_GLOBAL_SHORTCUTS } });
},
}),
{ name: 'psysonic_global_shortcuts' }
)
);
export type { GlobalAction } from '../config/shortcutActions';