From 862941c1454b37241332829532fa96e635e1d287 Mon Sep 17 00:00:00 2001 From: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com> Date: Tue, 30 Jun 2026 01:37:05 +0200 Subject: [PATCH] refactor(playlist): co-locate playlist feature into features/playlist --- src/app/AppRoutes.tsx | 4 +- src/components/QueuePanel.tsx | 4 +- .../contextMenu/AddToPlaylistSubmenu.tsx | 4 +- .../contextMenu/MoveToFolderSubmenu.tsx | 2 +- .../MultiAlbumToPlaylistSubmenu.tsx | 6 +- .../MultiArtistToPlaylistSubmenu.tsx | 8 +-- .../contextMenu/PlaylistContextItems.tsx | 8 +-- .../PlaylistToPlaylistSubmenus.tsx | 14 ++-- .../contextMenu/SongContextItems.tsx | 4 +- .../queuePanel/LoadPlaylistModal.tsx | 2 +- .../artist/utils/runArtistDetailActions.ts | 2 +- .../deviceSync/hooks/useDeviceSyncBrowser.ts | 2 +- .../offline/utils/offlineMediaResolve.test.ts | 2 +- .../offline/utils/offlineMediaResolve.ts | 2 +- .../offline/utils/pinnedOfflineSync.test.ts | 2 +- .../offline/utils/pinnedOfflineSync.ts | 4 +- .../utils/resumeIncompleteOfflinePins.ts | 2 +- src/features/orbit/utils/cleanup.test.ts | 2 +- src/features/orbit/utils/cleanup.ts | 2 +- src/features/orbit/utils/guest.ts | 2 +- src/features/orbit/utils/host.test.ts | 2 +- src/features/orbit/utils/host.ts | 2 +- src/features/orbit/utils/moderation.ts | 2 +- src/features/orbit/utils/remote.ts | 2 +- src/features/orbit/utils/sweep.ts | 2 +- .../playlist}/api/subsonicPlaylists.ts | 8 +-- .../components}/CsvImportReportModal.tsx | 4 +- .../components}/PlaylistArtistCell.test.tsx | 6 +- .../components}/PlaylistArtistCell.tsx | 4 +- .../playlist/components}/PlaylistCard.tsx | 12 ++-- .../components}/PlaylistCoverImages.tsx | 2 +- .../components}/PlaylistEditModal.tsx | 10 +-- .../components}/PlaylistFilterToolbar.tsx | 4 +- .../PlaylistFolderSection.test.tsx | 6 +- .../components}/PlaylistFolderSection.tsx | 10 +-- .../playlist/components}/PlaylistHero.tsx | 16 ++--- .../playlist/components}/PlaylistRow.tsx | 16 ++--- .../components}/PlaylistSongSearchPanel.tsx | 14 ++-- .../components}/PlaylistSuggestions.tsx | 28 ++++---- .../components}/PlaylistTracklist.tsx | 28 ++++---- .../components}/PlaylistsFolderView.tsx | 10 +-- .../components}/PlaylistsFolderViewToggle.tsx | 4 +- .../playlist/components}/PlaylistsHeader.tsx | 8 +-- .../components}/PlaylistsNewFolderButton.tsx | 4 +- .../components}/PlaylistsSmartEditor.tsx | 4 +- .../hooks/usePlaylistBulkPlayCallbacks.ts | 4 +- .../playlist}/hooks/usePlaylistCovers.ts | 10 +-- .../playlist}/hooks/usePlaylistDerived.ts | 10 +-- .../playlist}/hooks/usePlaylistDnDReorder.ts | 6 +- .../playlist}/hooks/usePlaylistPreview.ts | 4 +- .../hooks/usePlaylistRouteEffects.ts | 2 +- .../playlist}/hooks/usePlaylistSelection.ts | 2 +- .../hooks/usePlaylistSongMutations.ts | 4 +- .../playlist}/hooks/usePlaylistSongSearch.ts | 4 +- .../playlist}/hooks/usePlaylistStarRating.ts | 6 +- .../playlist}/hooks/usePlaylistSuggestions.ts | 4 +- .../hooks/usePlaylistsLibraryScopeCounts.ts | 6 +- src/features/playlist/index.ts | 44 ++++++++++++ .../playlist}/pages/PlaylistDetail.tsx | 68 +++++++++---------- .../playlist}/pages/Playlists.tsx | 44 ++++++------ .../store/playlistFolderStore.test.ts | 2 +- .../playlist}/store/playlistFolderStore.ts | 2 +- .../playlist}/store/playlistLayoutStore.ts | 0 .../playlist}/store/playlistStore.ts | 8 +-- .../playlist/utils}/playPlaylistById.ts | 8 +-- .../utils}/playlistBulkPlayActions.test.ts | 4 +- .../utils}/playlistBulkPlayActions.ts | 2 +- .../utils}/playlistDisplayedSongs.test.ts | 4 +- .../playlist/utils}/playlistDisplayedSongs.ts | 2 +- .../playlist/utils}/playlistFolders.test.ts | 2 +- .../playlist/utils}/playlistFolders.ts | 0 .../playlist/utils}/playlistsSmart.test.ts | 2 +- .../playlist/utils}/playlistsSmart.ts | 0 .../playlist/utils}/runPlaylistCsvImport.ts | 10 +-- .../playlist/utils}/runPlaylistLoad.ts | 10 +-- .../playlist/utils}/runPlaylistReorderDrop.ts | 2 +- .../playlist/utils}/runPlaylistSaveMeta.ts | 6 +- .../playlist/utils}/runPlaylistZipDownload.ts | 6 +- .../playlist/utils}/runPlaylistsActions.ts | 8 +-- .../utils}/runPlaylistsOpenSmartEditor.ts | 8 +-- .../playlist/utils}/runPlaylistsSaveSmart.ts | 10 +-- .../playlist/utils}/spotifyCsvImport.ts | 0 .../playlist/utils}/spotifyCsvMatch.ts | 0 .../playlist/utils}/startPlaylistRowDrag.ts | 4 +- .../components/PersonalisationTab.tsx | 2 +- .../components/PlaylistLayoutCustomizer.tsx | 2 +- src/features/sidebar/components/Sidebar.tsx | 2 +- .../components/SidebarPlaylistsSection.tsx | 6 +- src/hooks/usePendingSmartPolling.ts | 6 +- src/hooks/useSmartCoverCollage.ts | 4 +- src/utils/migrations/advancedModeMigration.ts | 2 +- 91 files changed, 336 insertions(+), 292 deletions(-) rename src/{ => features/playlist}/api/subsonicPlaylists.ts (93%) rename src/{components/playlist => features/playlist/components}/CsvImportReportModal.tsx (98%) rename src/{components/playlist => features/playlist/components}/PlaylistArtistCell.test.tsx (86%) rename src/{components/playlist => features/playlist/components}/PlaylistArtistCell.tsx (89%) rename src/{components/playlists => features/playlist/components}/PlaylistCard.tsx (94%) rename src/{components/playlists => features/playlist/components}/PlaylistCoverImages.tsx (92%) rename src/{components/playlist => features/playlist/components}/PlaylistEditModal.tsx (94%) rename src/{components/playlist => features/playlist/components}/PlaylistFilterToolbar.tsx (96%) rename src/{components/playlists => features/playlist/components}/PlaylistFolderSection.test.tsx (87%) rename src/{components/playlists => features/playlist/components}/PlaylistFolderSection.tsx (94%) rename src/{components/playlist => features/playlist/components}/PlaylistHero.tsx (95%) rename src/{components/playlist => features/playlist/components}/PlaylistRow.tsx (93%) rename src/{components/playlist => features/playlist/components}/PlaylistSongSearchPanel.tsx (92%) rename src/{components/playlist => features/playlist/components}/PlaylistSuggestions.tsx (92%) rename src/{components/playlist => features/playlist/components}/PlaylistTracklist.tsx (94%) rename src/{components/playlists => features/playlist/components}/PlaylistsFolderView.tsx (80%) rename src/{components/playlists => features/playlist/components}/PlaylistsFolderViewToggle.tsx (89%) rename src/{components/playlists => features/playlist/components}/PlaylistsHeader.tsx (94%) rename src/{components/playlists => features/playlist/components}/PlaylistsNewFolderButton.tsx (94%) rename src/{components/playlists => features/playlist/components}/PlaylistsSmartEditor.tsx (99%) rename src/{ => features/playlist}/hooks/usePlaylistBulkPlayCallbacks.ts (91%) rename src/{ => features/playlist}/hooks/usePlaylistCovers.ts (89%) rename src/{ => features/playlist}/hooks/usePlaylistDerived.ts (83%) rename src/{ => features/playlist}/hooks/usePlaylistDnDReorder.ts (88%) rename src/{ => features/playlist}/hooks/usePlaylistPreview.ts (85%) rename src/{ => features/playlist}/hooks/usePlaylistRouteEffects.ts (94%) rename src/{ => features/playlist}/hooks/usePlaylistSelection.ts (96%) rename src/{ => features/playlist}/hooks/usePlaylistSongMutations.ts (93%) rename src/{ => features/playlist}/hooks/usePlaylistSongSearch.ts (93%) rename src/{ => features/playlist}/hooks/usePlaylistStarRating.ts (88%) rename src/{ => features/playlist}/hooks/usePlaylistSuggestions.ts (94%) rename src/{ => features/playlist}/hooks/usePlaylistsLibraryScopeCounts.ts (93%) create mode 100644 src/features/playlist/index.ts rename src/{ => features/playlist}/pages/PlaylistDetail.tsx (85%) rename src/{ => features/playlist}/pages/Playlists.tsx (87%) rename src/{ => features/playlist}/store/playlistFolderStore.test.ts (96%) rename src/{ => features/playlist}/store/playlistFolderStore.ts (97%) rename src/{ => features/playlist}/store/playlistLayoutStore.ts (100%) rename src/{ => features/playlist}/store/playlistStore.ts (88%) rename src/{utils/playlist => features/playlist/utils}/playPlaylistById.ts (71%) rename src/{utils/playlist => features/playlist/utils}/playlistBulkPlayActions.test.ts (94%) rename src/{utils/playlist => features/playlist/utils}/playlistBulkPlayActions.ts (95%) rename src/{utils/playlist => features/playlist/utils}/playlistDisplayedSongs.test.ts (94%) rename src/{utils/playlist => features/playlist/utils}/playlistDisplayedSongs.ts (97%) rename src/{utils/playlist => features/playlist/utils}/playlistFolders.test.ts (97%) rename src/{utils/playlist => features/playlist/utils}/playlistFolders.ts (100%) rename src/{utils/playlist => features/playlist/utils}/playlistsSmart.test.ts (95%) rename src/{utils/playlist => features/playlist/utils}/playlistsSmart.ts (100%) rename src/{utils/playlist => features/playlist/utils}/runPlaylistCsvImport.ts (96%) rename src/{utils/playlist => features/playlist/utils}/runPlaylistLoad.ts (85%) rename src/{utils/playlist => features/playlist/utils}/runPlaylistReorderDrop.ts (96%) rename src/{utils/playlist => features/playlist/utils}/runPlaylistSaveMeta.ts (90%) rename src/{utils/playlist => features/playlist/utils}/runPlaylistZipDownload.ts (84%) rename src/{utils/playlist => features/playlist/utils}/runPlaylistsActions.ts (92%) rename src/{utils/playlist => features/playlist/utils}/runPlaylistsOpenSmartEditor.ts (91%) rename src/{utils/playlist => features/playlist/utils}/runPlaylistsSaveSmart.ts (90%) rename src/{utils/playlist => features/playlist/utils}/spotifyCsvImport.ts (100%) rename src/{utils/playlist => features/playlist/utils}/spotifyCsvMatch.ts (100%) rename src/{utils/playlist => features/playlist/utils}/startPlaylistRowDrag.ts (93%) diff --git a/src/app/AppRoutes.tsx b/src/app/AppRoutes.tsx index d22d07d1..5ca95a9f 100644 --- a/src/app/AppRoutes.tsx +++ b/src/app/AppRoutes.tsx @@ -24,8 +24,8 @@ const MostPlayed = lazy(() => import('../pages/MostPlayed')); const LosslessAlbums = lazy(() => import('@/features/album/pages/LosslessAlbums')); const RandomAlbums = lazy(() => import('@/features/album/pages/RandomAlbums')); const LuckyMixPage = lazy(() => import('../pages/LuckyMix')); -const Playlists = lazy(() => import('../pages/Playlists')); -const PlaylistDetail = lazy(() => import('../pages/PlaylistDetail')); +const Playlists = lazy(() => import('@/features/playlist/pages/Playlists')); +const PlaylistDetail = lazy(() => import('@/features/playlist/pages/PlaylistDetail')); const NowPlayingPage = lazy(() => import('@/features/nowPlaying/pages/NowPlaying')); const Settings = lazy(() => import('@/features/settings/pages/Settings')); const Statistics = lazy(() => import('@/features/stats/pages/Statistics')); diff --git a/src/components/QueuePanel.tsx b/src/components/QueuePanel.tsx index 86dbd562..f9b833af 100644 --- a/src/components/QueuePanel.tsx +++ b/src/components/QueuePanel.tsx @@ -1,5 +1,5 @@ import { Play } from 'lucide-react'; -import { updatePlaylist } from '../api/subsonicPlaylists'; +import { updatePlaylist } from '@/features/playlist'; import { resolvePlaylist, resolveMediaServerId } from '@/features/offline'; import { songToTrack } from '../utils/playback/songToTrack'; import type { Track } from '../store/playerStoreTypes'; @@ -8,7 +8,7 @@ import { usePlayerStore } from '../store/playerStore'; import { useOrbitStore } from '@/features/orbit'; import { OrbitGuestQueue, OrbitQueueHead } from '@/features/orbit'; import HostApprovalQueue from './HostApprovalQueue'; -import { usePlaylistStore } from '../store/playlistStore'; +import { usePlaylistStore } from '@/features/playlist'; import { useTranslation } from 'react-i18next'; import { usePlaybackLibraryNavigate } from '../hooks/usePlaybackLibraryNavigate'; import { useAuthStore } from '../store/authStore'; diff --git a/src/components/contextMenu/AddToPlaylistSubmenu.tsx b/src/components/contextMenu/AddToPlaylistSubmenu.tsx index 82cd41dd..a20cf97b 100644 --- a/src/components/contextMenu/AddToPlaylistSubmenu.tsx +++ b/src/components/contextMenu/AddToPlaylistSubmenu.tsx @@ -1,9 +1,9 @@ import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ListMusic, Plus } from 'lucide-react'; -import { getPlaylist, updatePlaylist } from '../../api/subsonicPlaylists'; +import { getPlaylist, updatePlaylist } from '@/features/playlist'; import type { SubsonicPlaylist } from '../../api/subsonicTypes'; -import { usePlaylistStore } from '../../store/playlistStore'; +import { usePlaylistStore } from '@/features/playlist'; import { showToast } from '../../utils/ui/toast'; import { confirmAddAllDuplicates, diff --git a/src/components/contextMenu/MoveToFolderSubmenu.tsx b/src/components/contextMenu/MoveToFolderSubmenu.tsx index aa9d8c38..bfaa2e66 100644 --- a/src/components/contextMenu/MoveToFolderSubmenu.tsx +++ b/src/components/contextMenu/MoveToFolderSubmenu.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 're import { useTranslation } from 'react-i18next'; import { Check, Folder, FolderMinus, Plus } from 'lucide-react'; import { useAuthStore } from '../../store/authStore'; -import { EMPTY_SERVER_FOLDERS, usePlaylistFolderStore } from '../../store/playlistFolderStore'; +import { EMPTY_SERVER_FOLDERS, usePlaylistFolderStore } from '@/features/playlist'; interface Props { playlistId: string; diff --git a/src/components/contextMenu/MultiAlbumToPlaylistSubmenu.tsx b/src/components/contextMenu/MultiAlbumToPlaylistSubmenu.tsx index a21dd7c4..223d8b0b 100644 --- a/src/components/contextMenu/MultiAlbumToPlaylistSubmenu.tsx +++ b/src/components/contextMenu/MultiAlbumToPlaylistSubmenu.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'; import { ListMusic, Plus } from 'lucide-react'; import { resolveAlbum, resolveMediaServerId, resolvePlaylist } from '@/features/offline'; import type { SubsonicPlaylist } from '../../api/subsonicTypes'; -import { usePlaylistStore } from '../../store/playlistStore'; +import { usePlaylistStore } from '@/features/playlist'; import { showToast } from '../../utils/ui/toast'; import { confirmAddAllDuplicates, @@ -39,7 +39,7 @@ export function MultiAlbumToPlaylistSubmenu({ albumIds, onDone, triggerId: _trig }, [albumIds]); const handleAddWithToast = async (pl: SubsonicPlaylist, songIds: string[]) => { - const { updatePlaylist } = await import('../../api/subsonicPlaylists'); + const { updatePlaylist } = await import('@/features/playlist'); const touchPlaylist = usePlaylistStore.getState().touchPlaylist; try { @@ -125,7 +125,7 @@ export function MultiAlbumToPlaylistSubmenu({ albumIds, onDone, triggerId: _trig const handleCreate = async () => { const name = newName.trim() || t('playlists.unnamed'); try { - const { createPlaylist } = await import('../../api/subsonicPlaylists'); + const { createPlaylist } = await import('@/features/playlist'); const pl = await createPlaylist(name, songIds); if (pl?.id) { usePlaylistStore.getState().touchPlaylist(pl.id); diff --git a/src/components/contextMenu/MultiArtistToPlaylistSubmenu.tsx b/src/components/contextMenu/MultiArtistToPlaylistSubmenu.tsx index 1bd7a5f5..a578ccb1 100644 --- a/src/components/contextMenu/MultiArtistToPlaylistSubmenu.tsx +++ b/src/components/contextMenu/MultiArtistToPlaylistSubmenu.tsx @@ -2,9 +2,9 @@ import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ListMusic, Plus } from 'lucide-react'; import { resolveAlbum, resolveArtist, resolveMediaServerId, resolvePlaylist } from '@/features/offline'; -import { getPlaylists } from '../../api/subsonicPlaylists'; +import { getPlaylists } from '@/features/playlist'; import type { SubsonicPlaylist } from '../../api/subsonicTypes'; -import { usePlaylistStore } from '../../store/playlistStore'; +import { usePlaylistStore } from '@/features/playlist'; import { showToast } from '../../utils/ui/toast'; import { confirmAddAllDuplicates, @@ -53,7 +53,7 @@ export function MultiArtistToPlaylistSubmenu({ artistIds, onDone, triggerId: _tr }, [artistIds]); const handleAddWithToast = async (pl: SubsonicPlaylist, songIds: string[]) => { - const { updatePlaylist } = await import('../../api/subsonicPlaylists'); + const { updatePlaylist } = await import('@/features/playlist'); const touchPlaylist = usePlaylistStore.getState().touchPlaylist; try { @@ -139,7 +139,7 @@ export function MultiArtistToPlaylistSubmenu({ artistIds, onDone, triggerId: _tr const handleCreate = async () => { const name = newName.trim() || t('playlists.unnamed'); try { - const { createPlaylist } = await import('../../api/subsonicPlaylists'); + const { createPlaylist } = await import('@/features/playlist'); const pl = await createPlaylist(name, songIds); if (pl?.id) { usePlaylistStore.getState().touchPlaylist(pl.id); diff --git a/src/components/contextMenu/PlaylistContextItems.tsx b/src/components/contextMenu/PlaylistContextItems.tsx index fa233200..78313fd4 100644 --- a/src/components/contextMenu/PlaylistContextItems.tsx +++ b/src/components/contextMenu/PlaylistContextItems.tsx @@ -1,7 +1,7 @@ import { useTranslation } from 'react-i18next'; import { Play, ChevronRight, FolderTree, ListMusic, Trash2 } from 'lucide-react'; import type { SubsonicPlaylist } from '../../api/subsonicTypes'; -import { usePlaylistStore } from '../../store/playlistStore'; +import { usePlaylistStore } from '@/features/playlist'; import { MultiPlaylistToPlaylistSubmenu, SinglePlaylistToPlaylistSubmenu } from './PlaylistToPlaylistSubmenus'; import MoveToFolderSubmenu from './MoveToFolderSubmenu'; import type { ContextMenuItemsProps } from './contextMenuItemTypes'; @@ -23,7 +23,7 @@ export default function PlaylistContextItems(props: ContextMenuItemsProps) { return ( <>
handleAction(async () => { - const { playPlaylistById } = await import('../../utils/playlist/playPlaylistById'); + const { playPlaylistById } = await import('@/features/playlist'); try { await playPlaylistById(playlist.id); } catch { @@ -65,7 +65,7 @@ export default function PlaylistContextItems(props: ContextMenuItemsProps) {
handleAction(async () => { const { showToast } = await import('../../utils/ui/toast'); - const { deletePlaylist } = await import('../../api/subsonicPlaylists'); + const { deletePlaylist } = await import('@/features/playlist'); const { removeId } = usePlaylistStore.getState(); try { await deletePlaylist(playlist.id); @@ -113,7 +113,7 @@ export default function PlaylistContextItems(props: ContextMenuItemsProps) { {offlinePolicy.canEditPlaylist && (
handleAction(async () => { const { showToast } = await import('../../utils/ui/toast'); - const { deletePlaylist } = await import('../../api/subsonicPlaylists'); + const { deletePlaylist } = await import('@/features/playlist'); const { removeId } = usePlaylistStore.getState(); const deletedIds: string[] = []; for (const pl of selectedPlaylists) { diff --git a/src/components/contextMenu/PlaylistToPlaylistSubmenus.tsx b/src/components/contextMenu/PlaylistToPlaylistSubmenus.tsx index cc514fca..4dadfb3a 100644 --- a/src/components/contextMenu/PlaylistToPlaylistSubmenus.tsx +++ b/src/components/contextMenu/PlaylistToPlaylistSubmenus.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ListMusic, Plus } from 'lucide-react'; -import { usePlaylistStore } from '../../store/playlistStore'; +import { usePlaylistStore } from '@/features/playlist'; import { showToast } from '../../utils/ui/toast'; import { isSmartPlaylistName } from '../../utils/componentHelpers/contextMenuHelpers'; @@ -41,7 +41,7 @@ export function SinglePlaylistToPlaylistSubmenu({ playlist, onDone, triggerId }: const handleCreate = async () => { if (!newName.trim()) return; - const { createPlaylist } = await import('../../api/subsonicPlaylists'); + const { createPlaylist } = await import('@/features/playlist'); try { const newPl = await createPlaylist(newName.trim(), []); if (newPl?.id) { @@ -55,7 +55,7 @@ export function SinglePlaylistToPlaylistSubmenu({ playlist, onDone, triggerId }: }; const handleAddToNewPlaylist = async (targetId: string, targetName: string) => { - const { getPlaylist, updatePlaylist } = await import('../../api/subsonicPlaylists'); + const { getPlaylist, updatePlaylist } = await import('@/features/playlist'); const touchPlaylist = usePlaylistStore.getState().touchPlaylist; try { @@ -73,7 +73,7 @@ export function SinglePlaylistToPlaylistSubmenu({ playlist, onDone, triggerId }: }; const handleAdd = async (targetId: string, targetName: string) => { - const { getPlaylist, updatePlaylist } = await import('../../api/subsonicPlaylists'); + const { getPlaylist, updatePlaylist } = await import('@/features/playlist'); const touchPlaylist = usePlaylistStore.getState().touchPlaylist; try { @@ -180,7 +180,7 @@ export function MultiPlaylistToPlaylistSubmenu({ playlists, onDone, triggerId }: const handleCreate = async () => { if (!newName.trim()) return; - const { createPlaylist } = await import('../../api/subsonicPlaylists'); + const { createPlaylist } = await import('@/features/playlist'); try { const newPl = await createPlaylist(newName.trim(), []); if (newPl?.id) { @@ -194,7 +194,7 @@ export function MultiPlaylistToPlaylistSubmenu({ playlists, onDone, triggerId }: }; const handleMergeToNewPlaylist = async (targetId: string, targetName: string) => { - const { getPlaylist, updatePlaylist } = await import('../../api/subsonicPlaylists'); + const { getPlaylist, updatePlaylist } = await import('@/features/playlist'); const touchPlaylist = usePlaylistStore.getState().touchPlaylist; try { @@ -223,7 +223,7 @@ export function MultiPlaylistToPlaylistSubmenu({ playlists, onDone, triggerId }: }; const handleMerge = async (targetId: string, targetName: string) => { - const { getPlaylist, updatePlaylist } = await import('../../api/subsonicPlaylists'); + const { getPlaylist, updatePlaylist } = await import('@/features/playlist'); const touchPlaylist = usePlaylistStore.getState().touchPlaylist; try { diff --git a/src/components/contextMenu/SongContextItems.tsx b/src/components/contextMenu/SongContextItems.tsx index aa0433d3..c6f40c58 100644 --- a/src/components/contextMenu/SongContextItems.tsx +++ b/src/components/contextMenu/SongContextItems.tsx @@ -7,7 +7,7 @@ import { queueSongStar } from '../../store/pendingStarSync'; import { getMusicNetworkRuntime, useEnrichmentPrimary } from '../../music-network'; import type { Track } from '../../store/playerStoreTypes'; import { useAuthStore } from '../../store/authStore'; -import { usePlaylistStore } from '../../store/playlistStore'; +import { usePlaylistStore } from '@/features/playlist'; import { songToTrack } from '../../utils/playback/songToTrack'; import { showToast } from '../../utils/ui/toast'; import { suggestOrbitTrack, hostEnqueueToOrbit, evaluateOrbitSuggestGate, OrbitSuggestBlockedError } from '@/features/orbit'; @@ -178,7 +178,7 @@ export default function SongContextItems(props: ContextMenuItemsProps) {
{offlinePolicy.canEditPlaylist && playlistId && playlistSongIndex !== undefined && (
handleAction(async () => { - const { updatePlaylist } = await import('../../api/subsonicPlaylists'); + const { updatePlaylist } = await import('@/features/playlist'); const { showToast } = await import('../../utils/ui/toast'); const touchPlaylist = usePlaylistStore.getState().touchPlaylist; try { diff --git a/src/components/queuePanel/LoadPlaylistModal.tsx b/src/components/queuePanel/LoadPlaylistModal.tsx index 3deb445b..bf4c61fb 100644 --- a/src/components/queuePanel/LoadPlaylistModal.tsx +++ b/src/components/queuePanel/LoadPlaylistModal.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import { Play, X, Trash2, ListPlus } from 'lucide-react'; import { useTranslation } from 'react-i18next'; -import { getPlaylists, deletePlaylist } from '../../api/subsonicPlaylists'; +import { getPlaylists, deletePlaylist } from '@/features/playlist'; import type { SubsonicPlaylist } from '../../api/subsonicTypes'; interface Props { diff --git a/src/features/artist/utils/runArtistDetailActions.ts b/src/features/artist/utils/runArtistDetailActions.ts index d75f142a..bf3948c3 100644 --- a/src/features/artist/utils/runArtistDetailActions.ts +++ b/src/features/artist/utils/runArtistDetailActions.ts @@ -1,6 +1,6 @@ import type React from 'react'; import type { TFunction } from 'i18next'; -import { uploadArtistImage } from '@/api/subsonicPlaylists'; +import { uploadArtistImage } from '@/features/playlist'; import { setRating, star, unstar } from '@/api/subsonicStarRating'; import type { SubsonicArtist } from '@/api/subsonicTypes'; import { useAuthStore } from '@/store/authStore'; diff --git a/src/features/deviceSync/hooks/useDeviceSyncBrowser.ts b/src/features/deviceSync/hooks/useDeviceSyncBrowser.ts index b55ba5ff..22774741 100644 --- a/src/features/deviceSync/hooks/useDeviceSyncBrowser.ts +++ b/src/features/deviceSync/hooks/useDeviceSyncBrowser.ts @@ -1,5 +1,5 @@ import { useCallback, useEffect, useState } from 'react'; -import { getPlaylists } from '@/api/subsonicPlaylists'; +import { getPlaylists } from '@/features/playlist'; import { getArtists, getArtist } from '@/features/artist'; import { getAlbumList } from '@/api/subsonicLibrary'; import { search as searchSubsonic } from '@/api/subsonicSearch'; diff --git a/src/features/offline/utils/offlineMediaResolve.test.ts b/src/features/offline/utils/offlineMediaResolve.test.ts index ea088c08..0f6ce5ca 100644 --- a/src/features/offline/utils/offlineMediaResolve.test.ts +++ b/src/features/offline/utils/offlineMediaResolve.test.ts @@ -57,7 +57,7 @@ vi.mock('@/features/artist', () => ({ getArtistForServer: (serverId: string, artistId: string) => getArtistForServerMock(serverId, artistId), })); -vi.mock('@/api/subsonicPlaylists', () => ({ +vi.mock('@/features/playlist', () => ({ getPlaylistForServer: (serverId: string, playlistId: string) => getPlaylistForServerMock(serverId, playlistId), })); diff --git a/src/features/offline/utils/offlineMediaResolve.ts b/src/features/offline/utils/offlineMediaResolve.ts index d121273c..a2cdebf7 100644 --- a/src/features/offline/utils/offlineMediaResolve.ts +++ b/src/features/offline/utils/offlineMediaResolve.ts @@ -1,6 +1,6 @@ import { getAlbumForServer } from '@/api/subsonicLibrary'; import { getArtistForServer } from '@/features/artist'; -import { getPlaylistForServer } from '@/api/subsonicPlaylists'; +import { getPlaylistForServer } from '@/features/playlist'; import type { SubsonicAlbum, SubsonicArtist, diff --git a/src/features/offline/utils/pinnedOfflineSync.test.ts b/src/features/offline/utils/pinnedOfflineSync.test.ts index 70911276..e09bf89b 100644 --- a/src/features/offline/utils/pinnedOfflineSync.test.ts +++ b/src/features/offline/utils/pinnedOfflineSync.test.ts @@ -29,7 +29,7 @@ vi.mock('@/utils/network/activeServerReachability', () => ({ onActiveServerBecameReachable: () => () => {}, })); -vi.mock('@/api/subsonicPlaylists', () => ({ +vi.mock('@/features/playlist', () => ({ getPlaylistForServer: (serverId: string, id: string) => getPlaylistMock(serverId, id), })); diff --git a/src/features/offline/utils/pinnedOfflineSync.ts b/src/features/offline/utils/pinnedOfflineSync.ts index 6bccd984..69758e40 100644 --- a/src/features/offline/utils/pinnedOfflineSync.ts +++ b/src/features/offline/utils/pinnedOfflineSync.ts @@ -1,6 +1,6 @@ import { libraryGetTracksByAlbum, subscribeLibrarySyncIdle } from '@/api/library'; import { getAlbumForServer, filterSongsToServerLibrary } from '@/api/subsonicLibrary'; -import { getPlaylistForServer } from '@/api/subsonicPlaylists'; +import { getPlaylistForServer } from '@/features/playlist'; import { getArtistForServer } from '@/features/artist'; import type { SubsonicSong } from '@/api/subsonicTypes'; import { invoke } from '@tauri-apps/api/core'; @@ -8,7 +8,7 @@ import { useAuthStore } from '@/store/authStore'; import type { PinSource } from '@/store/localPlaybackStore'; import { useLocalPlaybackStore } from '@/store/localPlaybackStore'; import { useOfflineStore } from '@/features/offline/store/offlineStore'; -import { usePlaylistStore } from '@/store/playlistStore'; +import { usePlaylistStore } from '@/features/playlist'; import { isSmartPlaylistName } from '@/utils/componentHelpers/playlistDetailHelpers'; import { getMediaDir } from '@/utils/media/mediaDir'; import { diff --git a/src/features/offline/utils/resumeIncompleteOfflinePins.ts b/src/features/offline/utils/resumeIncompleteOfflinePins.ts index 69b6b80f..e7e8155a 100644 --- a/src/features/offline/utils/resumeIncompleteOfflinePins.ts +++ b/src/features/offline/utils/resumeIncompleteOfflinePins.ts @@ -1,5 +1,5 @@ import { libraryGetTracksBatchChunked } from '@/api/library'; -import { getPlaylist } from '@/api/subsonicPlaylists'; +import { getPlaylist } from '@/features/playlist'; import type { SubsonicSong } from '@/api/subsonicTypes'; import { useAuthStore } from '@/store/authStore'; import type { OfflineAlbumMeta } from '@/features/offline/store/offlineStore'; diff --git a/src/features/orbit/utils/cleanup.test.ts b/src/features/orbit/utils/cleanup.test.ts index 0f26d778..328d6d6d 100644 --- a/src/features/orbit/utils/cleanup.test.ts +++ b/src/features/orbit/utils/cleanup.test.ts @@ -19,7 +19,7 @@ const { authState, orbitState } = vi.hoisted(() => ({ orbitState: { sessionId: null as string | null }, })); -vi.mock('@/api/subsonicPlaylists', () => ({ getPlaylists, deletePlaylist })); +vi.mock('@/features/playlist', () => ({ getPlaylists, deletePlaylist })); vi.mock('@/store/authStore', () => ({ useAuthStore: { getState: () => ({ diff --git a/src/features/orbit/utils/cleanup.ts b/src/features/orbit/utils/cleanup.ts index 6cdb6f64..12a3ef19 100644 --- a/src/features/orbit/utils/cleanup.ts +++ b/src/features/orbit/utils/cleanup.ts @@ -1,4 +1,4 @@ -import { deletePlaylist, getPlaylists } from '@/api/subsonicPlaylists'; +import { deletePlaylist, getPlaylists } from '@/features/playlist'; import { useAuthStore } from '@/store/authStore'; import { useOrbitStore } from '@/features/orbit/store/orbitStore'; import { ORBIT_PLAYLIST_PREFIX, parseOrbitState } from '@/features/orbit/api/orbit'; diff --git a/src/features/orbit/utils/guest.ts b/src/features/orbit/utils/guest.ts index 0f5e41c7..b2acc6c3 100644 --- a/src/features/orbit/utils/guest.ts +++ b/src/features/orbit/utils/guest.ts @@ -1,4 +1,4 @@ -import { createPlaylist, deletePlaylist, getPlaylist, getPlaylists, updatePlaylist } from '@/api/subsonicPlaylists'; +import { createPlaylist, deletePlaylist, getPlaylist, getPlaylists, updatePlaylist } from '@/features/playlist'; import { getSong } from '@/api/subsonicLibrary'; import { songToTrack } from '@/utils/playback/songToTrack'; import { useAuthStore } from '@/store/authStore'; diff --git a/src/features/orbit/utils/host.test.ts b/src/features/orbit/utils/host.test.ts index 54f46d65..7441e819 100644 --- a/src/features/orbit/utils/host.test.ts +++ b/src/features/orbit/utils/host.test.ts @@ -24,7 +24,7 @@ vi.mock('@/features/orbit/utils/remote', () => ({ vi.mock('@/features/orbit/store/orbitStore', () => ({ useOrbitStore: { getState: () => orbitStore } })); vi.mock('@/store/authStore', () => ({ useAuthStore: { getState: () => ({}) } })); vi.mock('@/store/playerStore', () => ({ usePlayerStore: { getState: () => ({ enqueue: vi.fn() }) } })); -vi.mock('@/api/subsonicPlaylists', () => ({ createPlaylist: vi.fn(), deletePlaylist: vi.fn() })); +vi.mock('@/features/playlist', () => ({ createPlaylist: vi.fn(), deletePlaylist: vi.fn() })); vi.mock('@/api/subsonicLibrary', () => ({ getSong: vi.fn() })); vi.mock('@/utils/playback/songToTrack', () => ({ songToTrack: vi.fn() })); diff --git a/src/features/orbit/utils/host.ts b/src/features/orbit/utils/host.ts index 4937967f..cc8d3d0e 100644 --- a/src/features/orbit/utils/host.ts +++ b/src/features/orbit/utils/host.ts @@ -1,4 +1,4 @@ -import { createPlaylist, deletePlaylist } from '@/api/subsonicPlaylists'; +import { createPlaylist, deletePlaylist } from '@/features/playlist'; import { getSong } from '@/api/subsonicLibrary'; import { songToTrack } from '@/utils/playback/songToTrack'; import { useAuthStore } from '@/store/authStore'; diff --git a/src/features/orbit/utils/moderation.ts b/src/features/orbit/utils/moderation.ts index 464706ce..36f58707 100644 --- a/src/features/orbit/utils/moderation.ts +++ b/src/features/orbit/utils/moderation.ts @@ -1,4 +1,4 @@ -import { deletePlaylist, getPlaylists } from '@/api/subsonicPlaylists'; +import { deletePlaylist, getPlaylists } from '@/features/playlist'; import { useOrbitStore } from '@/features/orbit/store/orbitStore'; import { orbitOutboxPlaylistName, type OrbitState } from '@/features/orbit/api/orbit'; import { writeOrbitState } from '@/features/orbit/utils/remote'; diff --git a/src/features/orbit/utils/remote.ts b/src/features/orbit/utils/remote.ts index bcce728d..448af079 100644 --- a/src/features/orbit/utils/remote.ts +++ b/src/features/orbit/utils/remote.ts @@ -1,4 +1,4 @@ -import { getPlaylist, getPlaylists, updatePlaylistMeta } from '@/api/subsonicPlaylists'; +import { getPlaylist, getPlaylists, updatePlaylistMeta } from '@/features/playlist'; import { orbitSessionPlaylistName, parseOrbitState, diff --git a/src/features/orbit/utils/sweep.ts b/src/features/orbit/utils/sweep.ts index ccbf599f..369fb1ab 100644 --- a/src/features/orbit/utils/sweep.ts +++ b/src/features/orbit/utils/sweep.ts @@ -1,4 +1,4 @@ -import { getPlaylist, getPlaylists, updatePlaylist } from '@/api/subsonicPlaylists'; +import { getPlaylist, getPlaylists, updatePlaylist } from '@/features/playlist'; import { type OrbitOutboxMeta } from '@/features/orbit/api/orbit'; import { parseOutboxPlaylistName } from '@/features/orbit/utils/helpers'; import { type OutboxSnapshot } from '@/features/orbit/utils/stateMath'; diff --git a/src/api/subsonicPlaylists.ts b/src/features/playlist/api/subsonicPlaylists.ts similarity index 93% rename from src/api/subsonicPlaylists.ts rename to src/features/playlist/api/subsonicPlaylists.ts index b1ea411d..80d55015 100644 --- a/src/api/subsonicPlaylists.ts +++ b/src/features/playlist/api/subsonicPlaylists.ts @@ -1,8 +1,8 @@ import { invoke } from '@tauri-apps/api/core'; -import { useAuthStore } from '../store/authStore'; -import { shouldAttemptSubsonicForServer } from '../utils/network/subsonicNetworkGuard'; -import { api, apiForServer } from './subsonicClient'; -import type { SubsonicPlaylist, SubsonicSong } from './subsonicTypes'; +import { useAuthStore } from '@/store/authStore'; +import { shouldAttemptSubsonicForServer } from '@/utils/network/subsonicNetworkGuard'; +import { api, apiForServer } from '@/api/subsonicClient'; +import type { SubsonicPlaylist, SubsonicSong } from '@/api/subsonicTypes'; export async function getPlaylists(includeOrbit = false): Promise { const data = await api<{ playlists: { playlist: SubsonicPlaylist[] } }>('getPlaylists.view', { _t: Date.now() }); diff --git a/src/components/playlist/CsvImportReportModal.tsx b/src/features/playlist/components/CsvImportReportModal.tsx similarity index 98% rename from src/components/playlist/CsvImportReportModal.tsx rename to src/features/playlist/components/CsvImportReportModal.tsx index 36c77617..11e32776 100644 --- a/src/components/playlist/CsvImportReportModal.tsx +++ b/src/features/playlist/components/CsvImportReportModal.tsx @@ -2,8 +2,8 @@ import React from 'react'; import { createPortal } from 'react-dom'; import { useTranslation } from 'react-i18next'; import { Download, X } from 'lucide-react'; -import { showToast } from '../../utils/ui/toast'; -import type { SpotifyCsvTrack } from '../../utils/playlist/spotifyCsvImport'; +import { showToast } from '@/utils/ui/toast'; +import type { SpotifyCsvTrack } from '@/features/playlist/utils/spotifyCsvImport'; interface CsvReportModalProps { report: { diff --git a/src/components/playlist/PlaylistArtistCell.test.tsx b/src/features/playlist/components/PlaylistArtistCell.test.tsx similarity index 86% rename from src/components/playlist/PlaylistArtistCell.test.tsx rename to src/features/playlist/components/PlaylistArtistCell.test.tsx index 65fa1fe2..6e2832dc 100644 --- a/src/components/playlist/PlaylistArtistCell.test.tsx +++ b/src/features/playlist/components/PlaylistArtistCell.test.tsx @@ -1,8 +1,8 @@ import { describe, expect, it } from 'vitest'; import { screen } from '@testing-library/react'; -import { renderWithProviders } from '../../test/helpers/renderWithProviders'; -import { PlaylistArtistCell } from './PlaylistArtistCell'; -import type { SubsonicSong } from '../../api/subsonicTypes'; +import { renderWithProviders } from '@/test/helpers/renderWithProviders'; +import { PlaylistArtistCell } from '@/features/playlist/components/PlaylistArtistCell'; +import type { SubsonicSong } from '@/api/subsonicTypes'; function song(overrides: Partial): SubsonicSong { return { diff --git a/src/components/playlist/PlaylistArtistCell.tsx b/src/features/playlist/components/PlaylistArtistCell.tsx similarity index 89% rename from src/components/playlist/PlaylistArtistCell.tsx rename to src/features/playlist/components/PlaylistArtistCell.tsx index 32511f92..c6f529f7 100644 --- a/src/components/playlist/PlaylistArtistCell.tsx +++ b/src/features/playlist/components/PlaylistArtistCell.tsx @@ -1,7 +1,7 @@ import React, { useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; -import type { SubsonicSong } from '../../api/subsonicTypes'; -import { resolveTrackArtistRefs } from '../../utils/playback/trackArtistRefs'; +import type { SubsonicSong } from '@/api/subsonicTypes'; +import { resolveTrackArtistRefs } from '@/utils/playback/trackArtistRefs'; /** * Multi-artist credit for playlist track rows (main list + suggestions). diff --git a/src/components/playlists/PlaylistCard.tsx b/src/features/playlist/components/PlaylistCard.tsx similarity index 94% rename from src/components/playlists/PlaylistCard.tsx rename to src/features/playlist/components/PlaylistCard.tsx index cb3b9f0f..0202c19d 100644 --- a/src/components/playlists/PlaylistCard.tsx +++ b/src/features/playlist/components/PlaylistCard.tsx @@ -2,14 +2,14 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { Check, Clock3, ListMusic, Pencil, Play, Sparkles, Trash2 } from 'lucide-react'; -import type { SubsonicPlaylist } from '../../api/subsonicTypes'; -import { usePlayerStore } from '../../store/playerStore'; +import type { SubsonicPlaylist } from '@/api/subsonicTypes'; +import { usePlayerStore } from '@/store/playerStore'; import { displayPlaylistName, isSmartPlaylistName, type PendingSmartPlaylist, -} from '../../utils/playlist/playlistsSmart'; -import { formatHumanHoursMinutes } from '../../utils/format/formatHumanDuration'; -import { useDragSource } from '../../contexts/DragDropContext'; -import { PlaylistCardMainCover, PlaylistSmartCoverCell } from './PlaylistCoverImages'; +} from '@/features/playlist/utils/playlistsSmart'; +import { formatHumanHoursMinutes } from '@/utils/format/formatHumanDuration'; +import { useDragSource } from '@/contexts/DragDropContext'; +import { PlaylistCardMainCover, PlaylistSmartCoverCell } from '@/features/playlist/components/PlaylistCoverImages'; interface Props { pl: SubsonicPlaylist; diff --git a/src/components/playlists/PlaylistCoverImages.tsx b/src/features/playlist/components/PlaylistCoverImages.tsx similarity index 92% rename from src/components/playlists/PlaylistCoverImages.tsx rename to src/features/playlist/components/PlaylistCoverImages.tsx index 7a7c59d8..5bc8050e 100644 --- a/src/components/playlists/PlaylistCoverImages.tsx +++ b/src/features/playlist/components/PlaylistCoverImages.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { AlbumCoverArtImage } from '../../cover/AlbumCoverArtImage'; +import { AlbumCoverArtImage } from '@/cover/AlbumCoverArtImage'; /** 2×2 collage cell — half of clamp(120px, 15vw, 200px) playlist hero grid. */ const PLAYLIST_QUAD_CELL_CSS_PX = 100; diff --git a/src/components/playlist/PlaylistEditModal.tsx b/src/features/playlist/components/PlaylistEditModal.tsx similarity index 94% rename from src/components/playlist/PlaylistEditModal.tsx rename to src/features/playlist/components/PlaylistEditModal.tsx index aa68ec61..3494bc85 100644 --- a/src/components/playlist/PlaylistEditModal.tsx +++ b/src/features/playlist/components/PlaylistEditModal.tsx @@ -1,11 +1,11 @@ import React, { useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Camera, Loader2, X } from 'lucide-react'; -import type { SubsonicPlaylist } from '../../api/subsonicTypes'; -import type { CoverArtId } from '../../cover/types'; -import { AlbumCoverArtImage } from '../../cover/AlbumCoverArtImage'; -import { PLAYLIST_MAIN_COVER_CSS_PX } from '../../hooks/usePlaylistCovers'; -import { PlaylistSmartCoverCell } from '../playlists/PlaylistCoverImages'; +import type { SubsonicPlaylist } from '@/api/subsonicTypes'; +import type { CoverArtId } from '@/cover/types'; +import { AlbumCoverArtImage } from '@/cover/AlbumCoverArtImage'; +import { PLAYLIST_MAIN_COVER_CSS_PX } from '@/features/playlist/hooks/usePlaylistCovers'; +import { PlaylistSmartCoverCell } from '@/features/playlist/components/PlaylistCoverImages'; interface EditModalProps { playlist: SubsonicPlaylist; diff --git a/src/components/playlist/PlaylistFilterToolbar.tsx b/src/features/playlist/components/PlaylistFilterToolbar.tsx similarity index 96% rename from src/components/playlist/PlaylistFilterToolbar.tsx rename to src/features/playlist/components/PlaylistFilterToolbar.tsx index aaa399b2..046dc675 100644 --- a/src/components/playlist/PlaylistFilterToolbar.tsx +++ b/src/features/playlist/components/PlaylistFilterToolbar.tsx @@ -1,8 +1,8 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { Search, X } from 'lucide-react'; -import SortDropdown, { type SortOption } from '../SortDropdown'; -import type { PlaylistSortKey, PlaylistSortDir } from '../../utils/playlist/playlistDisplayedSongs'; +import SortDropdown, { type SortOption } from '@/components/SortDropdown'; +import type { PlaylistSortKey, PlaylistSortDir } from '@/features/playlist/utils/playlistDisplayedSongs'; type PlaylistSortValue = | 'added-newest' diff --git a/src/components/playlists/PlaylistFolderSection.test.tsx b/src/features/playlist/components/PlaylistFolderSection.test.tsx similarity index 87% rename from src/components/playlists/PlaylistFolderSection.test.tsx rename to src/features/playlist/components/PlaylistFolderSection.test.tsx index 23c50c79..76dd1e92 100644 --- a/src/components/playlists/PlaylistFolderSection.test.tsx +++ b/src/features/playlist/components/PlaylistFolderSection.test.tsx @@ -1,8 +1,8 @@ import { beforeEach, describe, expect, it } from 'vitest'; import { renderWithProviders } from '@/test/helpers/renderWithProviders'; -import PlaylistFolderSection from './PlaylistFolderSection'; -import { usePlaylistFolderStore } from '@/store/playlistFolderStore'; -import type { PlaylistFolder } from '@/utils/playlist/playlistFolders'; +import PlaylistFolderSection from '@/features/playlist/components/PlaylistFolderSection'; +import { usePlaylistFolderStore } from '@/features/playlist/store/playlistFolderStore'; +import type { PlaylistFolder } from '@/features/playlist/utils/playlistFolders'; const store = () => usePlaylistFolderStore.getState(); const assignments = (serverId: string) => store().byServer[serverId]?.assignments ?? {}; diff --git a/src/components/playlists/PlaylistFolderSection.tsx b/src/features/playlist/components/PlaylistFolderSection.tsx similarity index 94% rename from src/components/playlists/PlaylistFolderSection.tsx rename to src/features/playlist/components/PlaylistFolderSection.tsx index a13771b9..387e336b 100644 --- a/src/components/playlists/PlaylistFolderSection.tsx +++ b/src/features/playlist/components/PlaylistFolderSection.tsx @@ -1,11 +1,11 @@ import React, { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ChevronRight, Folder, Pencil, Trash2 } from 'lucide-react'; -import type { SubsonicPlaylist } from '../../api/subsonicTypes'; -import { usePlaylistFolderStore } from '../../store/playlistFolderStore'; -import type { PlaylistFolder } from '../../utils/playlist/playlistFolders'; -import { useDragDrop } from '../../contexts/DragDropContext'; -import { VirtualCardGrid } from '../VirtualCardGrid'; +import type { SubsonicPlaylist } from '@/api/subsonicTypes'; +import { usePlaylistFolderStore } from '@/features/playlist/store/playlistFolderStore'; +import type { PlaylistFolder } from '@/features/playlist/utils/playlistFolders'; +import { useDragDrop } from '@/contexts/DragDropContext'; +import { VirtualCardGrid } from '@/components/VirtualCardGrid'; interface Props { serverId: string; diff --git a/src/components/playlist/PlaylistHero.tsx b/src/features/playlist/components/PlaylistHero.tsx similarity index 95% rename from src/components/playlist/PlaylistHero.tsx rename to src/features/playlist/components/PlaylistHero.tsx index 5facfd04..07d47858 100644 --- a/src/components/playlist/PlaylistHero.tsx +++ b/src/features/playlist/components/PlaylistHero.tsx @@ -5,19 +5,19 @@ import { Camera, ChevronLeft, Download, FileUp, Globe, HardDriveDownload, ListPlus, Loader2, Lock, Pencil, Play, Search, Shuffle, Sparkles, Trash2, } from 'lucide-react'; -import type { SubsonicPlaylist, SubsonicSong } from '../../api/subsonicTypes'; +import type { SubsonicPlaylist, SubsonicSong } from '@/api/subsonicTypes'; import type { ZipDownload } from '@/features/offline'; import type { AlbumOfflineStatus } from '@/features/album'; import { dequeueOfflinePin } from '@/features/offline'; -import { useThemeStore } from '../../store/themeStore'; -import { usePlaylistLayoutStore, type PlaylistLayoutItemId } from '../../store/playlistLayoutStore'; +import { useThemeStore } from '@/store/themeStore'; +import { usePlaylistLayoutStore, type PlaylistLayoutItemId } from '@/features/playlist/store/playlistLayoutStore'; import { displayPlaylistName, formatSize, isSmartPlaylistName, totalDurationLabel, -} from '../../utils/componentHelpers/playlistDetailHelpers'; -import type { CoverArtId } from '../../cover/types'; -import { AlbumCoverArtImage } from '../../cover/AlbumCoverArtImage'; -import { PLAYLIST_MAIN_COVER_CSS_PX } from '../../hooks/usePlaylistCovers'; -import { PlaylistSmartCoverCell } from '../playlists/PlaylistCoverImages'; +} from '@/utils/componentHelpers/playlistDetailHelpers'; +import type { CoverArtId } from '@/cover/types'; +import { AlbumCoverArtImage } from '@/cover/AlbumCoverArtImage'; +import { PLAYLIST_MAIN_COVER_CSS_PX } from '@/features/playlist/hooks/usePlaylistCovers'; +import { PlaylistSmartCoverCell } from '@/features/playlist/components/PlaylistCoverImages'; import type { OfflineActionPolicy } from '@/features/offline'; interface Props { diff --git a/src/components/playlist/PlaylistRow.tsx b/src/features/playlist/components/PlaylistRow.tsx similarity index 93% rename from src/components/playlist/PlaylistRow.tsx rename to src/features/playlist/components/PlaylistRow.tsx index 7b5270da..e9542aca 100644 --- a/src/components/playlist/PlaylistRow.tsx +++ b/src/features/playlist/components/PlaylistRow.tsx @@ -1,14 +1,14 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { AudioLines, ChevronRight, Heart, Play, Square, Trash2 } from 'lucide-react'; -import type { ColDef } from '../../utils/useTracklistColumns'; -import type { SubsonicSong } from '../../api/subsonicTypes'; -import { codecLabel } from '../../utils/componentHelpers/playlistDetailHelpers'; -import { formatLastSeen } from '../../utils/componentHelpers/userMgmtHelpers'; -import i18n from '../../i18n'; -import { formatTrackTime } from '../../utils/format/formatDuration'; -import StarRating from '../StarRating'; -import { PlaylistArtistCell } from './PlaylistArtistCell'; +import type { ColDef } from '@/utils/useTracklistColumns'; +import type { SubsonicSong } from '@/api/subsonicTypes'; +import { codecLabel } from '@/utils/componentHelpers/playlistDetailHelpers'; +import { formatLastSeen } from '@/utils/componentHelpers/userMgmtHelpers'; +import i18n from '@/i18n'; +import { formatTrackTime } from '@/utils/format/formatDuration'; +import StarRating from '@/components/StarRating'; +import { PlaylistArtistCell } from '@/features/playlist/components/PlaylistArtistCell'; export interface PlaylistRowCallbacks { activate: (song: SubsonicSong, index: number, e: React.MouseEvent) => void; diff --git a/src/components/playlist/PlaylistSongSearchPanel.tsx b/src/features/playlist/components/PlaylistSongSearchPanel.tsx similarity index 92% rename from src/components/playlist/PlaylistSongSearchPanel.tsx rename to src/features/playlist/components/PlaylistSongSearchPanel.tsx index 6978a5d5..abe82a9f 100644 --- a/src/components/playlist/PlaylistSongSearchPanel.tsx +++ b/src/features/playlist/components/PlaylistSongSearchPanel.tsx @@ -1,13 +1,13 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { Check, ListPlus, X } from 'lucide-react'; -import type { SubsonicSong } from '../../api/subsonicTypes'; -import { usePlayerStore } from '../../store/playerStore'; -import { songToTrack } from '../../utils/playback/songToTrack'; -import { formatTrackTime } from '../../utils/format/formatDuration'; -import { AlbumCoverArtImage } from '../../cover/AlbumCoverArtImage'; -import { COVER_DENSE_SEARCH_CSS_PX } from '../../cover/layoutSizes'; -import { AddToPlaylistSubmenu } from '../ContextMenu'; +import type { SubsonicSong } from '@/api/subsonicTypes'; +import { usePlayerStore } from '@/store/playerStore'; +import { songToTrack } from '@/utils/playback/songToTrack'; +import { formatTrackTime } from '@/utils/format/formatDuration'; +import { AlbumCoverArtImage } from '@/cover/AlbumCoverArtImage'; +import { COVER_DENSE_SEARCH_CSS_PX } from '@/cover/layoutSizes'; +import { AddToPlaylistSubmenu } from '@/components/ContextMenu'; function PlaylistSearchResultThumb({ albumId, coverArt }: { albumId: string; coverArt: string }) { return ( diff --git a/src/components/playlist/PlaylistSuggestions.tsx b/src/features/playlist/components/PlaylistSuggestions.tsx similarity index 92% rename from src/components/playlist/PlaylistSuggestions.tsx rename to src/features/playlist/components/PlaylistSuggestions.tsx index 0b70cef3..29688e41 100644 --- a/src/components/playlist/PlaylistSuggestions.tsx +++ b/src/features/playlist/components/PlaylistSuggestions.tsx @@ -2,20 +2,20 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { ChevronRight, Heart, Play, Plus, RefreshCw, Square } from 'lucide-react'; -import type { ColDef } from '../../utils/useTracklistColumns'; -import type { SubsonicSong } from '../../api/subsonicTypes'; -import { usePlayerStore } from '../../store/playerStore'; -import { usePreviewStore } from '../../store/previewStore'; -import StarRating from '../StarRating'; -import { PlaylistArtistCell } from './PlaylistArtistCell'; -import { useThemeStore } from '../../store/themeStore'; -import { usePlaylistLayoutStore } from '../../store/playlistLayoutStore'; -import { songToTrack } from '../../utils/playback/songToTrack'; -import { getQueueTracksView } from '../../utils/library/queueTrackView'; -import { codecLabel } from '../../utils/componentHelpers/playlistDetailHelpers'; -import { formatLastSeen } from '../../utils/componentHelpers/userMgmtHelpers'; -import { formatTrackTime } from '../../utils/format/formatDuration'; -import i18n from '../../i18n'; +import type { ColDef } from '@/utils/useTracklistColumns'; +import type { SubsonicSong } from '@/api/subsonicTypes'; +import { usePlayerStore } from '@/store/playerStore'; +import { usePreviewStore } from '@/store/previewStore'; +import StarRating from '@/components/StarRating'; +import { PlaylistArtistCell } from '@/features/playlist/components/PlaylistArtistCell'; +import { useThemeStore } from '@/store/themeStore'; +import { usePlaylistLayoutStore } from '@/features/playlist/store/playlistLayoutStore'; +import { songToTrack } from '@/utils/playback/songToTrack'; +import { getQueueTracksView } from '@/utils/library/queueTrackView'; +import { codecLabel } from '@/utils/componentHelpers/playlistDetailHelpers'; +import { formatLastSeen } from '@/utils/componentHelpers/userMgmtHelpers'; +import { formatTrackTime } from '@/utils/format/formatDuration'; +import i18n from '@/i18n'; const PL_CENTERED = new Set(['favorite', 'rating', 'duration', 'playCount', 'bpm']); diff --git a/src/components/playlist/PlaylistTracklist.tsx b/src/features/playlist/components/PlaylistTracklist.tsx similarity index 94% rename from src/components/playlist/PlaylistTracklist.tsx rename to src/features/playlist/components/PlaylistTracklist.tsx index 886056d1..94f24efd 100644 --- a/src/components/playlist/PlaylistTracklist.tsx +++ b/src/features/playlist/components/PlaylistTracklist.tsx @@ -1,25 +1,25 @@ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useVirtualizer } from '@tanstack/react-virtual'; -import PlaylistRow, { type PlaylistRowCallbacks } from './PlaylistRow'; -import { TracklistColumnPicker } from '../albumTrackList/TracklistColumnPicker'; +import PlaylistRow, { type PlaylistRowCallbacks } from '@/features/playlist/components/PlaylistRow'; +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 { APP_MAIN_SCROLL_VIEWPORT_ID } from '@/constants/appScroll'; +import { useElementClientHeightById } from '@/hooks/useResizeClientHeight'; import { useNavigate } from 'react-router-dom'; import { ListPlus, Search, Trash2, X, } from 'lucide-react'; -import type { ColDef } from '../../utils/useTracklistColumns'; -import type { SubsonicSong } from '../../api/subsonicTypes'; -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 type { ColDef } from '@/utils/useTracklistColumns'; +import type { SubsonicSong } from '@/api/subsonicTypes'; +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 { useOrbitSongRowBehavior } from '@/features/orbit'; -import { songToTrack } from '../../utils/playback/songToTrack'; -import type { PlaylistSortKey, PlaylistSortDir } from '../../utils/playlist/playlistDisplayedSongs'; -import { AddToPlaylistSubmenu } from '../ContextMenu'; +import { songToTrack } from '@/utils/playback/songToTrack'; +import type { PlaylistSortKey, PlaylistSortDir } from '@/features/playlist/utils/playlistDisplayedSongs'; +import { AddToPlaylistSubmenu } from '@/components/ContextMenu'; const PL_CENTERED = new Set(['favorite', 'rating', 'duration', 'playCount', 'bpm']); diff --git a/src/components/playlists/PlaylistsFolderView.tsx b/src/features/playlist/components/PlaylistsFolderView.tsx similarity index 80% rename from src/components/playlists/PlaylistsFolderView.tsx rename to src/features/playlist/components/PlaylistsFolderView.tsx index f7a238cb..9aa6ac6d 100644 --- a/src/components/playlists/PlaylistsFolderView.tsx +++ b/src/features/playlist/components/PlaylistsFolderView.tsx @@ -1,9 +1,9 @@ import React from 'react'; -import type { SubsonicPlaylist } from '../../api/subsonicTypes'; -import { EMPTY_SERVER_FOLDERS, usePlaylistFolderStore } from '../../store/playlistFolderStore'; -import { groupPlaylistsByFolder } from '../../utils/playlist/playlistFolders'; -import { useDragDrop } from '../../contexts/DragDropContext'; -import PlaylistFolderSection from './PlaylistFolderSection'; +import type { SubsonicPlaylist } from '@/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 PlaylistFolderSection from '@/features/playlist/components/PlaylistFolderSection'; interface Props { serverId: string; diff --git a/src/components/playlists/PlaylistsFolderViewToggle.tsx b/src/features/playlist/components/PlaylistsFolderViewToggle.tsx similarity index 89% rename from src/components/playlists/PlaylistsFolderViewToggle.tsx rename to src/features/playlist/components/PlaylistsFolderViewToggle.tsx index 21815824..3a50d6a1 100644 --- a/src/components/playlists/PlaylistsFolderViewToggle.tsx +++ b/src/features/playlist/components/PlaylistsFolderViewToggle.tsx @@ -1,8 +1,8 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { FolderTree } from 'lucide-react'; -import { useAuthStore } from '../../store/authStore'; -import { usePlaylistFolderStore } from '../../store/playlistFolderStore'; +import { useAuthStore } from '@/store/authStore'; +import { usePlaylistFolderStore } from '@/features/playlist/store/playlistFolderStore'; /** * Header toggle to switch the Playlists page between the grouped folder view diff --git a/src/components/playlists/PlaylistsHeader.tsx b/src/features/playlist/components/PlaylistsHeader.tsx similarity index 94% rename from src/components/playlists/PlaylistsHeader.tsx rename to src/features/playlist/components/PlaylistsHeader.tsx index c764b43c..f67c739f 100644 --- a/src/components/playlists/PlaylistsHeader.tsx +++ b/src/features/playlist/components/PlaylistsHeader.tsx @@ -1,13 +1,13 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { CheckSquare2, Plus, Sparkles, Trash2 } from 'lucide-react'; -import type { SubsonicPlaylist } from '../../api/subsonicTypes'; +import type { SubsonicPlaylist } from '@/api/subsonicTypes'; import { defaultSmartFilters, type SmartFilters, -} from '../../utils/playlist/playlistsSmart'; +} from '@/features/playlist/utils/playlistsSmart'; import { offlineActionPolicy, type OfflineActionPolicy } from '@/features/offline'; -import PlaylistsNewFolderButton from './PlaylistsNewFolderButton'; -import PlaylistsFolderViewToggle from './PlaylistsFolderViewToggle'; +import PlaylistsNewFolderButton from '@/features/playlist/components/PlaylistsNewFolderButton'; +import PlaylistsFolderViewToggle from '@/features/playlist/components/PlaylistsFolderViewToggle'; interface Props { selectionMode: boolean; diff --git a/src/components/playlists/PlaylistsNewFolderButton.tsx b/src/features/playlist/components/PlaylistsNewFolderButton.tsx similarity index 94% rename from src/components/playlists/PlaylistsNewFolderButton.tsx rename to src/features/playlist/components/PlaylistsNewFolderButton.tsx index f768b63e..6da71242 100644 --- a/src/components/playlists/PlaylistsNewFolderButton.tsx +++ b/src/features/playlist/components/PlaylistsNewFolderButton.tsx @@ -1,8 +1,8 @@ import React, { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { FolderPlus } from 'lucide-react'; -import { useAuthStore } from '../../store/authStore'; -import { usePlaylistFolderStore } from '../../store/playlistFolderStore'; +import { useAuthStore } from '@/store/authStore'; +import { usePlaylistFolderStore } from '@/features/playlist/store/playlistFolderStore'; /** * "New folder" action for the Playlists header row. Self-contained: toggles an diff --git a/src/components/playlists/PlaylistsSmartEditor.tsx b/src/features/playlist/components/PlaylistsSmartEditor.tsx similarity index 99% rename from src/components/playlists/PlaylistsSmartEditor.tsx rename to src/features/playlist/components/PlaylistsSmartEditor.tsx index 88d6a384..a8198d7c 100644 --- a/src/components/playlists/PlaylistsSmartEditor.tsx +++ b/src/features/playlist/components/PlaylistsSmartEditor.tsx @@ -1,12 +1,12 @@ import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { Plus } from 'lucide-react'; -import StarRating from '../StarRating'; +import StarRating from '@/components/StarRating'; import CustomSelect from '@/ui/CustomSelect'; import { LIMIT_MAX, YEAR_MAX, YEAR_MIN, clampYear, defaultSmartFilters, type SmartFilters, -} from '../../utils/playlist/playlistsSmart'; +} from '@/features/playlist/utils/playlistsSmart'; interface Props { smartFilters: SmartFilters; diff --git a/src/hooks/usePlaylistBulkPlayCallbacks.ts b/src/features/playlist/hooks/usePlaylistBulkPlayCallbacks.ts similarity index 91% rename from src/hooks/usePlaylistBulkPlayCallbacks.ts rename to src/features/playlist/hooks/usePlaylistBulkPlayCallbacks.ts index ec3551f3..5a206d33 100644 --- a/src/hooks/usePlaylistBulkPlayCallbacks.ts +++ b/src/features/playlist/hooks/usePlaylistBulkPlayCallbacks.ts @@ -1,6 +1,6 @@ import { useCallback } from 'react'; -import type { Track } from '../store/playerStoreTypes'; -import { enqueuePlaylistAll, playPlaylistAll, shufflePlaylistAll } from '../utils/playlist/playlistBulkPlayActions'; +import type { Track } from '@/store/playerStoreTypes'; +import { enqueuePlaylistAll, playPlaylistAll, shufflePlaylistAll } from '@/features/playlist/utils/playlistBulkPlayActions'; export interface PlaylistBulkPlayCallbacksDeps { songsLength: number; diff --git a/src/hooks/usePlaylistCovers.ts b/src/features/playlist/hooks/usePlaylistCovers.ts similarity index 89% rename from src/hooks/usePlaylistCovers.ts rename to src/features/playlist/hooks/usePlaylistCovers.ts index 0f7d3012..2a89b89e 100644 --- a/src/hooks/usePlaylistCovers.ts +++ b/src/features/playlist/hooks/usePlaylistCovers.ts @@ -1,9 +1,9 @@ import { useEffect, useMemo, useState } from 'react'; -import type { SubsonicSong } from '../api/subsonicTypes'; -import type { CoverArtId, CoverArtRef } from '../cover/types'; -import { coverPrefetchRegister } from '../cover/prefetchRegistry'; -import { resolveAlbumCoverRefFromLibrary } from '../cover/resolveEntryLibrary'; -import { useCoverArt } from '../cover/useCoverArt'; +import type { SubsonicSong } from '@/api/subsonicTypes'; +import type { CoverArtId, CoverArtRef } from '@/cover/types'; +import { coverPrefetchRegister } from '@/cover/prefetchRegistry'; +import { resolveAlbumCoverRefFromLibrary } from '@/cover/resolveEntryLibrary'; +import { useCoverArt } from '@/cover/useCoverArt'; const PLAYLIST_HERO_BG_CSS_PX = 200; const PLAYLIST_MAIN_COVER_CSS_PX = 200; diff --git a/src/hooks/usePlaylistDerived.ts b/src/features/playlist/hooks/usePlaylistDerived.ts similarity index 83% rename from src/hooks/usePlaylistDerived.ts rename to src/features/playlist/hooks/usePlaylistDerived.ts index 704af4be..d986257e 100644 --- a/src/hooks/usePlaylistDerived.ts +++ b/src/features/playlist/hooks/usePlaylistDerived.ts @@ -1,9 +1,9 @@ import { useMemo } from 'react'; -import type { SubsonicSong } from '../api/subsonicTypes'; -import type { Track } from '../store/playerStoreTypes'; -import { usePlayerStore } from '../store/playerStore'; -import { songToTrack } from '../utils/playback/songToTrack'; -import { getDisplayedSongs, type PlaylistSortDir, type PlaylistSortKey } from '../utils/playlist/playlistDisplayedSongs'; +import type { SubsonicSong } from '@/api/subsonicTypes'; +import type { Track } from '@/store/playerStoreTypes'; +import { usePlayerStore } from '@/store/playerStore'; +import { songToTrack } from '@/utils/playback/songToTrack'; +import { getDisplayedSongs, type PlaylistSortDir, type PlaylistSortKey } from '@/features/playlist/utils/playlistDisplayedSongs'; export interface PlaylistDerivedOptions { filterText: string; diff --git a/src/hooks/usePlaylistDnDReorder.ts b/src/features/playlist/hooks/usePlaylistDnDReorder.ts similarity index 88% rename from src/hooks/usePlaylistDnDReorder.ts rename to src/features/playlist/hooks/usePlaylistDnDReorder.ts index 395a0468..008668e1 100644 --- a/src/hooks/usePlaylistDnDReorder.ts +++ b/src/features/playlist/hooks/usePlaylistDnDReorder.ts @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react'; -import type { SubsonicSong } from '../api/subsonicTypes'; -import { useDragDrop } from '../contexts/DragDropContext'; -import { runPlaylistReorderDrop } from '../utils/playlist/runPlaylistReorderDrop'; +import type { SubsonicSong } from '@/api/subsonicTypes'; +import { useDragDrop } from '@/contexts/DragDropContext'; +import { runPlaylistReorderDrop } from '@/features/playlist/utils/runPlaylistReorderDrop'; export interface PlaylistDnDReorderDeps { tracklistRef: React.RefObject; diff --git a/src/hooks/usePlaylistPreview.ts b/src/features/playlist/hooks/usePlaylistPreview.ts similarity index 85% rename from src/hooks/usePlaylistPreview.ts rename to src/features/playlist/hooks/usePlaylistPreview.ts index cb0af03c..c81e7bd5 100644 --- a/src/hooks/usePlaylistPreview.ts +++ b/src/features/playlist/hooks/usePlaylistPreview.ts @@ -1,6 +1,6 @@ import { useCallback, useEffect } from 'react'; -import { previewInputFromSong, usePreviewStore } from '../store/previewStore'; -import type { SubsonicSong } from '../api/subsonicTypes'; +import { previewInputFromSong, usePreviewStore } from '@/store/previewStore'; +import type { SubsonicSong } from '@/api/subsonicTypes'; export function usePlaylistPreview(): { startPreview: (song: SubsonicSong) => void; diff --git a/src/hooks/usePlaylistRouteEffects.ts b/src/features/playlist/hooks/usePlaylistRouteEffects.ts similarity index 94% rename from src/hooks/usePlaylistRouteEffects.ts rename to src/features/playlist/hooks/usePlaylistRouteEffects.ts index 50f63c9d..2cf090da 100644 --- a/src/hooks/usePlaylistRouteEffects.ts +++ b/src/features/playlist/hooks/usePlaylistRouteEffects.ts @@ -1,6 +1,6 @@ import React, { useEffect } from 'react'; import type { Location, NavigateFunction } from 'react-router-dom'; -import { usePlayerStore } from '../store/playerStore'; +import { usePlayerStore } from '@/store/playerStore'; export interface PlaylistRouteEffectsDeps { setContextMenuSongId: React.Dispatch>; diff --git a/src/hooks/usePlaylistSelection.ts b/src/features/playlist/hooks/usePlaylistSelection.ts similarity index 96% rename from src/hooks/usePlaylistSelection.ts rename to src/features/playlist/hooks/usePlaylistSelection.ts index 269f604a..d2e167d8 100644 --- a/src/hooks/usePlaylistSelection.ts +++ b/src/features/playlist/hooks/usePlaylistSelection.ts @@ -1,6 +1,6 @@ import { useState } from 'react'; import type React from 'react'; -import type { SubsonicSong } from '../api/subsonicTypes'; +import type { SubsonicSong } from '@/api/subsonicTypes'; export interface PlaylistSelection { selectedIds: Set; diff --git a/src/hooks/usePlaylistSongMutations.ts b/src/features/playlist/hooks/usePlaylistSongMutations.ts similarity index 93% rename from src/hooks/usePlaylistSongMutations.ts rename to src/features/playlist/hooks/usePlaylistSongMutations.ts index 06030b03..c1fa475e 100644 --- a/src/hooks/usePlaylistSongMutations.ts +++ b/src/features/playlist/hooks/usePlaylistSongMutations.ts @@ -1,7 +1,7 @@ import type React from 'react'; import type { TFunction } from 'i18next'; -import type { SubsonicPlaylist, SubsonicSong } from '../api/subsonicTypes'; -import { showToast } from '../utils/ui/toast'; +import type { SubsonicPlaylist, SubsonicSong } from '@/api/subsonicTypes'; +import { showToast } from '@/utils/ui/toast'; export interface PlaylistSongMutationsDeps { songs: SubsonicSong[]; diff --git a/src/hooks/usePlaylistSongSearch.ts b/src/features/playlist/hooks/usePlaylistSongSearch.ts similarity index 93% rename from src/hooks/usePlaylistSongSearch.ts rename to src/features/playlist/hooks/usePlaylistSongSearch.ts index e59f3b32..ebd1c15c 100644 --- a/src/hooks/usePlaylistSongSearch.ts +++ b/src/features/playlist/hooks/usePlaylistSongSearch.ts @@ -1,7 +1,7 @@ import { useEffect, useRef, useState } from 'react'; import type React from 'react'; -import { search } from '../api/subsonicSearch'; -import type { SubsonicSong } from '../api/subsonicTypes'; +import { search } from '@/api/subsonicSearch'; +import type { SubsonicSong } from '@/api/subsonicTypes'; export interface PlaylistSongSearchResult { searchResults: SubsonicSong[]; diff --git a/src/hooks/usePlaylistStarRating.ts b/src/features/playlist/hooks/usePlaylistStarRating.ts similarity index 88% rename from src/hooks/usePlaylistStarRating.ts rename to src/features/playlist/hooks/usePlaylistStarRating.ts index d963a9ba..877cbe73 100644 --- a/src/hooks/usePlaylistStarRating.ts +++ b/src/features/playlist/hooks/usePlaylistStarRating.ts @@ -1,7 +1,7 @@ import React from 'react'; -import { queueSongStar, queueSongRating } from '../store/pendingStarSync'; -import type { SubsonicSong } from '../api/subsonicTypes'; -import { usePlayerStore } from '../store/playerStore'; +import { queueSongStar, queueSongRating } from '@/store/pendingStarSync'; +import type { SubsonicSong } from '@/api/subsonicTypes'; +import { usePlayerStore } from '@/store/playerStore'; export interface PlaylistStarRatingDeps { ratings: Record; diff --git a/src/hooks/usePlaylistSuggestions.ts b/src/features/playlist/hooks/usePlaylistSuggestions.ts similarity index 94% rename from src/hooks/usePlaylistSuggestions.ts rename to src/features/playlist/hooks/usePlaylistSuggestions.ts index fdb36dba..f5269f38 100644 --- a/src/hooks/usePlaylistSuggestions.ts +++ b/src/features/playlist/hooks/usePlaylistSuggestions.ts @@ -1,7 +1,7 @@ import { useCallback, useEffect, useState } from 'react'; import type React from 'react'; -import { getRandomSongs } from '../api/subsonicLibrary'; -import type { SubsonicSong } from '../api/subsonicTypes'; +import { getRandomSongs } from '@/api/subsonicLibrary'; +import type { SubsonicSong } from '@/api/subsonicTypes'; export interface PlaylistSuggestionsResult { suggestions: SubsonicSong[]; diff --git a/src/hooks/usePlaylistsLibraryScopeCounts.ts b/src/features/playlist/hooks/usePlaylistsLibraryScopeCounts.ts similarity index 93% rename from src/hooks/usePlaylistsLibraryScopeCounts.ts rename to src/features/playlist/hooks/usePlaylistsLibraryScopeCounts.ts index 5ebc2549..574a4f3f 100644 --- a/src/hooks/usePlaylistsLibraryScopeCounts.ts +++ b/src/features/playlist/hooks/usePlaylistsLibraryScopeCounts.ts @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; -import { filterSongsToActiveLibrary } from '../api/subsonicLibrary'; -import { getPlaylist } from '../api/subsonicPlaylists'; -import type { SubsonicPlaylist } from '../api/subsonicTypes'; +import { filterSongsToActiveLibrary } from '@/api/subsonicLibrary'; +import { getPlaylist } from '@/features/playlist/api/subsonicPlaylists'; +import type { SubsonicPlaylist } from '@/api/subsonicTypes'; import { useOfflineBrowseContext } from '@/features/offline'; export interface PlaylistsLibraryScopeCountsResult { diff --git a/src/features/playlist/index.ts b/src/features/playlist/index.ts new file mode 100644 index 00000000..7bec89ea --- /dev/null +++ b/src/features/playlist/index.ts @@ -0,0 +1,44 @@ +/** + * Playlist feature — the Playlists overview + PlaylistDetail (lazy via deep + * `pages/*`, not re-exported), playlist/folder UI (cards, hero, tracklist, + * filter toolbar, smart editor, folder views, CSV import), the playlist + + * folder + layout stores, the playlist Subsonic API, and the playlist data/ + * selection/DnD/search/mutation/star hooks + play + CSV/smart utils. + * + * Stays OUT: the playlist context-menu items + add/move submenus (context-menu + * subsystem), the queue-panel Save/Load-playlist modals (queue UI), and + * `playlistDetailHelpers` (shared with offline + favorites; keeping it here + * would create a playlist⟷offline barrel cycle → lib/shared in M4). + */ +export * from './api/subsonicPlaylists'; +export * from './hooks/usePlaylistBulkPlayCallbacks'; +export * from './hooks/usePlaylistCovers'; +export * from './hooks/usePlaylistDerived'; +export * from './hooks/usePlaylistDnDReorder'; +export * from './hooks/usePlaylistPreview'; +export * from './hooks/usePlaylistRouteEffects'; +export * from './hooks/usePlaylistSelection'; +export * from './hooks/usePlaylistsLibraryScopeCounts'; +export * from './hooks/usePlaylistSongMutations'; +export * from './hooks/usePlaylistSongSearch'; +export * from './hooks/usePlaylistStarRating'; +export * from './hooks/usePlaylistSuggestions'; +export * from './store/playlistFolderStore'; +export * from './store/playlistLayoutStore'; +export * from './store/playlistStore'; +export * from './utils/playlistBulkPlayActions'; +export * from './utils/playlistDisplayedSongs'; +export * from './utils/playlistFolders'; +export * from './utils/playlistsSmart'; +export * from './utils/playPlaylistById'; +export * from './utils/runPlaylistCsvImport'; +export * from './utils/runPlaylistLoad'; +export * from './utils/runPlaylistReorderDrop'; +export * from './utils/runPlaylistsActions'; +export * from './utils/runPlaylistSaveMeta'; +export * from './utils/runPlaylistsOpenSmartEditor'; +export * from './utils/runPlaylistsSaveSmart'; +export * from './utils/runPlaylistZipDownload'; +export * from './utils/spotifyCsvImport'; +export * from './utils/spotifyCsvMatch'; +export * from './utils/startPlaylistRowDrag'; diff --git a/src/pages/PlaylistDetail.tsx b/src/features/playlist/pages/PlaylistDetail.tsx similarity index 85% rename from src/pages/PlaylistDetail.tsx rename to src/features/playlist/pages/PlaylistDetail.tsx index babca984..67df8a19 100644 --- a/src/pages/PlaylistDetail.tsx +++ b/src/features/playlist/pages/PlaylistDetail.tsx @@ -1,45 +1,45 @@ -import { updatePlaylist } from '../api/subsonicPlaylists'; -import type { SubsonicPlaylist, SubsonicSong } from '../api/subsonicTypes'; +import { updatePlaylist } from '@/features/playlist/api/subsonicPlaylists'; +import type { SubsonicPlaylist, SubsonicSong } from '@/api/subsonicTypes'; import React, { useEffect, useState, useCallback, useMemo } from 'react'; import { useParams, useNavigate, useLocation } from 'react-router-dom'; -import { useTracklistColumns, type ColDef } from '../utils/useTracklistColumns'; -import { usePlayerStore } from '../store/playerStore'; +import { useTracklistColumns, type ColDef } from '@/utils/useTracklistColumns'; +import { usePlayerStore } from '@/store/playerStore'; import { useShallow } from 'zustand/react/shallow'; -import { usePlaylistStore } from '../store/playlistStore'; +import { usePlaylistStore } from '@/features/playlist/store/playlistStore'; import { useOfflineStore } from '@/features/offline'; -import { useLocalPlaybackStore } from '../store/localPlaybackStore'; +import { useLocalPlaybackStore } from '@/store/localPlaybackStore'; import { useAlbumOfflineState } from '@/features/album'; -import { useAuthStore } from '../store/authStore'; +import { useAuthStore } from '@/store/authStore'; import { useDownloadModalStore } from '@/features/offline'; import { useZipDownloadStore } from '@/features/offline'; -import { useDragDrop } from '../contexts/DragDropContext'; +import { useDragDrop } from '@/contexts/DragDropContext'; import { useTranslation } from 'react-i18next'; -import type { SpotifyCsvTrack } from '../utils/playlist/spotifyCsvImport'; -import { runPlaylistCsvImport } from '../utils/playlist/runPlaylistCsvImport'; -import PlaylistEditModal from '../components/playlist/PlaylistEditModal'; -import CsvImportReportModal from '../components/playlist/CsvImportReportModal'; -import PlaylistSongSearchPanel from '../components/playlist/PlaylistSongSearchPanel'; -import PlaylistSuggestions from '../components/playlist/PlaylistSuggestions'; -import PlaylistHero from '../components/playlist/PlaylistHero'; -import PlaylistTracklist from '../components/playlist/PlaylistTracklist'; -import PlaylistFilterToolbar from '../components/playlist/PlaylistFilterToolbar'; -import type { PlaylistSortKey, PlaylistSortDir } from '../utils/playlist/playlistDisplayedSongs'; -import { runPlaylistZipDownload } from '../utils/playlist/runPlaylistZipDownload'; -import { runPlaylistSaveMeta } from '../utils/playlist/runPlaylistSaveMeta'; -import { runPlaylistLoad } from '../utils/playlist/runPlaylistLoad'; -import { startPlaylistRowDrag } from '../utils/playlist/startPlaylistRowDrag'; -import { usePlaylistCovers } from '../hooks/usePlaylistCovers'; -import { usePlaylistSelection } from '../hooks/usePlaylistSelection'; -import { usePlaylistSuggestions } from '../hooks/usePlaylistSuggestions'; -import { usePlaylistSongSearch } from '../hooks/usePlaylistSongSearch'; -import { usePlaylistSongMutations } from '../hooks/usePlaylistSongMutations'; -import { usePlaylistStarRating } from '../hooks/usePlaylistStarRating'; -import { usePlaylistPreview } from '../hooks/usePlaylistPreview'; -import { usePlaylistBulkPlayCallbacks } from '../hooks/usePlaylistBulkPlayCallbacks'; -import { usePlaylistDerived } from '../hooks/usePlaylistDerived'; -import { usePlaylistRouteEffects } from '../hooks/usePlaylistRouteEffects'; -import { useBulkPlPickerOutsideClick } from '../hooks/useBulkPlPickerOutsideClick'; -import { usePlaylistDnDReorder } from '../hooks/usePlaylistDnDReorder'; +import type { SpotifyCsvTrack } from '@/features/playlist/utils/spotifyCsvImport'; +import { runPlaylistCsvImport } from '@/features/playlist/utils/runPlaylistCsvImport'; +import PlaylistEditModal from '@/features/playlist/components/PlaylistEditModal'; +import CsvImportReportModal from '@/features/playlist/components/CsvImportReportModal'; +import PlaylistSongSearchPanel from '@/features/playlist/components/PlaylistSongSearchPanel'; +import PlaylistSuggestions from '@/features/playlist/components/PlaylistSuggestions'; +import PlaylistHero from '@/features/playlist/components/PlaylistHero'; +import PlaylistTracklist from '@/features/playlist/components/PlaylistTracklist'; +import PlaylistFilterToolbar from '@/features/playlist/components/PlaylistFilterToolbar'; +import type { PlaylistSortKey, PlaylistSortDir } from '@/features/playlist/utils/playlistDisplayedSongs'; +import { runPlaylistZipDownload } from '@/features/playlist/utils/runPlaylistZipDownload'; +import { runPlaylistSaveMeta } from '@/features/playlist/utils/runPlaylistSaveMeta'; +import { runPlaylistLoad } from '@/features/playlist/utils/runPlaylistLoad'; +import { startPlaylistRowDrag } from '@/features/playlist/utils/startPlaylistRowDrag'; +import { usePlaylistCovers } from '@/features/playlist/hooks/usePlaylistCovers'; +import { usePlaylistSelection } from '@/features/playlist/hooks/usePlaylistSelection'; +import { usePlaylistSuggestions } from '@/features/playlist/hooks/usePlaylistSuggestions'; +import { usePlaylistSongSearch } from '@/features/playlist/hooks/usePlaylistSongSearch'; +import { usePlaylistSongMutations } from '@/features/playlist/hooks/usePlaylistSongMutations'; +import { usePlaylistStarRating } from '@/features/playlist/hooks/usePlaylistStarRating'; +import { usePlaylistPreview } from '@/features/playlist/hooks/usePlaylistPreview'; +import { usePlaylistBulkPlayCallbacks } from '@/features/playlist/hooks/usePlaylistBulkPlayCallbacks'; +import { usePlaylistDerived } from '@/features/playlist/hooks/usePlaylistDerived'; +import { usePlaylistRouteEffects } from '@/features/playlist/hooks/usePlaylistRouteEffects'; +import { useBulkPlPickerOutsideClick } from '@/hooks/useBulkPlPickerOutsideClick'; +import { usePlaylistDnDReorder } from '@/features/playlist/hooks/usePlaylistDnDReorder'; import { useOfflineBrowseContext } from '@/features/offline'; import { offlineActionPolicy } from '@/features/offline'; diff --git a/src/pages/Playlists.tsx b/src/features/playlist/pages/Playlists.tsx similarity index 87% rename from src/pages/Playlists.tsx rename to src/features/playlist/pages/Playlists.tsx index 347b81f3..8e5b3d3d 100644 --- a/src/pages/Playlists.tsx +++ b/src/features/playlist/pages/Playlists.tsx @@ -1,37 +1,37 @@ import { resolveMediaServerId, resolvePlaylist } from '@/features/offline'; -import { getGenres } from '../api/subsonicGenres'; -import { filterSongsToActiveLibrary } from '../api/subsonicLibrary'; -import type { SubsonicPlaylist, SubsonicGenre } from '../api/subsonicTypes'; -import { songToTrack } from '../utils/playback/songToTrack'; +import { getGenres } from '@/api/subsonicGenres'; +import { filterSongsToActiveLibrary } from '@/api/subsonicLibrary'; +import type { SubsonicPlaylist, SubsonicGenre } from '@/api/subsonicTypes'; +import { songToTrack } from '@/utils/playback/songToTrack'; import React, { useEffect, useState, useRef, useCallback } from 'react'; -import { usePlayerStore } from '../store/playerStore'; -import { usePlaylistStore } from '../store/playlistStore'; -import { useAuthStore } from '../store/authStore'; +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 '@/hooks/useRangeSelection'; import { defaultSmartFilters, type SmartFilters, type PendingSmartPlaylist, -} from '../utils/playlist/playlistsSmart'; -import { useSmartCoverCollage } from '../hooks/useSmartCoverCollage'; -import { usePlaylistsLibraryScopeCounts } from '../hooks/usePlaylistsLibraryScopeCounts'; -import { usePendingSmartPolling } from '../hooks/usePendingSmartPolling'; -import { runPlaylistsOpenSmartEditor } from '../utils/playlist/runPlaylistsOpenSmartEditor'; -import { runPlaylistsSaveSmart } from '../utils/playlist/runPlaylistsSaveSmart'; +} from '@/features/playlist/utils/playlistsSmart'; +import { useSmartCoverCollage } from '@/hooks/useSmartCoverCollage'; +import { usePlaylistsLibraryScopeCounts } from '@/features/playlist/hooks/usePlaylistsLibraryScopeCounts'; +import { usePendingSmartPolling } from '@/hooks/usePendingSmartPolling'; +import { runPlaylistsOpenSmartEditor } from '@/features/playlist/utils/runPlaylistsOpenSmartEditor'; +import { runPlaylistsSaveSmart } from '@/features/playlist/utils/runPlaylistsSaveSmart'; import { runPlaylistDelete, runPlaylistDeleteSelected, -} from '../utils/playlist/runPlaylistsActions'; -import PlaylistsSmartEditor from '../components/playlists/PlaylistsSmartEditor'; -import PlaylistsHeader from '../components/playlists/PlaylistsHeader'; -import PlaylistCard from '../components/playlists/PlaylistCard'; -import { usePerfProbeFlags } from '../utils/perf/perfFlags'; -import { VirtualCardGrid } from '../components/VirtualCardGrid'; +} from '@/features/playlist/utils/runPlaylistsActions'; +import PlaylistsSmartEditor from '@/features/playlist/components/PlaylistsSmartEditor'; +import PlaylistsHeader from '@/features/playlist/components/PlaylistsHeader'; +import PlaylistCard from '@/features/playlist/components/PlaylistCard'; +import { usePerfProbeFlags } from '@/utils/perf/perfFlags'; +import { VirtualCardGrid } from '@/components/VirtualCardGrid'; import { useOfflineBrowseContext } from '@/features/offline'; import { offlineActionPolicy } from '@/features/offline'; import { Info } from 'lucide-react'; -import PlaylistsFolderView from '../components/playlists/PlaylistsFolderView'; -import { usePlaylistFolderStore } from '../store/playlistFolderStore'; +import PlaylistsFolderView from '@/features/playlist/components/PlaylistsFolderView'; +import { usePlaylistFolderStore } from '@/features/playlist/store/playlistFolderStore'; export default function Playlists() { const { t } = useTranslation(); diff --git a/src/store/playlistFolderStore.test.ts b/src/features/playlist/store/playlistFolderStore.test.ts similarity index 96% rename from src/store/playlistFolderStore.test.ts rename to src/features/playlist/store/playlistFolderStore.test.ts index e30c87ec..64c65c18 100644 --- a/src/store/playlistFolderStore.test.ts +++ b/src/features/playlist/store/playlistFolderStore.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it } from 'vitest'; -import { usePlaylistFolderStore } from './playlistFolderStore'; +import { usePlaylistFolderStore } from '@/features/playlist/store/playlistFolderStore'; const get = () => usePlaylistFolderStore.getState(); const server = (id: string) => get().byServer[id] ?? { folders: [], assignments: {} }; diff --git a/src/store/playlistFolderStore.ts b/src/features/playlist/store/playlistFolderStore.ts similarity index 97% rename from src/store/playlistFolderStore.ts rename to src/features/playlist/store/playlistFolderStore.ts index a56b2504..04e5adbc 100644 --- a/src/store/playlistFolderStore.ts +++ b/src/features/playlist/store/playlistFolderStore.ts @@ -1,6 +1,6 @@ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; -import { nextFolderOrder, type PlaylistFolder } from '../utils/playlist/playlistFolders'; +import { nextFolderOrder, type PlaylistFolder } from '@/features/playlist/utils/playlistFolders'; /** Folder state for a single server (playlist ids are server-scoped). */ export interface ServerPlaylistFolders { diff --git a/src/store/playlistLayoutStore.ts b/src/features/playlist/store/playlistLayoutStore.ts similarity index 100% rename from src/store/playlistLayoutStore.ts rename to src/features/playlist/store/playlistLayoutStore.ts diff --git a/src/store/playlistStore.ts b/src/features/playlist/store/playlistStore.ts similarity index 88% rename from src/store/playlistStore.ts rename to src/features/playlist/store/playlistStore.ts index e8ee45bd..f4c758d9 100644 --- a/src/store/playlistStore.ts +++ b/src/features/playlist/store/playlistStore.ts @@ -1,9 +1,9 @@ -import { getPlaylists } from '../api/subsonicPlaylists'; -import type { SubsonicPlaylist } from '../api/subsonicTypes'; +import { getPlaylists } from '@/features/playlist/api/subsonicPlaylists'; +import type { SubsonicPlaylist } from '@/api/subsonicTypes'; import { create } from 'zustand'; import { persist } from 'zustand/middleware'; -import { createPlaylist as apiCreatePlaylist } from '../api/subsonicPlaylists'; -import { useAuthStore } from './authStore'; +import { createPlaylist as apiCreatePlaylist } from '@/features/playlist/api/subsonicPlaylists'; +import { useAuthStore } from '@/store/authStore'; import { isOfflineBrowseActive } from '@/features/offline'; import { fetchOfflineBrowsablePlaylists } from '@/features/offline'; interface PlaylistStore { diff --git a/src/utils/playlist/playPlaylistById.ts b/src/features/playlist/utils/playPlaylistById.ts similarity index 71% rename from src/utils/playlist/playPlaylistById.ts rename to src/features/playlist/utils/playPlaylistById.ts index 3bb13dea..362965e6 100644 --- a/src/utils/playlist/playPlaylistById.ts +++ b/src/features/playlist/utils/playPlaylistById.ts @@ -1,7 +1,7 @@ -import { getPlaylist } from '../../api/subsonicPlaylists'; -import { songToTrack } from '../playback/songToTrack'; -import { usePlayerStore } from '../../store/playerStore'; -import { playPlaylistAll } from './playlistBulkPlayActions'; +import { getPlaylist } from '@/features/playlist/api/subsonicPlaylists'; +import { songToTrack } from '@/utils/playback/songToTrack'; +import { usePlayerStore } from '@/store/playerStore'; +import { playPlaylistAll } from '@/features/playlist/utils/playlistBulkPlayActions'; /** * Load a playlist's songs and start playback immediately ("Play Now"). diff --git a/src/utils/playlist/playlistBulkPlayActions.test.ts b/src/features/playlist/utils/playlistBulkPlayActions.test.ts similarity index 94% rename from src/utils/playlist/playlistBulkPlayActions.test.ts rename to src/features/playlist/utils/playlistBulkPlayActions.test.ts index ecc9ab41..46ffc6a8 100644 --- a/src/utils/playlist/playlistBulkPlayActions.test.ts +++ b/src/features/playlist/utils/playlistBulkPlayActions.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest'; -import { enqueuePlaylistAll, playPlaylistAll, shufflePlaylistAll } from './playlistBulkPlayActions'; -import type { Track } from '../../store/playerStoreTypes'; +import { enqueuePlaylistAll, playPlaylistAll, shufflePlaylistAll } from '@/features/playlist/utils/playlistBulkPlayActions'; +import type { Track } from '@/store/playerStoreTypes'; // Only id/queue identity matters for these actions. const tracks = [{ id: 'a' }, { id: 'b' }, { id: 'c' }] as unknown as Track[]; diff --git a/src/utils/playlist/playlistBulkPlayActions.ts b/src/features/playlist/utils/playlistBulkPlayActions.ts similarity index 95% rename from src/utils/playlist/playlistBulkPlayActions.ts rename to src/features/playlist/utils/playlistBulkPlayActions.ts index b62d4a02..1b4d3117 100644 --- a/src/utils/playlist/playlistBulkPlayActions.ts +++ b/src/features/playlist/utils/playlistBulkPlayActions.ts @@ -1,4 +1,4 @@ -import type { Track } from '../../store/playerStoreTypes'; +import type { Track } from '@/store/playerStoreTypes'; // No `touchPlaylist` here: playing/shuffling/enqueuing does not modify the // playlist. Touching it bumps `lastModified`, which is the playlist detail diff --git a/src/utils/playlist/playlistDisplayedSongs.test.ts b/src/features/playlist/utils/playlistDisplayedSongs.test.ts similarity index 94% rename from src/utils/playlist/playlistDisplayedSongs.test.ts rename to src/features/playlist/utils/playlistDisplayedSongs.test.ts index 16faeb46..3a049580 100644 --- a/src/utils/playlist/playlistDisplayedSongs.test.ts +++ b/src/features/playlist/utils/playlistDisplayedSongs.test.ts @@ -1,11 +1,11 @@ import { describe, it, expect } from 'vitest'; -import type { SubsonicSong } from '../../api/subsonicTypes'; +import type { SubsonicSong } from '@/api/subsonicTypes'; import { getDisplayedSongs, type DisplayedSongsOptions, type PlaylistSortKey, type PlaylistSortDir, -} from './playlistDisplayedSongs'; +} from '@/features/playlist/utils/playlistDisplayedSongs'; const song = (id: string, title = id, artist = ''): SubsonicSong => ({ id, title, artist }) as SubsonicSong; diff --git a/src/utils/playlist/playlistDisplayedSongs.ts b/src/features/playlist/utils/playlistDisplayedSongs.ts similarity index 97% rename from src/utils/playlist/playlistDisplayedSongs.ts rename to src/features/playlist/utils/playlistDisplayedSongs.ts index 57af9a7c..2bc5eedf 100644 --- a/src/utils/playlist/playlistDisplayedSongs.ts +++ b/src/features/playlist/utils/playlistDisplayedSongs.ts @@ -1,4 +1,4 @@ -import type { SubsonicSong } from '../../api/subsonicTypes'; +import type { SubsonicSong } from '@/api/subsonicTypes'; export type PlaylistSortKey = 'natural' | 'position' | 'title' | 'artist' | 'album' | 'favorite' | 'rating' | 'duration' | 'playCount' | 'lastPlayed' | 'bpm'; export type PlaylistSortDir = 'asc' | 'desc'; diff --git a/src/utils/playlist/playlistFolders.test.ts b/src/features/playlist/utils/playlistFolders.test.ts similarity index 97% rename from src/utils/playlist/playlistFolders.test.ts rename to src/features/playlist/utils/playlistFolders.test.ts index 9fdf494f..387bc5fa 100644 --- a/src/utils/playlist/playlistFolders.test.ts +++ b/src/features/playlist/utils/playlistFolders.test.ts @@ -3,7 +3,7 @@ import { groupPlaylistsByFolder, nextFolderOrder, type PlaylistFolder, -} from './playlistFolders'; +} from '@/features/playlist/utils/playlistFolders'; const folder = (id: string, order: number, name = id): PlaylistFolder => ({ id, name, order, collapsed: false }); diff --git a/src/utils/playlist/playlistFolders.ts b/src/features/playlist/utils/playlistFolders.ts similarity index 100% rename from src/utils/playlist/playlistFolders.ts rename to src/features/playlist/utils/playlistFolders.ts diff --git a/src/utils/playlist/playlistsSmart.test.ts b/src/features/playlist/utils/playlistsSmart.test.ts similarity index 95% rename from src/utils/playlist/playlistsSmart.test.ts rename to src/features/playlist/utils/playlistsSmart.test.ts index 1f620d32..30249767 100644 --- a/src/utils/playlist/playlistsSmart.test.ts +++ b/src/features/playlist/utils/playlistsSmart.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { buildSmartRulesPayload, defaultSmartFilters, parseSmartRulesToFilters } from './playlistsSmart'; +import { buildSmartRulesPayload, defaultSmartFilters, parseSmartRulesToFilters } from '@/features/playlist/utils/playlistsSmart'; describe('buildSmartRulesPayload', () => { it('collapses exclude-all-genres into an untagged-only rule', () => { diff --git a/src/utils/playlist/playlistsSmart.ts b/src/features/playlist/utils/playlistsSmart.ts similarity index 100% rename from src/utils/playlist/playlistsSmart.ts rename to src/features/playlist/utils/playlistsSmart.ts diff --git a/src/utils/playlist/runPlaylistCsvImport.ts b/src/features/playlist/utils/runPlaylistCsvImport.ts similarity index 96% rename from src/utils/playlist/runPlaylistCsvImport.ts rename to src/features/playlist/utils/runPlaylistCsvImport.ts index 4b19281d..8114ba12 100644 --- a/src/utils/playlist/runPlaylistCsvImport.ts +++ b/src/features/playlist/utils/runPlaylistCsvImport.ts @@ -1,16 +1,16 @@ import type { TFunction } from 'i18next'; import { open as openDialog } from '@tauri-apps/plugin-dialog'; import { readTextFile } from '@tauri-apps/plugin-fs'; -import { search } from '../../api/subsonicSearch'; -import type { SubsonicSong } from '../../api/subsonicTypes'; -import { showToast } from '../ui/toast'; -import { parseSpotifyCsv, type SpotifyCsvTrack } from './spotifyCsvImport'; +import { search } from '@/api/subsonicSearch'; +import type { SubsonicSong } from '@/api/subsonicTypes'; +import { showToast } from '@/utils/ui/toast'; +import { parseSpotifyCsv, type SpotifyCsvTrack } from '@/features/playlist/utils/spotifyCsvImport'; import { cleanTrackTitle, similarityScore, calculateDynamicThreshold, processBatch, -} from './spotifyCsvMatch'; +} from '@/features/playlist/utils/spotifyCsvMatch'; export interface CsvImportReport { added: number; diff --git a/src/utils/playlist/runPlaylistLoad.ts b/src/features/playlist/utils/runPlaylistLoad.ts similarity index 85% rename from src/utils/playlist/runPlaylistLoad.ts rename to src/features/playlist/utils/runPlaylistLoad.ts index f220bf17..9ab0fb44 100644 --- a/src/utils/playlist/runPlaylistLoad.ts +++ b/src/features/playlist/utils/runPlaylistLoad.ts @@ -1,9 +1,9 @@ import type React from 'react'; -import { getPlaylist } from '../../api/subsonicPlaylists'; -import { filterSongsToActiveLibrary } from '../../api/subsonicLibrary'; -import type { SubsonicPlaylist, SubsonicSong } from '../../api/subsonicTypes'; -import { useAuthStore } from '../../store/authStore'; -import { usePlaylistStore } from '../../store/playlistStore'; +import { getPlaylist } from '@/features/playlist/api/subsonicPlaylists'; +import { filterSongsToActiveLibrary } from '@/api/subsonicLibrary'; +import type { SubsonicPlaylist, SubsonicSong } from '@/api/subsonicTypes'; +import { useAuthStore } from '@/store/authStore'; +import { usePlaylistStore } from '@/features/playlist/store/playlistStore'; import { isOfflineBrowseActive } from '@/features/offline'; import { resolvePlaylist } from '@/features/offline'; diff --git a/src/utils/playlist/runPlaylistReorderDrop.ts b/src/features/playlist/utils/runPlaylistReorderDrop.ts similarity index 96% rename from src/utils/playlist/runPlaylistReorderDrop.ts rename to src/features/playlist/utils/runPlaylistReorderDrop.ts index c1004cbd..790b2306 100644 --- a/src/utils/playlist/runPlaylistReorderDrop.ts +++ b/src/features/playlist/utils/runPlaylistReorderDrop.ts @@ -1,5 +1,5 @@ import type React from 'react'; -import type { SubsonicSong } from '../../api/subsonicTypes'; +import type { SubsonicSong } from '@/api/subsonicTypes'; export interface RunPlaylistReorderDropDeps { e: Event; diff --git a/src/utils/playlist/runPlaylistSaveMeta.ts b/src/features/playlist/utils/runPlaylistSaveMeta.ts similarity index 90% rename from src/utils/playlist/runPlaylistSaveMeta.ts rename to src/features/playlist/utils/runPlaylistSaveMeta.ts index eaa2a564..16633d47 100644 --- a/src/utils/playlist/runPlaylistSaveMeta.ts +++ b/src/features/playlist/utils/runPlaylistSaveMeta.ts @@ -1,7 +1,7 @@ import type { TFunction } from 'i18next'; -import { getPlaylist, updatePlaylistMeta, uploadPlaylistCoverArt } from '../../api/subsonicPlaylists'; -import type { SubsonicPlaylist } from '../../api/subsonicTypes'; -import { showToast } from '../ui/toast'; +import { getPlaylist, updatePlaylistMeta, uploadPlaylistCoverArt } from '@/features/playlist/api/subsonicPlaylists'; +import type { SubsonicPlaylist } from '@/api/subsonicTypes'; +import { showToast } from '@/utils/ui/toast'; export interface RunPlaylistSaveMetaDeps { id: string; diff --git a/src/utils/playlist/runPlaylistZipDownload.ts b/src/features/playlist/utils/runPlaylistZipDownload.ts similarity index 84% rename from src/utils/playlist/runPlaylistZipDownload.ts rename to src/features/playlist/utils/runPlaylistZipDownload.ts index abdab8e9..b175f4c0 100644 --- a/src/utils/playlist/runPlaylistZipDownload.ts +++ b/src/features/playlist/utils/runPlaylistZipDownload.ts @@ -1,9 +1,9 @@ import { invoke } from '@tauri-apps/api/core'; import { join } from '@tauri-apps/api/path'; -import { buildDownloadUrl } from '../../api/subsonicStreamUrl'; -import type { SubsonicPlaylist } from '../../api/subsonicTypes'; +import { buildDownloadUrl } from '@/api/subsonicStreamUrl'; +import type { SubsonicPlaylist } from '@/api/subsonicTypes'; import { useZipDownloadStore } from '@/features/offline'; -import { sanitizeFilename } from '../componentHelpers/playlistDetailHelpers'; +import { sanitizeFilename } from '@/utils/componentHelpers/playlistDetailHelpers'; export interface RunPlaylistZipDownloadDeps { playlist: SubsonicPlaylist; diff --git a/src/utils/playlist/runPlaylistsActions.ts b/src/features/playlist/utils/runPlaylistsActions.ts similarity index 92% rename from src/utils/playlist/runPlaylistsActions.ts rename to src/features/playlist/utils/runPlaylistsActions.ts index 71ac56c0..44b93da1 100644 --- a/src/utils/playlist/runPlaylistsActions.ts +++ b/src/features/playlist/utils/runPlaylistsActions.ts @@ -1,9 +1,9 @@ import type React from 'react'; import type { TFunction } from 'i18next'; -import { deletePlaylist, getPlaylist, updatePlaylist } from '../../api/subsonicPlaylists'; -import type { SubsonicPlaylist } from '../../api/subsonicTypes'; -import { usePlaylistStore } from '../../store/playlistStore'; -import { showToast } from '../ui/toast'; +import { deletePlaylist, getPlaylist, updatePlaylist } from '@/features/playlist/api/subsonicPlaylists'; +import type { SubsonicPlaylist } from '@/api/subsonicTypes'; +import { usePlaylistStore } from '@/features/playlist/store/playlistStore'; +import { showToast } from '@/utils/ui/toast'; export interface RunPlaylistDeleteDeps { e: React.MouseEvent; diff --git a/src/utils/playlist/runPlaylistsOpenSmartEditor.ts b/src/features/playlist/utils/runPlaylistsOpenSmartEditor.ts similarity index 91% rename from src/utils/playlist/runPlaylistsOpenSmartEditor.ts rename to src/features/playlist/utils/runPlaylistsOpenSmartEditor.ts index a742ac3c..05341362 100644 --- a/src/utils/playlist/runPlaylistsOpenSmartEditor.ts +++ b/src/features/playlist/utils/runPlaylistsOpenSmartEditor.ts @@ -1,12 +1,12 @@ import type React from 'react'; import type { TFunction } from 'i18next'; -import { ndGetSmartPlaylist, ndListSmartPlaylists } from '../../api/navidromeSmart'; -import type { SubsonicGenre, SubsonicPlaylist } from '../../api/subsonicTypes'; +import { ndGetSmartPlaylist, ndListSmartPlaylists } from '@/api/navidromeSmart'; +import type { SubsonicGenre, SubsonicPlaylist } from '@/api/subsonicTypes'; import { defaultSmartFilters, displayPlaylistName, isSmartPlaylistName, parseSmartRulesToFilters, type SmartFilters, -} from './playlistsSmart'; -import { showToast } from '../ui/toast'; +} from '@/features/playlist/utils/playlistsSmart'; +import { showToast } from '@/utils/ui/toast'; export interface RunPlaylistsOpenSmartEditorDeps { pl: SubsonicPlaylist; diff --git a/src/utils/playlist/runPlaylistsSaveSmart.ts b/src/features/playlist/utils/runPlaylistsSaveSmart.ts similarity index 90% rename from src/utils/playlist/runPlaylistsSaveSmart.ts rename to src/features/playlist/utils/runPlaylistsSaveSmart.ts index 8684826a..44be8391 100644 --- a/src/utils/playlist/runPlaylistsSaveSmart.ts +++ b/src/features/playlist/utils/runPlaylistsSaveSmart.ts @@ -1,13 +1,13 @@ import type React from 'react'; import type { TFunction } from 'i18next'; -import { ndCreateSmartPlaylist, ndUpdateSmartPlaylist } from '../../api/navidromeSmart'; -import type { SubsonicPlaylist } from '../../api/subsonicTypes'; -import { usePlaylistStore } from '../../store/playlistStore'; +import { ndCreateSmartPlaylist, ndUpdateSmartPlaylist } from '@/api/navidromeSmart'; +import type { SubsonicPlaylist } from '@/api/subsonicTypes'; +import { usePlaylistStore } from '@/features/playlist/store/playlistStore'; import { buildSmartRulesPayload, defaultSmartFilters, SMART_PREFIX, type PendingSmartPlaylist, type SmartFilters, -} from './playlistsSmart'; -import { showToast } from '../ui/toast'; +} from '@/features/playlist/utils/playlistsSmart'; +import { showToast } from '@/utils/ui/toast'; export interface RunPlaylistsSaveSmartDeps { isNavidromeServer: boolean; diff --git a/src/utils/playlist/spotifyCsvImport.ts b/src/features/playlist/utils/spotifyCsvImport.ts similarity index 100% rename from src/utils/playlist/spotifyCsvImport.ts rename to src/features/playlist/utils/spotifyCsvImport.ts diff --git a/src/utils/playlist/spotifyCsvMatch.ts b/src/features/playlist/utils/spotifyCsvMatch.ts similarity index 100% rename from src/utils/playlist/spotifyCsvMatch.ts rename to src/features/playlist/utils/spotifyCsvMatch.ts diff --git a/src/utils/playlist/startPlaylistRowDrag.ts b/src/features/playlist/utils/startPlaylistRowDrag.ts similarity index 93% rename from src/utils/playlist/startPlaylistRowDrag.ts rename to src/features/playlist/utils/startPlaylistRowDrag.ts index b6d65896..107baf7a 100644 --- a/src/utils/playlist/startPlaylistRowDrag.ts +++ b/src/features/playlist/utils/startPlaylistRowDrag.ts @@ -1,6 +1,6 @@ import type React from 'react'; -import type { SubsonicSong } from '../../api/subsonicTypes'; -import { songToTrack } from '../playback/songToTrack'; +import type { SubsonicSong } from '@/api/subsonicTypes'; +import { songToTrack } from '@/utils/playback/songToTrack'; export interface StartPlaylistRowDragDeps { e: React.MouseEvent; diff --git a/src/features/settings/components/PersonalisationTab.tsx b/src/features/settings/components/PersonalisationTab.tsx index 932fd4e5..952d08f1 100644 --- a/src/features/settings/components/PersonalisationTab.tsx +++ b/src/features/settings/components/PersonalisationTab.tsx @@ -5,7 +5,7 @@ import { useAuthStore } from '@/store/authStore'; import type { QueueDisplayMode } from '@/store/authStoreTypes'; import { useHomeStore } from '@/store/homeStore'; import { usePlayerBarLayoutStore } from '@/store/playerBarLayoutStore'; -import { usePlaylistLayoutStore } from '@/store/playlistLayoutStore'; +import { usePlaylistLayoutStore } from '@/features/playlist'; import { useQueueToolbarStore } from '@/store/queueToolbarStore'; import { useSidebarStore } from '@/features/sidebar'; import SettingsSubSection from '@/features/settings/components/SettingsSubSection'; diff --git a/src/features/settings/components/PlaylistLayoutCustomizer.tsx b/src/features/settings/components/PlaylistLayoutCustomizer.tsx index 0d4d19c1..7adefbfe 100644 --- a/src/features/settings/components/PlaylistLayoutCustomizer.tsx +++ b/src/features/settings/components/PlaylistLayoutCustomizer.tsx @@ -1,6 +1,6 @@ import { useTranslation } from 'react-i18next'; import { Download, FileUp, HardDrive, Lightbulb, Search } from 'lucide-react'; -import { usePlaylistLayoutStore, type PlaylistLayoutItemId } from '@/store/playlistLayoutStore'; +import { usePlaylistLayoutStore, type PlaylistLayoutItemId } from '@/features/playlist'; const PLAYLIST_LAYOUT_ICONS: Record = { addSongs: Search, diff --git a/src/features/sidebar/components/Sidebar.tsx b/src/features/sidebar/components/Sidebar.tsx index 323e938b..9557150d 100644 --- a/src/features/sidebar/components/Sidebar.tsx +++ b/src/features/sidebar/components/Sidebar.tsx @@ -11,7 +11,7 @@ import { useTranslation } from 'react-i18next'; import { PanelLeft, PanelLeftClose, Trash2 } from 'lucide-react'; import PsysonicLogo from '@/components/PsysonicLogo'; import PSmallLogo from '@/components/PSmallLogo'; -import { usePlaylistStore } from '@/store/playlistStore'; +import { usePlaylistStore } from '@/features/playlist'; import OverlayScrollArea from '@/ui/OverlayScrollArea'; import { getLibraryItemsForReorder, diff --git a/src/features/sidebar/components/SidebarPlaylistsSection.tsx b/src/features/sidebar/components/SidebarPlaylistsSection.tsx index ae3d9d53..13b65659 100644 --- a/src/features/sidebar/components/SidebarPlaylistsSection.tsx +++ b/src/features/sidebar/components/SidebarPlaylistsSection.tsx @@ -5,9 +5,9 @@ import { ChevronRight, Folder, PlayCircle, Sparkles } from 'lucide-react'; import { displayPlaylistName, isSmartPlaylistName } from '@/features/sidebar/utils/sidebarHelpers'; import { useAuthStore } from '@/store/authStore'; import { usePlayerStore } from '@/store/playerStore'; -import { usePlaylistStore } from '@/store/playlistStore'; -import { EMPTY_SERVER_FOLDERS, usePlaylistFolderStore } from '@/store/playlistFolderStore'; -import { groupPlaylistsByFolder } from '@/utils/playlist/playlistFolders'; +import { usePlaylistStore } from '@/features/playlist'; +import { EMPTY_SERVER_FOLDERS, usePlaylistFolderStore } from '@/features/playlist'; +import { groupPlaylistsByFolder } from '@/features/playlist'; interface SidebarPlaylist { id: string; diff --git a/src/hooks/usePendingSmartPolling.ts b/src/hooks/usePendingSmartPolling.ts index df6eb14b..6a923a95 100644 --- a/src/hooks/usePendingSmartPolling.ts +++ b/src/hooks/usePendingSmartPolling.ts @@ -1,9 +1,9 @@ import { useEffect } from 'react'; import type React from 'react'; -import { getPlaylist } from '../api/subsonicPlaylists'; +import { getPlaylist } from '@/features/playlist'; import type { SubsonicPlaylist } from '../api/subsonicTypes'; -import { usePlaylistStore } from '../store/playlistStore'; -import type { PendingSmartPlaylist } from '../utils/playlist/playlistsSmart'; +import { usePlaylistStore } from '@/features/playlist'; +import type { PendingSmartPlaylist } from '@/features/playlist'; /** * Poll Navidrome every 10 s for each pending smart playlist until its diff --git a/src/hooks/useSmartCoverCollage.ts b/src/hooks/useSmartCoverCollage.ts index ea5bee1f..9321cec2 100644 --- a/src/hooks/useSmartCoverCollage.ts +++ b/src/hooks/useSmartCoverCollage.ts @@ -1,8 +1,8 @@ import { useEffect, useState } from 'react'; import { filterSongsToActiveLibrary } from '../api/subsonicLibrary'; -import { getPlaylist } from '../api/subsonicPlaylists'; +import { getPlaylist } from '@/features/playlist'; import type { SubsonicPlaylist } from '../api/subsonicTypes'; -import { isSmartPlaylistName } from '../utils/playlist/playlistsSmart'; +import { isSmartPlaylistName } from '@/features/playlist'; /** * Build the 2×2 cover collage for each smart playlist. Pulls each smart diff --git a/src/utils/migrations/advancedModeMigration.ts b/src/utils/migrations/advancedModeMigration.ts index af4974c8..e541764b 100644 --- a/src/utils/migrations/advancedModeMigration.ts +++ b/src/utils/migrations/advancedModeMigration.ts @@ -1,7 +1,7 @@ import { useAuthStore } from '../../store/authStore'; import { DEFAULT_ARTIST_SECTIONS, useArtistLayoutStore } from '@/features/artist'; import { DEFAULT_QUEUE_TOOLBAR_BUTTONS, useQueueToolbarStore } from '../../store/queueToolbarStore'; -import { DEFAULT_PLAYLIST_LAYOUT_ITEMS, usePlaylistLayoutStore } from '../../store/playlistLayoutStore'; +import { DEFAULT_PLAYLIST_LAYOUT_ITEMS, usePlaylistLayoutStore } from '@/features/playlist'; const MIGRATION_FLAG = 'psysonic_advanced_mode_migrated'; const LEGACY_OPEN_KEY = 'psysonic_personalisation_advanced_open';