mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25:46 +00:00
refactor(playlist): co-locate playlist feature into features/playlist
This commit is contained in:
@@ -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'));
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,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,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';
|
||||
|
||||
@@ -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,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,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,4 +1,4 @@
|
||||
import { getPlaylist, getPlaylists, updatePlaylistMeta } from '@/api/subsonicPlaylists';
|
||||
import { getPlaylist, getPlaylists, updatePlaylistMeta } from '@/features/playlist';
|
||||
import {
|
||||
orbitSessionPlaylistName,
|
||||
parseOrbitState,
|
||||
|
||||
@@ -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
-2
@@ -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: {
|
||||
+3
-3
@@ -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 {
|
||||
+2
-2
@@ -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).
|
||||
+6
-6
@@ -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
-1
@@ -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;
|
||||
+5
-5
@@ -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;
|
||||
+2
-2
@@ -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'
|
||||
+3
-3
@@ -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 ?? {};
|
||||
+5
-5
@@ -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;
|
||||
+8
-8
@@ -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 {
|
||||
+8
-8
@@ -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;
|
||||
+7
-7
@@ -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 (
|
||||
+14
-14
@@ -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']);
|
||||
|
||||
+14
-14
@@ -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']);
|
||||
|
||||
+5
-5
@@ -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;
|
||||
+2
-2
@@ -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
|
||||
+4
-4
@@ -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;
|
||||
+2
-2
@@ -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
|
||||
+2
-2
@@ -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;
|
||||
+2
-2
@@ -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;
|
||||
+3
-3
@@ -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
-1
@@ -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
-1
@@ -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>;
|
||||
+2
-2
@@ -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[];
|
||||
+2
-2
@@ -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[];
|
||||
+3
-3
@@ -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>;
|
||||
+2
-2
@@ -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[];
|
||||
+3
-3
@@ -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 {
|
||||
@@ -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
-1
@@ -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
-1
@@ -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 {
|
||||
+4
-4
@@ -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").
|
||||
+2
-2
@@ -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
-1
@@ -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
|
||||
+2
-2
@@ -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
-1
@@ -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';
|
||||
+1
-1
@@ -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
-1
@@ -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', () => {
|
||||
+5
-5
@@ -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;
|
||||
+5
-5
@@ -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
-1
@@ -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;
|
||||
+3
-3
@@ -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;
|
||||
+3
-3
@@ -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;
|
||||
+4
-4
@@ -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;
|
||||
+4
-4
@@ -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;
|
||||
+5
-5
@@ -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;
|
||||
+2
-2
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user