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';