refactor(playlist): co-locate playlist feature into features/playlist

This commit is contained in:
Psychotoxical
2026-06-30 01:37:05 +02:00
parent 2a425862ae
commit 862941c145
91 changed files with 336 additions and 292 deletions
+2 -2
View File
@@ -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'));
+2 -2
View File
@@ -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';
@@ -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,
@@ -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;
@@ -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);
@@ -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);
@@ -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 (
<>
<div className="context-menu-item" onClick={() => 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) {
<div className="context-menu-divider" />
<div className="context-menu-item" style={{ color: 'var(--danger)' }} onClick={() => 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 && (
<div className="context-menu-item" style={{ color: 'var(--danger)' }} onClick={() => 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) {
@@ -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 {
@@ -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) {
</div>
{offlinePolicy.canEditPlaylist && playlistId && playlistSongIndex !== undefined && (
<div className="context-menu-item" style={{ color: 'var(--danger)' }} onClick={() => 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 {
@@ -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 {
@@ -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';
@@ -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';
@@ -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),
}));
@@ -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,
@@ -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),
}));
@@ -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 {
@@ -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';
+1 -1
View File
@@ -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: () => ({
+1 -1
View File
@@ -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';
+1 -1
View File
@@ -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';
+1 -1
View File
@@ -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() }));
+1 -1
View File
@@ -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';
+1 -1
View File
@@ -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';
+1 -1
View File
@@ -1,4 +1,4 @@
import { getPlaylist, getPlaylists, updatePlaylistMeta } from '@/api/subsonicPlaylists';
import { getPlaylist, getPlaylists, updatePlaylistMeta } from '@/features/playlist';
import {
orbitSessionPlaylistName,
parseOrbitState,
+1 -1
View File
@@ -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';
@@ -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<SubsonicPlaylist[]> {
const data = await api<{ playlists: { playlist: SubsonicPlaylist[] } }>('getPlaylists.view', { _t: Date.now() });
@@ -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: {
@@ -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>): SubsonicSong {
return {
@@ -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).
@@ -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;
@@ -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;
@@ -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;
@@ -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'
@@ -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 ?? {};
@@ -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;
@@ -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 {
@@ -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;
@@ -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 (
@@ -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']);
@@ -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']);
@@ -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;
@@ -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
@@ -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;
@@ -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
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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<HTMLDivElement | null>;
@@ -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;
@@ -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<React.SetStateAction<string | null>>;
@@ -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<string>;
@@ -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[];
@@ -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[];
@@ -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<string, number>;
@@ -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[];
@@ -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 {
+44
View File
@@ -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';
@@ -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';
@@ -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();
@@ -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: {} };
@@ -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 {
@@ -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 {
@@ -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").
@@ -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[];
@@ -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
@@ -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;
@@ -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';
@@ -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 });
@@ -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', () => {
@@ -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;
@@ -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';
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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';
@@ -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<PlaylistLayoutItemId, typeof Search> = {
addSongs: Search,
+1 -1
View File
@@ -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,
@@ -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;
+3 -3
View File
@@ -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
+2 -2
View File
@@ -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
@@ -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';