refactor(lib): move generic hooks, DnD engine, shortcut contract to lib

Continue M4 lib/ de-flattening with domain-agnostic infra:
- lib/hooks/: 9 pure generic hooks (useDebouncedValue, useIsMobile,
  useLongPressAction, useRangeSelection, useResizeClientHeight,
  useWindowVisibility, useSystemPrefersDark, useVirtualizerScrollMargin,
  useRemeasureGridVirtualizer) — all PURE (only react/zustand/tanstack deps).
- lib/dnd/DragDropContext.tsx: the generic mouse-event DnD engine (WebKitGTK
  HTML5-DnD workaround, ~24 importers, self-contained) — empties src/contexts/.
- lib/shortcuts/: shortcutActions + shortcutTypes (the action-id + binding
  contract; registry/dispatch/bindings stay app-level and import the contract,
  app->lib direction).

useWindowFullscreenState deliberately NOT moved — kept in hooks/ as app-shell
per the handoff iron-rule list (overrides its pure-helper appearance).

Pure move via deep @/lib/* specifiers. tsc 0, lint 0/0, full suite 319 files /
2353 tests green.
This commit is contained in:
Psychotoxical
2026-06-30 08:20:11 +02:00
parent 209dd61442
commit 7ad196711e
74 changed files with 78 additions and 78 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ import { usePlayerStore } from '../store/playerStore';
import { usePreviewStore } from '../store/previewStore';
import { useLyricsStore } from '../store/lyricsStore';
import { showToast } from '../utils/ui/toast';
import type { ActionContext, ShortcutSlot, ShortcutActionMeta } from './shortcutTypes';
import type { ActionContext, ShortcutSlot, ShortcutActionMeta } from '@/lib/shortcuts/shortcutTypes';
let cliPremuteVolume: number | null = null;
-74
View File
@@ -1,74 +0,0 @@
// ─── Shortcut-action subsystem ───────────────────────────────────────────────
//
// Barrel + contract reference. Split into:
// shortcutTypes.ts — shared types
// shortcutActionRegistry.ts — SHORTCUT_ACTION_REGISTRY + action id types
// shortcutDispatch.ts — runtime + CLI dispatch
// shortcutBindings.ts — derived in-app / global binding tables
// Existing call sites import from this module unchanged.
//
// ── The contract ─────────────────────────────────────────────────────────────
//
// Every shortcut action is one entry in `SHORTCUT_ACTION_REGISTRY`, keyed by a
// stable id (`ShortcutAction`). Its `ShortcutActionMeta` declares which of three
// independent *trigger surfaces* the action is exposed on — an action may opt
// into any combination:
//
// • inApp? — bindable to a keyboard chord while the MAIN window is focused.
// Presence of the `inApp` slot makes the id a `KeyAction`; the
// slot's `defaultBinding` is the out-of-box chord (or null =
// unbound), `hidden` keeps it out of the Settings UI list.
// Surfaced via IN_APP_SHORTCUT_ACTIONS / DEFAULT_IN_APP_BINDINGS;
// matched at runtime by `shortcuts/runtime.ts`.
// • global? — registrable as an OS-LEVEL global hotkey (fires even when the
// app is unfocused). Presence makes the id a `GlobalAction`;
// surfaced via GLOBAL_SHORTCUT_ACTIONS / DEFAULT_GLOBAL_SHORTCUTS.
// `isGlobalShortcutActionId` is the runtime guard.
// • runInMiniWindow — whether the action may run when triggered FROM the
// mini-player window. `canRunShortcutActionInMiniWindow` gates
// cross-window `shortcut:run-action` events.
//
// Two extra, surface-independent fields:
// • cli? — exposes the action to `psysonic --player <verb>`. No-arg CLI
// verbs are auto-collected and dispatched by
// `executeCliPlayerCommand`; arg-carrying commands (play-id,
// seek-relative, set-volume, set-repeat, set-rating-current)
// are handled explicitly there.
// • run(ctx) — the handler. `ctx.previewPolicy` ('stop' | 'ignore') decides
// whether an active track-preview is interrupted: media keys
// pass 'ignore', explicit UI / in-app keys pass 'stop'.
//
// Dispatch entry points: `executeRuntimeAction` (any trigger surface) and
// `executeCliPlayerCommand` (CLI). `isShortcutAction` validates an arbitrary
// string against the registry.
export type {
TranslateLike,
ShortcutSlot,
ActionContext,
CliContext,
ShortcutActionMeta,
} from './shortcutTypes';
export {
SHORTCUT_ACTION_REGISTRY,
type ShortcutAction,
type KeyAction,
type GlobalAction,
} from './shortcutActionRegistry';
export {
isShortcutAction,
isGlobalShortcutActionId,
canRunShortcutActionInMiniWindow,
executeRuntimeAction,
executeCliPlayerCommand,
type RuntimeAction,
} from './shortcutDispatch';
export {
IN_APP_SHORTCUT_ACTIONS,
GLOBAL_SHORTCUT_ACTIONS,
DEFAULT_IN_APP_BINDINGS,
DEFAULT_GLOBAL_SHORTCUTS,
} from './shortcutBindings';
+1 -1
View File
@@ -1,4 +1,4 @@
import type { ShortcutSlot } from './shortcutTypes';
import type { ShortcutSlot } from '@/lib/shortcuts/shortcutTypes';
import {
SHORTCUT_ACTION_REGISTRY,
type ShortcutAction,
+1 -1
View File
@@ -3,7 +3,7 @@ import i18n from '@/lib/i18n';
import { usePlayerStore } from '../store/playerStore';
import { showToast } from '../utils/ui/toast';
import { playByOpaqueId } from '../utils/playback/playByOpaqueId';
import type { ActionContext, CliContext } from './shortcutTypes';
import type { ActionContext, CliContext } from '@/lib/shortcuts/shortcutTypes';
import {
SHORTCUT_ACTION_REGISTRY,
type ShortcutAction,
-45
View File
@@ -1,45 +0,0 @@
// Shared types for the shortcut-action subsystem. The contract overview lives
// in the barrel, `shortcutActions.ts`.
import type { NavigateOptions } from 'react-router-dom';
export type TranslateLike = (key: string, options?: Record<string, unknown>) => string;
/** One bindable slot (in-app OR global). `defaultBinding` is the out-of-box
* chord, or `null` for "unbound by default". `hidden` keeps the action out of
* the Settings shortcut-list UI (still bindable / dispatchable). */
export type ShortcutSlot = { defaultBinding: string | null; hidden?: boolean };
/** Passed to an action's `run`. `previewPolicy` decides whether an active
* track-preview is interrupted: 'stop' for explicit UI / in-app keys, 'ignore'
* for hardware media keys. */
export type ActionContext = {
navigate: (to: string, options?: NavigateOptions) => void;
previewPolicy: 'stop' | 'ignore';
};
/** Passed to `executeCliPlayerCommand` — the raw `cli:player-command` payload
* plus a navigate fn. */
export type CliContext = {
navigate: (to: string, options?: NavigateOptions) => void;
payload: Record<string, unknown>;
};
/** Registry entry for one shortcut action. `inApp` / `global` /
* `runInMiniWindow` are the three independent trigger surfaces; `cli` and
* `run` are surface-independent. See the contract block in `shortcutActions.ts`. */
export type ShortcutActionMeta = {
/** Localized display label for the Settings UI. */
getLabel: (t: TranslateLike) => string;
/** Present ⇒ bindable to a main-window keyboard chord (id becomes a `KeyAction`). */
inApp?: ShortcutSlot;
/** Present ⇒ registrable as an OS-level global hotkey (id becomes a `GlobalAction`). */
global?: ShortcutSlot;
/** Whether the action may run when triggered from the mini-player window. */
runInMiniWindow: boolean;
/** The handler. */
run: (ctx: ActionContext) => void;
/** Present ⇒ exposed to `psysonic --player <verb>`. `command` overrides the
* CLI verb used for no-arg dispatch (defaults to the action id). */
cli?: { verb: string; description: string; command?: string };
};