mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
refactor(queue): co-locate QueuePanel UI into features/queue
Extract the queue UI (QueuePanel + queuePanel/* components + useQueue* hooks)
into features/queue/ with a barrel. This is the chosen playback/queue boundary:
queue = the QueuePanel UI that CONSUMES the playback store; queue STATE and the
audio engine stay in store/ (playback-core, moved separately). Verified
one-way: no playback-core file imports the queue UI back (only doc comments
mention it).
Excluded as NOT queue-UI: useIdlePlayQueuePull (AppShell) and
usePlayQueueSyncLedState (ConnectionIndicator) are queue-SYNC orchestration,
not panel UI — left in hooks/ to move with playback.
Pure move. One test fix: QueuePanel.test.tsx reads its subject via a hardcoded
readFileSync('src/components/QueuePanel.tsx') path (architecture-pin grep for
forbidden HTML5 DnD) — repointed to the new location (the move tool can't
rewrite non-import string paths). tsc 0, lint 0/0, suite 319/2353 green.
This commit is contained in:
@@ -12,7 +12,7 @@ import { useIsMobile } from '@/lib/hooks/useIsMobile';
|
||||
import { LiveSearch } from '@/features/search';
|
||||
import DevNetworkModeToggle from '../components/DevNetworkModeToggle';
|
||||
import { NowPlayingDropdown } from '@/features/nowPlaying';
|
||||
import QueuePanel from '../components/QueuePanel';
|
||||
import QueuePanel from '@/features/queue';
|
||||
import AppRoutes from './AppRoutes';
|
||||
import FullscreenPlayer from '@/features/fullscreenPlayer';
|
||||
import ContextMenu from '../components/ContextMenu';
|
||||
@@ -44,7 +44,7 @@ import { useNowPlayingTrayTitle } from '../hooks/useNowPlayingTrayTitle';
|
||||
import { usePrefetchReleaseNotes } from '../hooks/usePrefetchReleaseNotes';
|
||||
import { useTrayMenuI18n } from '../hooks/useTrayMenuI18n';
|
||||
import { useServerCapabilitiesProbe } from '../hooks/useServerCapabilitiesProbe';
|
||||
import { useQueueResizer } from '../hooks/useQueueResizer';
|
||||
import { useQueueResizer } from '@/features/queue';
|
||||
import { useGlobalDndAndSelectionBlockers } from '../hooks/useGlobalDndAndSelectionBlockers';
|
||||
import { useAppActivityTracking } from '../hooks/useAppActivityTracking';
|
||||
import { useMainScrollingIndicator } from '../hooks/useMainScrollingIndicator';
|
||||
|
||||
@@ -13,7 +13,7 @@ import { useArtistFanart } from '@/cover/useArtistFanart';
|
||||
import { backdropFromConfig } from '@/cover/artistBackdrop';
|
||||
import { useThemeStore } from '@/store/themeStore';
|
||||
import { useFsIdleFade } from '@/hooks/useFsIdleFade';
|
||||
import { useQueueTrackAt } from '@/hooks/useQueueTracks';
|
||||
import { useQueueTrackAt } from '@/features/queue';
|
||||
import { WaveformSeek } from '@/features/waveform';
|
||||
import { FsQueueModal } from '@/features/fullscreenPlayer/components/FsQueueModal';
|
||||
import { FsLyricsApple } from '@/features/fullscreenPlayer/components/FsLyricsApple';
|
||||
|
||||
+14
-14
@@ -1,24 +1,24 @@
|
||||
import React from 'react';
|
||||
import { ChevronDown, FolderOpen, HardDrive, Music, Waves } from 'lucide-react';
|
||||
import type { TFunction } from 'i18next';
|
||||
import type { Track } from '../../store/playerStoreTypes';
|
||||
import type { LoudnessLufsPreset, NormalizationEngine } from '../../store/authStoreTypes';
|
||||
import type { PlaybackSourceKind } from '../../utils/playback/resolvePlaybackUrl';
|
||||
import type { Track } from '@/store/playerStoreTypes';
|
||||
import type { LoudnessLufsPreset, NormalizationEngine } from '@/store/authStoreTypes';
|
||||
import type { PlaybackSourceKind } from '@/utils/playback/resolvePlaybackUrl';
|
||||
import {
|
||||
formatQueueReplayGainParts,
|
||||
renderStars,
|
||||
} from '../../utils/componentHelpers/queuePanelHelpers';
|
||||
import { loudnessGainPlaceholderUntilCacheDb } from '../../utils/audio/loudnessPlaceholder';
|
||||
import { effectiveLoudnessPreAnalysisAttenuationDb } from '../../utils/audio/loudnessPreAnalysisSlider';
|
||||
import { formatQueueBpmTech, formatQueueMoodLabels } from '../../utils/library/trackEnrichment';
|
||||
import { useQueueTrackEnrichment } from '../../hooks/useQueueTrackEnrichment';
|
||||
import { QueueLufsTargetMenu } from './QueueLufsTargetMenu';
|
||||
import { PlaybackBufferingOverlay } from '../playback/PlaybackBufferingOverlay';
|
||||
import { CoverArtImage } from '../../cover/CoverArtImage';
|
||||
} from '@/utils/componentHelpers/queuePanelHelpers';
|
||||
import { loudnessGainPlaceholderUntilCacheDb } from '@/utils/audio/loudnessPlaceholder';
|
||||
import { effectiveLoudnessPreAnalysisAttenuationDb } from '@/utils/audio/loudnessPreAnalysisSlider';
|
||||
import { formatQueueBpmTech, formatQueueMoodLabels } from '@/utils/library/trackEnrichment';
|
||||
import { useQueueTrackEnrichment } from '@/features/queue/hooks/useQueueTrackEnrichment';
|
||||
import { QueueLufsTargetMenu } from '@/features/queue/components/QueueLufsTargetMenu';
|
||||
import { PlaybackBufferingOverlay } from '@/components/playback/PlaybackBufferingOverlay';
|
||||
import { CoverArtImage } from '@/cover/CoverArtImage';
|
||||
import { OpenArtistRefInline } from '@/features/artist';
|
||||
import { usePlaybackTrackCoverRef } from '../../cover/useLibraryCoverRef';
|
||||
import { usePlayerStore } from '../../store/playerStore';
|
||||
import { resolveTrackArtistRefs } from '../../utils/playback/trackArtistRefs';
|
||||
import { usePlaybackTrackCoverRef } from '@/cover/useLibraryCoverRef';
|
||||
import { usePlayerStore } from '@/store/playerStore';
|
||||
import { resolveTrackArtistRefs } from '@/utils/playback/trackArtistRefs';
|
||||
|
||||
interface Props {
|
||||
currentTrack: Track;
|
||||
+7
-7
@@ -2,18 +2,18 @@ import { useDeferredValue, useMemo, useSyncExternalStore } from 'react';
|
||||
import { AlignCenterVertical, ChevronDown, ListMusic, ListOrdered } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { TFunction } from 'i18next';
|
||||
import { usePlayerStore } from '../../store/playerStore';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
import type { QueueItemRef } from '../../store/playerStoreTypes';
|
||||
import type { QueueDisplayMode } from '../../store/authStoreTypes';
|
||||
import type { DurationMode } from '../../utils/componentHelpers/queuePanelHelpers';
|
||||
import { usePlayerStore } from '@/store/playerStore';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import type { QueueItemRef } from '@/store/playerStoreTypes';
|
||||
import type { QueueDisplayMode } from '@/store/authStoreTypes';
|
||||
import type { DurationMode } from '@/utils/componentHelpers/queuePanelHelpers';
|
||||
import { formatLongDuration } from '@/lib/format/formatDuration';
|
||||
import { formatClockTime } from '@/lib/format/formatClockTime';
|
||||
import { resolveQueueTrack } from '../../utils/library/queueTrackView';
|
||||
import { resolveQueueTrack } from '@/utils/library/queueTrackView';
|
||||
import {
|
||||
getQueueResolverVersion,
|
||||
subscribeQueueResolver,
|
||||
} from '../../utils/library/queueTrackResolver';
|
||||
} from '@/utils/library/queueTrackResolver';
|
||||
|
||||
interface Props {
|
||||
queue: QueueItemRef[];
|
||||
+10
-10
@@ -3,20 +3,20 @@ import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { Play } from 'lucide-react';
|
||||
import type { TFunction } from 'i18next';
|
||||
import OverlayScrollArea from '@/ui/OverlayScrollArea';
|
||||
import { usePlayerStore } from '../../store/playerStore';
|
||||
import { useLuckyMixStore } from '../../store/luckyMixStore';
|
||||
import type { QueueItemRef, PlayerState } from '../../store/playerStoreTypes';
|
||||
import type { QueueDisplayMode } from '../../store/authStoreTypes';
|
||||
import { usePlayerStore } from '@/store/playerStore';
|
||||
import { useLuckyMixStore } from '@/store/luckyMixStore';
|
||||
import type { QueueItemRef, PlayerState } from '@/store/playerStoreTypes';
|
||||
import type { QueueDisplayMode } from '@/store/authStoreTypes';
|
||||
import { formatTrackTime } from '@/lib/format/formatDuration';
|
||||
import { resolveQueueTrack } from '../../utils/library/queueTrackView';
|
||||
import { resolveQueueTrack } from '@/utils/library/queueTrackView';
|
||||
import {
|
||||
getQueueResolverVersion,
|
||||
subscribeQueueResolver,
|
||||
} from '../../utils/library/queueTrackResolver';
|
||||
import { findQueueItemRefIndex } from '../../utils/playback/queueIdentity';
|
||||
import type { TimelineDisplayRow } from '../../utils/queue/buildTimelineDisplayRows';
|
||||
import { findTimelineScrollLocalIndex } from '../../utils/queue/buildTimelineDisplayRows';
|
||||
import { playTimelineHistoryTrack } from '../../utils/queue/playTimelineHistoryTrack';
|
||||
} from '@/utils/library/queueTrackResolver';
|
||||
import { findQueueItemRefIndex } from '@/utils/playback/queueIdentity';
|
||||
import type { TimelineDisplayRow } from '@/utils/queue/buildTimelineDisplayRows';
|
||||
import { findTimelineScrollLocalIndex } from '@/utils/queue/buildTimelineDisplayRows';
|
||||
import { playTimelineHistoryTrack } from '@/utils/queue/playTimelineHistoryTrack';
|
||||
|
||||
type StartDrag = (
|
||||
payload: { data: string; label: string },
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import type { LoudnessLufsPreset } from '../../store/authStoreTypes';
|
||||
import type { LoudnessLufsPreset } from '@/store/authStoreTypes';
|
||||
|
||||
interface Props {
|
||||
menuRef: React.RefObject<HTMLDivElement | null>;
|
||||
+3
-3
@@ -30,7 +30,7 @@ vi.mock('@/features/orbit/utils/orbitBulkGuard', () => ({
|
||||
orbitBulkGuard: vi.fn(async () => true),
|
||||
}));
|
||||
|
||||
import QueuePanel from './QueuePanel';
|
||||
import QueuePanel from '@/features/queue/components/QueuePanel';
|
||||
import { fireEvent } from '@testing-library/react';
|
||||
import { renderWithProviders } from '@/test/helpers/renderWithProviders';
|
||||
import { usePlayerStore } from '@/store/playerStore';
|
||||
@@ -224,7 +224,7 @@ describe('QueuePanel — DnD architecture pin (§4.4 of v2 plan)', () => {
|
||||
// `dragenter` / `dragover` props are allowed because the document
|
||||
// listens for them to render the drop indicator without acting as
|
||||
// a sink for HTML5 payloads.
|
||||
const source = readFileSync(join(process.cwd(), 'src/components/QueuePanel.tsx'), 'utf8');
|
||||
const source = readFileSync(join(process.cwd(), 'src/features/queue/components/QueuePanel.tsx'), 'utf8');
|
||||
expect(source).not.toMatch(/dataTransfer\.setData/);
|
||||
expect(source).not.toMatch(/dataTransfer\.getData/);
|
||||
expect(source).not.toMatch(/\bonDragStart\s*=/);
|
||||
@@ -232,7 +232,7 @@ describe('QueuePanel — DnD architecture pin (§4.4 of v2 plan)', () => {
|
||||
});
|
||||
|
||||
it('the source file does not use `application/json` MIME anywhere (WebView2 restriction)', () => {
|
||||
const source = readFileSync(join(process.cwd(), 'src/components/QueuePanel.tsx'), 'utf8');
|
||||
const source = readFileSync(join(process.cwd(), 'src/features/queue/components/QueuePanel.tsx'), 'utf8');
|
||||
expect(source).not.toMatch(/application\/json/);
|
||||
});
|
||||
});
|
||||
@@ -1,40 +1,40 @@
|
||||
import { Play } from 'lucide-react';
|
||||
import { updatePlaylist } from '@/lib/api/subsonicPlaylists';
|
||||
import { resolvePlaylist, resolveMediaServerId } from '@/features/offline';
|
||||
import { songToTrack } from '../utils/playback/songToTrack';
|
||||
import type { Track } from '../store/playerStoreTypes';
|
||||
import { songToTrack } from '@/utils/playback/songToTrack';
|
||||
import type { Track } from '@/store/playerStoreTypes';
|
||||
import { useState, useRef, useMemo } from 'react';
|
||||
import { usePlayerStore } from '../store/playerStore';
|
||||
import { usePlayerStore } from '@/store/playerStore';
|
||||
import { useOrbitStore } from '@/features/orbit';
|
||||
import { OrbitGuestQueue, OrbitQueueHead } from '@/features/orbit';
|
||||
import HostApprovalQueue from './HostApprovalQueue';
|
||||
import HostApprovalQueue from '@/components/HostApprovalQueue';
|
||||
import { usePlaylistStore } from '@/features/playlist';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePlaybackLibraryNavigate } from '../hooks/usePlaybackLibraryNavigate';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
import { encodeSharePayload } from '../utils/share/shareLink';
|
||||
import { serverShareBaseUrl } from '../utils/server/serverEndpoint';
|
||||
import { copyTextToClipboard } from '../utils/server/serverMagicString';
|
||||
import { showToast } from '../utils/ui/toast';
|
||||
import { useThemeStore } from '../store/themeStore';
|
||||
import { useLyricsStore } from '../store/lyricsStore';
|
||||
import LyricsPane from './LyricsPane';
|
||||
import { usePlaybackLibraryNavigate } from '@/hooks/usePlaybackLibraryNavigate';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { encodeSharePayload } from '@/utils/share/shareLink';
|
||||
import { serverShareBaseUrl } from '@/utils/server/serverEndpoint';
|
||||
import { copyTextToClipboard } from '@/utils/server/serverMagicString';
|
||||
import { showToast } from '@/utils/ui/toast';
|
||||
import { useThemeStore } from '@/store/themeStore';
|
||||
import { useLyricsStore } from '@/store/lyricsStore';
|
||||
import LyricsPane from '@/components/LyricsPane';
|
||||
import { NowPlayingInfo } from '@/features/nowPlaying';
|
||||
import { useLuckyMixStore } from '../store/luckyMixStore';
|
||||
import { useQueueToolbarStore } from '../store/queueToolbarStore';
|
||||
import { SavePlaylistModal } from './queuePanel/SavePlaylistModal';
|
||||
import { LoadPlaylistModal } from './queuePanel/LoadPlaylistModal';
|
||||
import { QueueHeader } from './queuePanel/QueueHeader';
|
||||
import { QueueCurrentTrack } from './queuePanel/QueueCurrentTrack';
|
||||
import { useQueuePanelDrag } from '../hooks/useQueuePanelDrag';
|
||||
import { useQueueLufsTgtPopover } from '../hooks/useQueueLufsTgtPopover';
|
||||
import { QueueToolbar } from './queuePanel/QueueToolbar';
|
||||
import { QueueList } from './queuePanel/QueueList';
|
||||
import { QueueTabBar } from './queuePanel/QueueTabBar';
|
||||
import { useQueueAutoScroll } from '../hooks/useQueueAutoScroll';
|
||||
import { useTimelineBootstrapOnMode, useTimelineHistoryResolver, useTimelinePlayHistory } from '../hooks/useTimelinePlayHistory';
|
||||
import { buildTimelineDisplayRows } from '../utils/queue/buildTimelineDisplayRows';
|
||||
import { activeServerQueueTrackIds } from '../utils/playback/trackServerScope';
|
||||
import { useLuckyMixStore } from '@/store/luckyMixStore';
|
||||
import { useQueueToolbarStore } from '@/store/queueToolbarStore';
|
||||
import { SavePlaylistModal } from '@/features/queue/components/SavePlaylistModal';
|
||||
import { LoadPlaylistModal } from '@/features/queue/components/LoadPlaylistModal';
|
||||
import { QueueHeader } from '@/features/queue/components/QueueHeader';
|
||||
import { QueueCurrentTrack } from '@/features/queue/components/QueueCurrentTrack';
|
||||
import { useQueuePanelDrag } from '@/features/queue/hooks/useQueuePanelDrag';
|
||||
import { useQueueLufsTgtPopover } from '@/features/queue/hooks/useQueueLufsTgtPopover';
|
||||
import { QueueToolbar } from '@/features/queue/components/QueueToolbar';
|
||||
import { QueueList } from '@/features/queue/components/QueueList';
|
||||
import { QueueTabBar } from '@/features/queue/components/QueueTabBar';
|
||||
import { useQueueAutoScroll } from '@/features/queue/hooks/useQueueAutoScroll';
|
||||
import { useTimelineBootstrapOnMode, useTimelineHistoryResolver, useTimelinePlayHistory } from '@/hooks/useTimelinePlayHistory';
|
||||
import { buildTimelineDisplayRows } from '@/utils/queue/buildTimelineDisplayRows';
|
||||
import { activeServerQueueTrackIds } from '@/utils/playback/trackServerScope';
|
||||
|
||||
export default function QueuePanel() {
|
||||
const orbitRole = useOrbitStore(s => s.role);
|
||||
+3
-3
@@ -3,12 +3,12 @@ import {
|
||||
Blend, Check, FolderOpen, Infinity as InfinityIcon, ListMusic, MoveRight, Save, Share2, Shuffle, Trash2, Waves,
|
||||
} from 'lucide-react';
|
||||
import type { TFunction } from 'i18next';
|
||||
import type { QueueItemRef } from '../../store/playerStoreTypes';
|
||||
import type { QueueItemRef } from '@/store/playerStoreTypes';
|
||||
import type {
|
||||
QueueToolbarButtonConfig,
|
||||
QueueToolbarButtonId,
|
||||
} from '../../store/queueToolbarStore';
|
||||
import { getTransitionMode, setTransitionMode } from '../../utils/playback/playbackTransition';
|
||||
} from '@/store/queueToolbarStore';
|
||||
import { getTransitionMode, setTransitionMode } from '@/utils/playback/playbackTransition';
|
||||
import { useOrbitStore } from '@/features/orbit';
|
||||
|
||||
interface Props {
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useLayoutEffect } from 'react';
|
||||
import { registerQueueListScrollTopReader, consumePendingQueueListScrollTop } from '../store/queueUndo';
|
||||
import type { QueueItemRef, Track } from '../store/playerStoreTypes';
|
||||
import { registerQueueListScrollTopReader, consumePendingQueueListScrollTop } from '@/store/queueUndo';
|
||||
import type { QueueItemRef, Track } from '@/store/playerStoreTypes';
|
||||
|
||||
interface Args {
|
||||
queue: QueueItemRef[];
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { resolveAlbum, resolveMediaServerId } from '@/features/offline';
|
||||
import { songToTrack } from '../utils/playback/songToTrack';
|
||||
import { songToTrack } from '@/utils/playback/songToTrack';
|
||||
import { useDragDrop, registerQueueDragHitTest } from '@/lib/dnd/DragDropContext';
|
||||
import { usePlayerStore } from '../store/playerStore';
|
||||
import type { Track } from '../store/playerStoreTypes';
|
||||
import { usePlayerStore } from '@/store/playerStore';
|
||||
import type { Track } from '@/store/playerStoreTypes';
|
||||
|
||||
/** Drag types that may be dropped into the queue panel. */
|
||||
const QUEUE_DROP_TYPES = new Set(['song', 'album', 'queue_reorder']);
|
||||
+5
-5
@@ -1,16 +1,16 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { libraryGetFacts, libraryGetTrack } from '@/lib/api/library';
|
||||
import { usePlaybackServerId } from './usePlaybackServerId';
|
||||
import { useLibraryIndexStore } from '../store/libraryIndexStore';
|
||||
import { usePlaybackServerId } from '@/hooks/usePlaybackServerId';
|
||||
import { useLibraryIndexStore } from '@/store/libraryIndexStore';
|
||||
import {
|
||||
enrichmentDisplayComplete,
|
||||
OXIMEDIA_MOOD_UI_ENABLED,
|
||||
parseTrackEnrichmentFacts,
|
||||
type ParsedTrackEnrichment,
|
||||
} from '../utils/library/trackEnrichment';
|
||||
import { libraryIsReady } from '../utils/library/libraryReady';
|
||||
import { normalizeAnalysisTrackId } from '../utils/playback/queueIdentity';
|
||||
} from '@/utils/library/trackEnrichment';
|
||||
import { libraryIsReady } from '@/utils/library/libraryReady';
|
||||
import { normalizeAnalysisTrackId } from '@/utils/playback/queueIdentity';
|
||||
|
||||
const EMPTY: ParsedTrackEnrichment = {
|
||||
serverBpm: null,
|
||||
@@ -4,7 +4,7 @@ import { usePlayerStore } from '@/store/playerStore';
|
||||
import type { Track } from '@/store/playerStoreTypes';
|
||||
import { getCachedTrack, _resetQueueResolverForTest } from '@/utils/library/queueTrackResolver';
|
||||
import { seedQueue } from '@/test/helpers/factories';
|
||||
import { useQueueTrackAt, useCurrentTrack, useQueueItems } from './useQueueTracks';
|
||||
import { useQueueTrackAt, useCurrentTrack, useQueueItems } from '@/features/queue/hooks/useQueueTracks';
|
||||
|
||||
const track = (id: string, over: Partial<Track> = {}): Track =>
|
||||
({ id, title: id, artist: '', album: 'A', albumId: 'A', duration: 1, ...over });
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useMemo, useSyncExternalStore } from 'react';
|
||||
import { usePlayerStore } from '../store/playerStore';
|
||||
import type { QueueItemRef, Track } from '../store/playerStoreTypes';
|
||||
import { resolveQueueTrack } from '../utils/library/queueTrackView';
|
||||
import { usePlayerStore } from '@/store/playerStore';
|
||||
import type { QueueItemRef, Track } from '@/store/playerStoreTypes';
|
||||
import { resolveQueueTrack } from '@/utils/library/queueTrackView';
|
||||
import {
|
||||
getQueueResolverVersion,
|
||||
subscribeQueueResolver,
|
||||
} from '../utils/library/queueTrackResolver';
|
||||
} from '@/utils/library/queueTrackResolver';
|
||||
|
||||
/**
|
||||
* Stable queue selectors (queue thin-state). The store is refs-canonical now:
|
||||
@@ -0,0 +1,5 @@
|
||||
// Public API of the queue feature (QueuePanel UI + its hooks). Consumes the
|
||||
// playback store; holds no audio-engine state itself.
|
||||
export { default } from './components/QueuePanel';
|
||||
export { useQueueResizer } from './hooks/useQueueResizer';
|
||||
export { useQueueTrackAt } from './hooks/useQueueTracks';
|
||||
Reference in New Issue
Block a user