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
+2 -2
View File
@@ -17,9 +17,9 @@ import { resolveCoverDisplayTier } from '@/cover/tiers';
import { acquireUrl } from '@/utils/imageCache/urlPool';
import { OpenArtistRefInline } from '@/features/artist';
import { fetchAlbumTracks, playAlbum, playAlbumShuffled } from '@/utils/playback/playAlbum';
import { useLongPressAction } from '@/hooks/useLongPressAction';
import { useLongPressAction } from '@/lib/hooks/useLongPressAction';
import { LongPressWaveOverlay } from '@/components/LongPressWaveOverlay';
import { useDragDrop } from '@/contexts/DragDropContext';
import { useDragDrop } from '@/lib/dnd/DragDropContext';
import { isAlbumRecentlyAdded } from '@/features/album/utils/albumRecency';
import { albumArtistDisplayName, deriveAlbumArtistRefs } from '@/features/album/utils/deriveAlbumHeaderArtistRefs';
import { coverServerScopeForServerId } from '@/cover/serverScope';
@@ -16,7 +16,7 @@ vi.mock('react-router-dom', async importActual => {
vi.mock('@/cover/useLibraryCoverRef', () => ({ useAlbumCoverRef: () => undefined }));
vi.mock('@/cover/lightbox', () => ({ useCoverLightboxSrc: () => ({ open: vi.fn(), lightbox: null }) }));
vi.mock('@/features/album/hooks/useAlbumDetailBack', () => ({ useAlbumDetailBack: () => vi.fn() }));
vi.mock('@/hooks/useIsMobile', () => ({ useIsMobile: () => false }));
vi.mock('@/lib/hooks/useIsMobile', () => ({ useIsMobile: () => false }));
vi.mock('@/store/themeStore', () => ({ useThemeStore: () => false }));
vi.mock('@/components/StarRating', () => ({ default: () => null }));
vi.mock('@/features/artist', () => ({ OpenArtistRefInline: () => null }));
@@ -7,7 +7,7 @@ import { CoverArtImage } from '@/cover/CoverArtImage';
import { useAlbumCoverRef } from '@/cover/useLibraryCoverRef';
import { useCoverLightboxSrc } from '@/cover/lightbox';
import { useTranslation } from 'react-i18next';
import { useIsMobile } from '@/hooks/useIsMobile';
import { useIsMobile } from '@/lib/hooks/useIsMobile';
import { useAlbumDetailBack } from '@/features/album/hooks/useAlbumDetailBack';
import { useThemeStore } from '@/store/themeStore';
import StarRating from '@/components/StarRating';
@@ -4,7 +4,7 @@ import React, { useMemo, useState, useEffect } from 'react';
import { useTracklistColumns } from '@/utils/useTracklistColumns';
import { usePlayerStore } from '@/store/playerStore';
import { useTranslation } from 'react-i18next';
import { useIsMobile } from '@/hooks/useIsMobile';
import { useIsMobile } from '@/lib/hooks/useIsMobile';
import { useSelectionStore } from '@/store/selectionStore';
import {
COLUMNS,
@@ -38,7 +38,7 @@ import {
} from '@/utils/library/albumBrowseCatalogChunk';
import { useOfflineBrowseContext } from '@/features/offline';
import { useClientSliceInfiniteScroll } from '@/hooks/useClientSliceInfiniteScroll';
import { useDebouncedValue } from '@/hooks/useDebouncedValue';
import { useDebouncedValue } from '@/lib/hooks/useDebouncedValue';
import { useInpageScrollSentinel } from '@/hooks/useInpageScrollSentinel';
const PAGE_SIZE = 30;
@@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useRef } from 'react';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import { useSelectionStore } from '@/store/selectionStore';
import { useDragDrop } from '@/contexts/DragDropContext';
import { useDragDrop } from '@/lib/dnd/DragDropContext';
import { songToTrack } from '@/utils/playback/songToTrack';
interface UseAlbumTrackListSelectionArgs {
+1 -1
View File
@@ -26,7 +26,7 @@ import { Download, HardDriveDownload, Disc3, ListPlus } from 'lucide-react';
import SelectionToggleButton from '@/components/SelectionToggleButton';
import FilterQuickClear from '@/components/FilterQuickClear';
import { usePerfProbeFlags } from '@/utils/perf/perfFlags';
import { useRangeSelection } from '@/hooks/useRangeSelection';
import { useRangeSelection } from '@/lib/hooks/useRangeSelection';
import { useMainstageInpageHeaderTight } from '@/hooks/useMainstageInpageHeaderTight';
import { useInpageScrollViewport } from '@/hooks/useInpageScrollViewport';
import InpageScrollSentinel from '@/components/InpageScrollSentinel';
+1 -1
View File
@@ -12,7 +12,7 @@ import { useOfflineStore } from '@/features/offline';
import { useDownloadModalStore } from '@/features/offline';
import { usePlayerStore } from '@/store/playerStore';
import { useZipDownloadStore } from '@/features/offline';
import { useRangeSelection } from '@/hooks/useRangeSelection';
import { useRangeSelection } from '@/lib/hooks/useRangeSelection';
import { useMainstageInpageHeaderTight } from '@/hooks/useMainstageInpageHeaderTight';
import { usePerfProbeFlags } from '@/utils/perf/perfFlags';
import { showToast } from '@/utils/ui/toast';
+1 -1
View File
@@ -22,7 +22,7 @@ import { invoke } from '@tauri-apps/api/core';
import { join } from '@tauri-apps/api/path';
import { showToast } from '@/utils/ui/toast';
import { useZipDownloadStore } from '@/features/offline';
import { useRangeSelection } from '@/hooks/useRangeSelection';
import { useRangeSelection } from '@/lib/hooks/useRangeSelection';
import { usePerfProbeFlags } from '@/utils/perf/perfFlags';
import { albumGridWarmCovers, COVER_DENSE_GRID_MIN_CELL_CSS_PX } from '@/cover/layoutSizes';
import {
@@ -10,7 +10,7 @@ import { useOfflineStore } from '@/features/offline';
import { useAuthStore } from '@/store/authStore';
import { useThemeStore } from '@/store/themeStore';
import { useArtistOfflineState } from '@/features/artist/hooks/useArtistOfflineState';
import { useIsMobile } from '@/hooks/useIsMobile';
import { useIsMobile } from '@/lib/hooks/useIsMobile';
import { ArtistHeroCover } from '@/cover/artistHero';
import { useArtistBanner, useArtistFanart } from '@/cover/useArtistFanart';
import { backdropFromConfig } from '@/cover/artistBackdrop';
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { ChevronDown, ChevronUp } from 'lucide-react';
import type { SubsonicArtist } from '@/lib/api/subsonicTypes';
import { useIsMobile } from '@/hooks/useIsMobile';
import { useIsMobile } from '@/lib/hooks/useIsMobile';
interface Props {
marginTop: string;
+2 -2
View File
@@ -9,8 +9,8 @@ import { useAuthStore } from '@/store/authStore';
import { useTranslation } from 'react-i18next';
import { useVirtualizer } from '@tanstack/react-virtual';
import { APP_MAIN_SCROLL_VIEWPORT_ID, ARTISTS_INPAGE_SCROLL_VIEWPORT_ID } from '@/constants/appScroll';
import { useElementClientHeightById, useElementClientHeightForElement } from '@/hooks/useResizeClientHeight';
import { useVirtualizerScrollMargin } from '@/hooks/useVirtualizerScrollMargin';
import { useElementClientHeightById, useElementClientHeightForElement } from '@/lib/hooks/useResizeClientHeight';
import { useVirtualizerScrollMargin } from '@/lib/hooks/useVirtualizerScrollMargin';
import { usePerfProbeFlags } from '@/utils/perf/perfFlags';
import {
ALL_SENTINEL,
@@ -10,12 +10,12 @@ import { usePlayerStore } from '@/store/playerStore';
import { previewInputFromSong, usePreviewStore } from '@/store/previewStore';
import { useSelectionStore } from '@/store/selectionStore';
import { useThemeStore } from '@/store/themeStore';
import { useDragDrop } from '@/contexts/DragDropContext';
import { useDragDrop } from '@/lib/dnd/DragDropContext';
import { useOrbitSongRowBehavior } from '@/features/orbit';
import { songToTrack } from '@/utils/playback/songToTrack';
import { appendServerQuery } from '@/utils/navigation/detailServerScope';
import { APP_MAIN_SCROLL_VIEWPORT_ID } from '@/constants/appScroll';
import { useElementClientHeightById } from '@/hooks/useResizeClientHeight';
import { useElementClientHeightById } from '@/lib/hooks/useResizeClientHeight';
import { SORTABLE_COLUMNS } from '@/features/favorites/hooks/useFavoritesSongFiltering';
interface Props {
@@ -5,7 +5,7 @@ import { emit } from '@tauri-apps/api/event';
import { invoke } from '@tauri-apps/api/core';
import { useTranslation } from 'react-i18next';
import { usePlayerStore } from '@/store/playerStore';
import { registerQueueDragHitTest } from '@/contexts/DragDropContext';
import { registerQueueDragHitTest } from '@/lib/dnd/DragDropContext';
import MiniContextMenu from '@/features/miniPlayer/components/MiniContextMenu';
import type { MiniSyncPayload, MiniControlAction, MiniTrackInfo } from '@/features/miniPlayer/utils/miniPlayerBridge';
import {
@@ -1,6 +1,6 @@
import React, { useEffect, useRef, useState } from 'react';
import { emit } from '@tauri-apps/api/event';
import { useDragDrop } from '@/contexts/DragDropContext';
import { useDragDrop } from '@/lib/dnd/DragDropContext';
import { usePlayerStore } from '@/store/playerStore';
interface Args {
+1 -1
View File
@@ -1,6 +1,6 @@
import { useEffect, useRef } from 'react';
import { emit, listen } from '@tauri-apps/api/event';
import { useWindowVisibility } from '@/hooks/useWindowVisibility';
import { useWindowVisibility } from '@/lib/hooks/useWindowVisibility';
import type { MiniSyncPayload } from '@/features/miniPlayer/utils/miniPlayerBridge';
interface ProgressPayload {
@@ -1,5 +1,5 @@
import React from 'react';
import { useDragSource } from '@/contexts/DragDropContext';
import { useDragSource } from '@/lib/dnd/DragDropContext';
import type { NpCardId } from '@/features/nowPlaying/store/nowPlayingLayoutStore';
interface NpCardWrapProps {
+1 -1
View File
@@ -13,7 +13,7 @@ import { useAuthStore } from '@/store/authStore';
import { useLyricsStore } from '@/store/lyricsStore';
import { songToTrack } from '@/utils/playback/songToTrack';
import { useRadioMetadata } from '@/features/radio';
import { useDragDrop } from '@/contexts/DragDropContext';
import { useDragDrop } from '@/lib/dnd/DragDropContext';
import OverlayScrollArea from '@/ui/OverlayScrollArea';
import {
useNpLayoutStore, NP_CARD_IDS,
@@ -8,7 +8,7 @@ import {
displayPlaylistName, isSmartPlaylistName, type PendingSmartPlaylist,
} from '@/features/playlist/utils/playlistsSmart';
import { formatHumanHoursMinutes } from '@/lib/format/formatHumanDuration';
import { useDragSource } from '@/contexts/DragDropContext';
import { useDragSource } from '@/lib/dnd/DragDropContext';
import { PlaylistCardMainCover, PlaylistSmartCoverCell } from '@/features/playlist/components/PlaylistCoverImages';
interface Props {
@@ -4,7 +4,7 @@ import { ChevronRight, Folder, Pencil, Trash2 } from 'lucide-react';
import type { SubsonicPlaylist } from '@/lib/api/subsonicTypes';
import { usePlaylistFolderStore } from '@/features/playlist/store/playlistFolderStore';
import type { PlaylistFolder } from '@/features/playlist/utils/playlistFolders';
import { useDragDrop } from '@/contexts/DragDropContext';
import { useDragDrop } from '@/lib/dnd/DragDropContext';
import { VirtualCardGrid } from '@/components/VirtualCardGrid';
interface Props {
@@ -4,7 +4,7 @@ import PlaylistRow, { type PlaylistRowCallbacks } from '@/features/playlist/comp
import { TracklistColumnPicker } from '@/components/albumTrackList/TracklistColumnPicker';
import { useTranslation } from 'react-i18next';
import { APP_MAIN_SCROLL_VIEWPORT_ID } from '@/constants/appScroll';
import { useElementClientHeightById } from '@/hooks/useResizeClientHeight';
import { useElementClientHeightById } from '@/lib/hooks/useResizeClientHeight';
import { useNavigate } from 'react-router-dom';
import {
ListPlus, Search, Trash2, X,
@@ -15,7 +15,7 @@ import type { Track } from '@/store/playerStoreTypes';
import { usePlayerStore } from '@/store/playerStore';
import { previewInputFromSong, usePreviewStore } from '@/store/previewStore';
import { useThemeStore } from '@/store/themeStore';
import { useDragDrop } from '@/contexts/DragDropContext';
import { useDragDrop } from '@/lib/dnd/DragDropContext';
import { useOrbitSongRowBehavior } from '@/features/orbit';
import { songToTrack } from '@/utils/playback/songToTrack';
import type { PlaylistSortKey, PlaylistSortDir } from '@/features/playlist/utils/playlistDisplayedSongs';
@@ -2,7 +2,7 @@ import React from 'react';
import type { SubsonicPlaylist } from '@/lib/api/subsonicTypes';
import { EMPTY_SERVER_FOLDERS, usePlaylistFolderStore } from '@/features/playlist/store/playlistFolderStore';
import { groupPlaylistsByFolder } from '@/features/playlist/utils/playlistFolders';
import { useDragDrop } from '@/contexts/DragDropContext';
import { useDragDrop } from '@/lib/dnd/DragDropContext';
import PlaylistFolderSection from '@/features/playlist/components/PlaylistFolderSection';
interface Props {
@@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import { useDragDrop } from '@/contexts/DragDropContext';
import { useDragDrop } from '@/lib/dnd/DragDropContext';
import { runPlaylistReorderDrop } from '@/features/playlist/utils/runPlaylistReorderDrop';
export interface PlaylistDnDReorderDeps {
@@ -12,7 +12,7 @@ import { useAlbumOfflineState } from '@/features/album';
import { useAuthStore } from '@/store/authStore';
import { useDownloadModalStore } from '@/features/offline';
import { useZipDownloadStore } from '@/features/offline';
import { useDragDrop } from '@/contexts/DragDropContext';
import { useDragDrop } from '@/lib/dnd/DragDropContext';
import { useTranslation } from 'react-i18next';
import type { SpotifyCsvTrack } from '@/features/playlist/utils/spotifyCsvImport';
import { runPlaylistCsvImport } from '@/features/playlist/utils/runPlaylistCsvImport';
+1 -1
View File
@@ -8,7 +8,7 @@ import { usePlayerStore } from '@/store/playerStore';
import { usePlaylistStore } from '@/features/playlist/store/playlistStore';
import { useAuthStore } from '@/store/authStore';
import { useTranslation } from 'react-i18next';
import { useRangeSelection } from '@/hooks/useRangeSelection';
import { useRangeSelection } from '@/lib/hooks/useRangeSelection';
import {
defaultSmartFilters,
+1 -1
View File
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
import { Cast, Globe, Heart, Square, Trash2, X } from 'lucide-react';
import { open } from '@tauri-apps/plugin-shell';
import type { InternetRadioStation } from '@/lib/api/subsonicTypes';
import { useDragDrop, useDragSource } from '@/contexts/DragDropContext';
import { useDragDrop, useDragSource } from '@/lib/dnd/DragDropContext';
import { CoverArtImage } from '@/cover/CoverArtImage';
import { albumCoverRef } from '@/cover/ref';
import { coverArtIdFromRadio } from '@/cover/ids';
@@ -1,6 +1,6 @@
import { useEffect, useRef, useState } from 'react';
import { ChevronDown, ChevronUp, GripVertical } from 'lucide-react';
import { useDragDrop, useDragSource } from '@/contexts/DragDropContext';
import { useDragDrop, useDragSource } from '@/lib/dnd/DragDropContext';
import type { BackdropSource, BackdropSourcePref } from '@/cover/artistBackdrop';
import type { BackdropSurface } from '@/store/themeStore';
import { moveSourceTo, dropSourceBefore } from '@/features/settings/components/backdropReorder';
@@ -1,7 +1,7 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Keyboard, RotateCcw, X } from 'lucide-react';
import { IN_APP_SHORTCUT_ACTIONS, GLOBAL_SHORTCUT_ACTIONS } from '@/config/shortcutActions';
import { IN_APP_SHORTCUT_ACTIONS, GLOBAL_SHORTCUT_ACTIONS } from '@/lib/shortcuts/shortcutActions';
import { useGlobalShortcutsStore, type GlobalAction, buildGlobalShortcut, formatGlobalShortcut } from '@/store/globalShortcutsStore';
import { useKeybindingsStore, type KeyAction, buildInAppBinding, formatBinding } from '@/store/keybindingsStore';
import SettingsSubSection from '@/features/settings/components/SettingsSubSection';
@@ -1,6 +1,6 @@
import { useTranslation } from 'react-i18next';
import { GripVertical } from 'lucide-react';
import { useDragSource } from '@/contexts/DragDropContext';
import { useDragSource } from '@/lib/dnd/DragDropContext';
/**
* Drag handle shared by the reorder customizers. Emits an id-based payload
@@ -1,5 +1,5 @@
import type { TFunction } from 'i18next';
import { GLOBAL_SHORTCUT_ACTIONS, IN_APP_SHORTCUT_ACTIONS } from '@/config/shortcutActions';
import { GLOBAL_SHORTCUT_ACTIONS, IN_APP_SHORTCUT_ACTIONS } from '@/lib/shortcuts/shortcutActions';
import { SETTINGS_INDEX, matchScore, type Tab } from '@/features/settings/components/settingsTabs';
export type SettingsSearchHit = {