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

This commit is contained in:
Psychotoxical
2026-06-30 01:33:29 +02:00
parent 9ffc42688c
commit 2a425862ae
76 changed files with 387 additions and 347 deletions
+1 -1
View File
@@ -27,7 +27,7 @@ vi.mock('../utils/network/subsonicNetworkGuard', () => ({
import axios from 'axios';
import { pingWithCredentials, pingWithCredentialsForProfile, ping } from './subsonic';
import { getAlbumInfo2 } from './subsonicAlbumInfo';
import { getAlbumInfo2 } from '@/features/album';
import { getStarred } from './subsonicStarRating';
import { search } from './subsonicSearch';
import { getAlbum, getMusicDirectory, getMusicFolders, getMusicIndexes, getRandomSongs, getSong } from './subsonicLibrary';
+5 -5
View File
@@ -10,7 +10,7 @@ import { resolveStartRoute } from '@/features/sidebar';
// Route-level lazy loading: keeps the non-page graph (shell, player, stores) in
// the entry chunk; each page is fetched when its route is first visited.
const Home = lazy(() => import('../pages/Home'));
const Albums = lazy(() => import('../pages/Albums'));
const Albums = lazy(() => import('@/features/album/pages/Albums'));
const Artists = lazy(() => import('@/features/artist/pages/Artists'));
const ArtistDetail = lazy(() => import('@/features/artist/pages/ArtistDetail'));
const Composers = lazy(() => import('../pages/Composers'));
@@ -19,10 +19,10 @@ const NewReleases = lazy(() => import('../pages/NewReleases'));
const Favorites = lazy(() => import('@/features/favorites/pages/Favorites'));
const RandomMix = lazy(() => import('../pages/RandomMix'));
const RandomLanding = lazy(() => import('../pages/RandomLanding'));
const AlbumDetail = lazy(() => import('../pages/AlbumDetail'));
const AlbumDetail = lazy(() => import('@/features/album/pages/AlbumDetail'));
const MostPlayed = lazy(() => import('../pages/MostPlayed'));
const LosslessAlbums = lazy(() => import('../pages/LosslessAlbums'));
const RandomAlbums = lazy(() => import('../pages/RandomAlbums'));
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'));
@@ -33,7 +33,7 @@ const Help = lazy(() => import('../pages/Help'));
const WhatsNew = lazy(() => import('../pages/WhatsNew'));
const DeviceSync = lazy(() => import('@/features/deviceSync/pages/DeviceSync'));
const OfflineLibrary = lazy(() => import('@/features/offline/pages/OfflineLibrary'));
const LabelAlbums = lazy(() => import('../pages/LabelAlbums'));
const LabelAlbums = lazy(() => import('@/features/album/pages/LabelAlbums'));
const SearchBrowsePage = lazy(() => import('@/features/search/pages/SearchBrowsePage'));
const FolderBrowser = lazy(() => import('../pages/FolderBrowser'));
const InternetRadio = lazy(() => import('@/features/radio/pages/InternetRadio'));
+1 -1
View File
@@ -146,7 +146,7 @@ export default function MainApp() {
const handleExport = async (since: number) => {
setExportPickerOpen(false);
try {
const { exportNewAlbumsImage } = await import('../utils/export/exportNewAlbums');
const { exportNewAlbumsImage } = await import('@/features/album');
const result = await exportNewAlbumsImage(since);
if (result) {
const files = result.paths.length > 1 ? ` (${result.paths.length} Dateien)` : '';
+2 -2
View File
@@ -24,8 +24,8 @@ import { playAlbum, playAlbumShuffled } from '../utils/playback/playAlbum';
import { useLongPressAction } from '../hooks/useLongPressAction';
import { LongPressWaveOverlay } from './LongPressWaveOverlay';
import { formatHumanHoursMinutes } from '../utils/format/formatHumanDuration';
import AlbumRow from './AlbumRow';
import { albumArtistDisplayName } from '../utils/album/deriveAlbumHeaderArtistRefs';
import { AlbumRow } from '@/features/album';
import { albumArtistDisplayName } from '@/features/album';
const ANCHOR_HISTORY_KEY_PREFIX = 'psysonic_because_anchor_history:';
const PICKS_HISTORY_KEY_PREFIX = 'psysonic_because_picks:';
+2 -2
View File
@@ -3,7 +3,7 @@ import { resolveAlbum, resolveMediaServerId } from '@/features/offline';
import type { SubsonicAlbum } from '../api/subsonicTypes';
import { songToTrack } from '../utils/playback/songToTrack';
import React, { useEffect, useState, useRef, useCallback, useMemo } from 'react';
import { useNavigateToAlbum } from '../hooks/useNavigateToAlbum';
import { useNavigateToAlbum } from '@/features/album';
import { Play, ListPlus, ChevronLeft, ChevronRight } from 'lucide-react';
import { CoverArtImage } from '../cover/CoverArtImage';
import { useAlbumCoverRef } from '../cover/useLibraryCoverRef';
@@ -22,7 +22,7 @@ import { usePerfProbeFlags } from '../utils/perf/perfFlags';
import { playAlbum, playAlbumShuffled } from '../utils/playback/playAlbum';
import { useLongPressAction } from '../hooks/useLongPressAction';
import { LongPressWaveOverlay } from './LongPressWaveOverlay';
import { albumArtistDisplayName, deriveAlbumArtistRefs } from '../utils/album/deriveAlbumHeaderArtistRefs';
import { albumArtistDisplayName, deriveAlbumArtistRefs } from '@/features/album';
const INTERVAL_MS = 10000;
const HERO_ALBUM_COUNT = 8;
+1 -1
View File
@@ -11,7 +11,7 @@ import { COVER_DENSE_RAIL_CELL_CSS_PX } from '../cover/layoutSizes';
import { enqueueAndPlay } from '../utils/playback/playSong';
import { useDragDrop } from '../contexts/DragDropContext';
import { useOrbitSongRowBehavior } from '@/features/orbit';
import { useNavigateToAlbum } from '../hooks/useNavigateToAlbum';
import { useNavigateToAlbum } from '@/features/album';
import { useNavigateToArtist } from '@/features/artist';
import { OpenArtistRefInline } from '@/features/artist';
import { resolveTrackArtistRefs } from '../utils/playback/trackArtistRefs';
+1 -1
View File
@@ -1,7 +1,7 @@
import type { SubsonicSong } from '../api/subsonicTypes';
import { songToTrack } from '../utils/playback/songToTrack';
import React, { memo } from 'react';
import { useNavigateToAlbum } from '../hooks/useNavigateToAlbum';
import { useNavigateToAlbum } from '@/features/album';
import { useNavigateToArtist } from '@/features/artist';
import { Play, ListPlus } from 'lucide-react';
import { useTranslation } from 'react-i18next';
@@ -1,6 +1,6 @@
import { useTranslation } from 'react-i18next';
import { Play, ListPlus, Radio, Heart, ChevronRight, ChevronsRight, User, Disc3, ListMusic, Info, Sparkles, Star, Trash2, HeartCrack, Share2, Orbit as OrbitIcon } from 'lucide-react';
import { useNavigateToAlbum } from '../../hooks/useNavigateToAlbum';
import { useNavigateToAlbum } from '@/features/album';
import { useNavigateToArtist } from '@/features/artist';
import { resolveAlbum, resolveMediaServerId, resolvePlaylist } from '@/features/offline';
import { queueSongStar } from '../../store/pendingStarSync';
+1 -1
View File
@@ -7,7 +7,7 @@ import {
} from 'lucide-react';
import type { SubsonicPlaylist, SubsonicSong } from '../../api/subsonicTypes';
import type { ZipDownload } from '@/features/offline';
import type { AlbumOfflineStatus } from '../../hooks/useAlbumOfflineState';
import type { AlbumOfflineStatus } from '@/features/album';
import { dequeueOfflinePin } from '@/features/offline';
import { useThemeStore } from '../../store/themeStore';
import { usePlaylistLayoutStore, type PlaylistLayoutItemId } from '../../store/playlistLayoutStore';
+1 -1
View File
@@ -11,7 +11,7 @@ import SongRail from '../SongRail';
import { playSongNow } from '../../utils/playback/playSong';
import { ndListSongs, ndInvalidateSongsCache } from '../../api/navidromeBrowse';
import { usePerfProbeFlags } from '../../utils/perf/perfFlags';
import { useNavigateToAlbum } from '../../hooks/useNavigateToAlbum';
import { useNavigateToAlbum } from '@/features/album';
import { useNavigateToArtist } from '@/features/artist';
import { OpenArtistRefInline } from '@/features/artist';
import { resolveTrackArtistRefs } from '../../utils/playback/trackArtistRefs';
+1 -1
View File
@@ -9,7 +9,7 @@ import { coverStorageKeyFromRef } from './storageKeys';
import { resolveCoverDisplayTier } from './tiers';
import type { CoverArtRef, CoverArtTier, CoverPrefetchPriority, CoverSurfaceKind } from './types';
import { useThemeStore } from '../store/themeStore';
import { deriveAlbumArtistRefs } from '../utils/album/deriveAlbumHeaderArtistRefs';
import { deriveAlbumArtistRefs } from '@/features/album';
import { getHeroBackdropUpgrade, recordHeroBackdropUpgrade } from './heroBackdropMemory';
export type CoverWarmItem = {
@@ -1,5 +1,5 @@
import { api } from './subsonicClient';
import type { AlbumInfo } from './subsonicTypes';
import { api } from '@/api/subsonicClient';
import type { AlbumInfo } from '@/api/subsonicTypes';
export async function getAlbumInfo2(albumId: string): Promise<AlbumInfo | null> {
try {
@@ -1,29 +1,29 @@
import type { SubsonicAlbum } from '../api/subsonicTypes';
import type { SubsonicAlbum } from '@/api/subsonicTypes';
import React, { memo, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { useNavigateToAlbum } from '../hooks/useNavigateToAlbum';
import { useNavigateToAlbum } from '@/features/album/hooks/useNavigateToAlbum';
import { Play, ListPlus, HardDriveDownload, Check } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { usePlayerStore } from '../store/playerStore';
import { useAuthStore } from '../store/authStore';
import { useLocalPlaybackStore } from '../store/localPlaybackStore';
import { usePlayerStore } from '@/store/playerStore';
import { useAuthStore } from '@/store/authStore';
import { useLocalPlaybackStore } from '@/store/localPlaybackStore';
import { isOfflinePinComplete } from '@/features/offline';
import { CoverArtImage } from '../cover/CoverArtImage';
import { useAlbumCoverRef } from '../cover/useLibraryCoverRef';
import { coverStorageKeyFromRef } from '../cover/storageKeys';
import type { CoverPrefetchPriority } from '../cover/types';
import { COVER_DENSE_GRID_MIN_CELL_CSS_PX } from '../cover/layoutSizes';
import { resolveCoverDisplayTier } from '../cover/tiers';
import { acquireUrl } from '../utils/imageCache/urlPool';
import { CoverArtImage } from '@/cover/CoverArtImage';
import { useAlbumCoverRef } from '@/cover/useLibraryCoverRef';
import { coverStorageKeyFromRef } from '@/cover/storageKeys';
import type { CoverPrefetchPriority } from '@/cover/types';
import { COVER_DENSE_GRID_MIN_CELL_CSS_PX } from '@/cover/layoutSizes';
import { resolveCoverDisplayTier } from '@/cover/tiers';
import { acquireUrl } from '@/utils/imageCache/urlPool';
import { OpenArtistRefInline } from '@/features/artist';
import { fetchAlbumTracks, playAlbum, playAlbumShuffled } from '../utils/playback/playAlbum';
import { useLongPressAction } from '../hooks/useLongPressAction';
import { LongPressWaveOverlay } from './LongPressWaveOverlay';
import { useDragDrop } from '../contexts/DragDropContext';
import { isAlbumRecentlyAdded } from '../utils/albumRecency';
import { albumArtistDisplayName, deriveAlbumArtistRefs } from '../utils/album/deriveAlbumHeaderArtistRefs';
import { coverServerScopeForServerId } from '../cover/serverScope';
import { appendServerQuery } from '../utils/navigation/detailServerScope';
import { fetchAlbumTracks, playAlbum, playAlbumShuffled } from '@/utils/playback/playAlbum';
import { useLongPressAction } from '@/hooks/useLongPressAction';
import { LongPressWaveOverlay } from '@/components/LongPressWaveOverlay';
import { useDragDrop } from '@/contexts/DragDropContext';
import { isAlbumRecentlyAdded } from '@/features/album/utils/albumRecency';
import { albumArtistDisplayName, deriveAlbumArtistRefs } from '@/features/album/utils/deriveAlbumHeaderArtistRefs';
import { coverServerScopeForServerId } from '@/cover/serverScope';
import { appendServerQuery } from '@/utils/navigation/detailServerScope';
interface AlbumCardProps {
album: SubsonicAlbum;
@@ -1,8 +1,8 @@
import React from 'react';
import { ListPlus, Search, X } from 'lucide-react';
import type { TFunction } from 'i18next';
import { useSelectionStore } from '../../store/selectionStore';
import { AddToPlaylistSubmenu } from '../ContextMenu';
import { useSelectionStore } from '@/store/selectionStore';
import { AddToPlaylistSubmenu } from '@/components/ContextMenu';
import { offlineActionPolicy, type OfflineActionPolicy } from '@/features/offline';
interface Props {
@@ -1,9 +1,9 @@
import { describe, expect, it, vi } from 'vitest';
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { renderWithProviders } from '../test/helpers/renderWithProviders';
import AlbumHeader from './AlbumHeader';
import type { SubsonicSong } from '../api/subsonicTypes';
import { renderWithProviders } from '@/test/helpers/renderWithProviders';
import AlbumHeader from '@/features/album/components/AlbumHeader';
import type { SubsonicSong } from '@/api/subsonicTypes';
const navigate = vi.fn();
@@ -13,14 +13,14 @@ vi.mock('react-router-dom', async importActual => {
});
// Genre-unrelated dependencies — stub so the test stays focused on the meta row.
vi.mock('../cover/useLibraryCoverRef', () => ({ useAlbumCoverRef: () => undefined }));
vi.mock('../cover/lightbox', () => ({ useCoverLightboxSrc: () => ({ open: vi.fn(), lightbox: null }) }));
vi.mock('../hooks/useAlbumDetailBack', () => ({ useAlbumDetailBack: () => vi.fn() }));
vi.mock('../hooks/useIsMobile', () => ({ useIsMobile: () => false }));
vi.mock('../store/themeStore', () => ({ useThemeStore: () => false }));
vi.mock('./StarRating', () => ({ default: () => null }));
vi.mock('@/cover/useLibraryCoverRef', () => ({ useAlbumCoverRef: () => undefined }));
vi.mock('@/cover/lightbox', () => ({ useCoverLightboxSrc: () => ({ open: vi.fn(), lightbox: null }) }));
vi.mock('@/features/album/hooks/useAlbumDetailBack', () => ({ useAlbumDetailBack: () => vi.fn() }));
vi.mock('@/hooks/useIsMobile', () => ({ useIsMobile: () => false }));
vi.mock('@/store/themeStore', () => ({ useThemeStore: () => false }));
vi.mock('@/components/StarRating', () => ({ default: () => null }));
vi.mock('@/features/artist', () => ({ OpenArtistRefInline: () => null }));
vi.mock('../cover/CoverArtImage', () => ({ CoverArtImage: () => null }));
vi.mock('@/cover/CoverArtImage', () => ({ CoverArtImage: () => null }));
function baseProps() {
return {
@@ -1,27 +1,27 @@
import type { EntityRatingSupportLevel, SubsonicItemGenre, SubsonicOpenArtistRef, SubsonicSong } from '../api/subsonicTypes';
import type { EntityRatingSupportLevel, SubsonicItemGenre, SubsonicOpenArtistRef, SubsonicSong } from '@/api/subsonicTypes';
import { type CSSProperties, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { useNavigate } from 'react-router-dom';
import { Play, Heart, X, ChevronLeft, Download, ListPlus, HardDriveDownload, Share2, Highlighter, Loader2, Shuffle } from 'lucide-react';
import { CoverArtImage } from '../cover/CoverArtImage';
import { useAlbumCoverRef } from '../cover/useLibraryCoverRef';
import { useCoverLightboxSrc } from '../cover/lightbox';
import { CoverArtImage } from '@/cover/CoverArtImage';
import { useAlbumCoverRef } from '@/cover/useLibraryCoverRef';
import { useCoverLightboxSrc } from '@/cover/lightbox';
import { useTranslation } from 'react-i18next';
import { useIsMobile } from '../hooks/useIsMobile';
import { useAlbumDetailBack } from '../hooks/useAlbumDetailBack';
import { useThemeStore } from '../store/themeStore';
import StarRating from './StarRating';
import { copyEntityShareLink } from '../utils/share/copyEntityShareLink';
import { showToast } from '../utils/ui/toast';
import { isAlbumRecentlyAdded } from '../utils/albumRecency';
import { formatLongDuration } from '../utils/format/formatDuration';
import { formatMb } from '../utils/format/formatBytes';
import { sanitizeHtml } from '../utils/sanitizeHtml';
import { useIsMobile } from '@/hooks/useIsMobile';
import { useAlbumDetailBack } from '@/features/album/hooks/useAlbumDetailBack';
import { useThemeStore } from '@/store/themeStore';
import StarRating from '@/components/StarRating';
import { copyEntityShareLink } from '@/utils/share/copyEntityShareLink';
import { showToast } from '@/utils/ui/toast';
import { isAlbumRecentlyAdded } from '@/features/album/utils/albumRecency';
import { formatLongDuration } from '@/utils/format/formatDuration';
import { formatMb } from '@/utils/format/formatBytes';
import { sanitizeHtml } from '@/utils/sanitizeHtml';
import { OpenArtistRefInline } from '@/features/artist';
import { tooltipAttrs } from '@/ui/tooltipAttrs';
import { offlineActionPolicy, type OfflineActionPolicy } from '@/features/offline';
import { deriveAlbumGenreTags } from '../utils/library/genreTags';
import { genreColor } from '../utils/library/genreColor';
import { deriveAlbumGenreTags } from '@/utils/library/genreTags';
import { genreColor } from '@/utils/library/genreColor';
/** True when the album artist label means "no single artist" `getArtistInfo`
* has nothing meaningful to return for these, so the Artist Bio entry is hidden.
@@ -1,11 +1,11 @@
import type { SubsonicAlbum } from '../api/subsonicTypes';
import type { SubsonicAlbum } from '@/api/subsonicTypes';
import React, { useRef, useState, useEffect, useLayoutEffect, useMemo } from 'react';
import AlbumCard from './AlbumCard';
import AlbumCard from '@/features/album/components/AlbumCard';
import { ChevronLeft, ChevronRight, ArrowRight } from 'lucide-react';
import { NavLink, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { usePerfProbeFlags } from '../utils/perf/perfFlags';
import { dedupeById } from '../utils/dedupeById';
import { usePerfProbeFlags } from '@/utils/perf/perfFlags';
import { dedupeById } from '@/utils/dedupeById';
interface Props {
title: string;
@@ -1,23 +1,23 @@
import type { SubsonicSong } from '../api/subsonicTypes';
import type { Track } from '../store/playerStoreTypes';
import type { SubsonicSong } from '@/api/subsonicTypes';
import type { Track } from '@/store/playerStoreTypes';
import React, { useMemo, useState, useEffect } from 'react';
import { useTracklistColumns } from '../utils/useTracklistColumns';
import { usePlayerStore } from '../store/playerStore';
import { useTracklistColumns } from '@/utils/useTracklistColumns';
import { usePlayerStore } from '@/store/playerStore';
import { useTranslation } from 'react-i18next';
import { useIsMobile } from '../hooks/useIsMobile';
import { useSelectionStore } from '../store/selectionStore';
import { useIsMobile } from '@/hooks/useIsMobile';
import { useSelectionStore } from '@/store/selectionStore';
import {
COLUMNS,
type SortKey,
} from '../utils/componentHelpers/albumTrackListHelpers';
import { useAlbumTrackListSelection } from '../hooks/useAlbumTrackListSelection';
import { TrackRow } from './albumTrackList/TrackRow';
import { AlbumTrackListMobile } from './albumTrackList/AlbumTrackListMobile';
import { TracklistColumnPicker } from './albumTrackList/TracklistColumnPicker';
import { TracklistHeaderRow } from './albumTrackList/TracklistHeaderRow';
} from '@/features/album/utils/albumTrackListHelpers';
import { useAlbumTrackListSelection } from '@/features/album/hooks/useAlbumTrackListSelection';
import { TrackRow } from '@/features/album/components/TrackRow';
import { AlbumTrackListMobile } from '@/features/album/components/AlbumTrackListMobile';
import { TracklistColumnPicker } from '@/components/albumTrackList/TracklistColumnPicker';
import { TracklistHeaderRow } from '@/features/album/components/TracklistHeaderRow';
import { offlineActionPolicy, type OfflineActionPolicy } from '@/features/offline';
export type { SortKey } from '../utils/componentHelpers/albumTrackListHelpers';
export type { SortKey } from '@/features/album/utils/albumTrackListHelpers';
interface AlbumTrackListProps {
songs: SubsonicSong[];
@@ -1,9 +1,9 @@
import React from 'react';
import { AudioLines } from 'lucide-react';
import type { SubsonicSong } from '../../api/subsonicTypes';
import type { Track } from '../../store/playerStoreTypes';
import { songToTrack } from '../../utils/playback/songToTrack';
import { formatLongDuration } from '../../utils/format/formatDuration';
import type { SubsonicSong } from '@/api/subsonicTypes';
import type { Track } from '@/store/playerStoreTypes';
import { songToTrack } from '@/utils/playback/songToTrack';
import { formatLongDuration } from '@/utils/format/formatDuration';
interface Props {
discNums: number[];
@@ -1,12 +1,12 @@
import type { SubsonicAlbum } from '../api/subsonicTypes';
import type { SubsonicAlbum } from '@/api/subsonicTypes';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ndListLosslessAlbumsPage } from '../api/navidromeBrowse';
import AlbumRow from './AlbumRow';
import { useAuthStore } from '../store/authStore';
import { useLibraryIndexStore } from '../store/libraryIndexStore';
import { runLocalLosslessAlbums } from '../utils/library/browseTextSearch';
import { LOSSLESS_MODE_QUERY } from '../utils/library/losslessMode';
import { ndListLosslessAlbumsPage } from '@/api/navidromeBrowse';
import AlbumRow from '@/features/album/components/AlbumRow';
import { useAuthStore } from '@/store/authStore';
import { useLibraryIndexStore } from '@/store/libraryIndexStore';
import { runLocalLosslessAlbums } from '@/utils/library/browseTextSearch';
import { LOSSLESS_MODE_QUERY } from '@/utils/library/losslessMode';
interface Props {
disableArtwork?: boolean;
@@ -2,20 +2,20 @@ import React from 'react';
import { AudioLines, ChevronRight, Heart, Play, Square } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import type { ColDef } from '../../utils/useTracklistColumns';
import type { SubsonicSong } from '../../api/subsonicTypes';
import type { Track } from '../../store/playerStoreTypes';
import { songToTrack } from '../../utils/playback/songToTrack';
import { useSelectionStore } from '../../store/selectionStore';
import { useThemeStore } from '../../store/themeStore';
import { previewInputFromSong, usePreviewStore } from '../../store/previewStore';
import StarRating from '../StarRating';
import { codecLabel, type ColKey } from '../../utils/componentHelpers/albumTrackListHelpers';
import { formatLongDuration } from '../../utils/format/formatDuration';
import { formatLastSeen } from '../../utils/componentHelpers/userMgmtHelpers';
import i18n from '../../i18n';
import type { ColDef } from '@/utils/useTracklistColumns';
import type { SubsonicSong } from '@/api/subsonicTypes';
import type { Track } from '@/store/playerStoreTypes';
import { songToTrack } from '@/utils/playback/songToTrack';
import { useSelectionStore } from '@/store/selectionStore';
import { useThemeStore } from '@/store/themeStore';
import { previewInputFromSong, usePreviewStore } from '@/store/previewStore';
import StarRating from '@/components/StarRating';
import { codecLabel, type ColKey } from '@/features/album/utils/albumTrackListHelpers';
import { formatLongDuration } from '@/utils/format/formatDuration';
import { formatLastSeen } from '@/utils/componentHelpers/userMgmtHelpers';
import i18n from '@/i18n';
import { offlineActionPolicy, type OfflineActionPolicy } from '@/features/offline';
import { resolveTrackArtistRefs } from '../../utils/playback/trackArtistRefs';
import { resolveTrackArtistRefs } from '@/utils/playback/trackArtistRefs';
type ContextMenuFn = (
x: number,
@@ -1,7 +1,7 @@
import React from 'react';
import type { TFunction } from 'i18next';
import type { ColDef } from '../../utils/useTracklistColumns';
import { CENTERED_COLS, isSortable, type ColKey, type SortKey } from '../../utils/componentHelpers/albumTrackListHelpers';
import type { ColDef } from '@/utils/useTracklistColumns';
import { CENTERED_COLS, isSortable, type ColKey, type SortKey } from '@/features/album/utils/albumTrackListHelpers';
interface Props {
visibleCols: readonly ColDef[];
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import type { SubsonicAlbum } from '../api/subsonicTypes';
import type { SubsonicAlbum } from '@/api/subsonicTypes';
// Keep pagination termination rules aligned with the hook implementation.
function resolveHasMoreAfterPage(
@@ -5,16 +5,16 @@ import {
useRef,
useState,
} from 'react';
import type { SubsonicAlbum } from '../api/subsonicTypes';
import type { SubsonicAlbum } from '@/api/subsonicTypes';
import {
coverTrafficBeginGridPagination,
coverTrafficEndGridPagination,
coverTrafficGridPaginationDepth,
} from '../cover/coverTraffic';
import { coverEnsureQueueBacklog, coverEnsureResumePump, coverEnsureSubscribeBacklogDrain } from '../cover/ensureQueue';
import { dedupeById } from '../utils/dedupeById';
import { albumBrowseCompScanComplete, albumBrowseCompFilterClientOnly } from '../utils/library/albumCompilation';
import type { AlbumCompFilter } from '../utils/library/albumCompilation';
} from '@/cover/coverTraffic';
import { coverEnsureQueueBacklog, coverEnsureResumePump, coverEnsureSubscribeBacklogDrain } from '@/cover/ensureQueue';
import { dedupeById } from '@/utils/dedupeById';
import { albumBrowseCompScanComplete, albumBrowseCompFilterClientOnly } from '@/utils/library/albumCompilation';
import type { AlbumCompFilter } from '@/utils/library/albumCompilation';
import {
albumBrowseHasGenreFilter,
albumBrowseHasServerFilters,
@@ -24,22 +24,22 @@ import {
fetchLocalAlbumCatalogChunk,
type AlbumBrowseQuery,
type GenreFilterOption,
} from '../utils/library/albumBrowseLoad';
import { libraryScopeForServer } from '../api/subsonicClient';
} from '@/utils/library/albumBrowseLoad';
import { libraryScopeForServer } from '@/api/subsonicClient';
import {
ALBUM_YEAR_FILTER_DEBOUNCE_MS,
resolveAlbumYearBounds,
} from '../utils/library/albumYearFilter';
} from '@/utils/library/albumYearFilter';
import { loadOfflineAlbumBrowseInitial } from '@/features/offline';
import { useOfflineBrowseReloadToken } from '@/features/offline';
import {
fetchAlbumBrowseCatalogChunk,
mergeAlbumCatalogChunk,
} from '../utils/library/albumBrowseCatalogChunk';
} from '@/utils/library/albumBrowseCatalogChunk';
import { useOfflineBrowseContext } from '@/features/offline';
import { useClientSliceInfiniteScroll } from './useClientSliceInfiniteScroll';
import { useDebouncedValue } from './useDebouncedValue';
import { useInpageScrollSentinel } from './useInpageScrollSentinel';
import { useClientSliceInfiniteScroll } from '@/hooks/useClientSliceInfiniteScroll';
import { useDebouncedValue } from '@/hooks/useDebouncedValue';
import { useInpageScrollSentinel } from '@/hooks/useInpageScrollSentinel';
const PAGE_SIZE = 30;
const CLIENT_SLICE_PAGE_SIZE = 60;
@@ -3,7 +3,7 @@ import { useLocation, useNavigationType, type NavigationType } from 'react-route
import {
ALBUMS_INPAGE_SCROLL_VIEWPORT_ID,
readInpageScrollTop,
} from '../constants/appScroll';
} from '@/constants/appScroll';
import {
DEFAULT_ALBUM_BROWSE_RETURN_FILTERS,
type AlbumBrowseCompFilter,
@@ -13,10 +13,10 @@ import {
albumBrowseSurfaceForPath,
isAlbumDetailPath,
useAlbumBrowseSessionStore,
} from '../store/albumBrowseSessionStore';
import type { AlbumBrowseSort } from '../utils/library/browseTextSearch';
import { shouldRestoreAlbumBrowseSession } from '../utils/navigation/albumDetailNavigation';
import { useLiveSearchScopeStore } from '../store/liveSearchScopeStore';
} from '@/features/album/store/albumBrowseSessionStore';
import type { AlbumBrowseSort } from '@/utils/library/browseTextSearch';
import { shouldRestoreAlbumBrowseSession } from '@/utils/navigation/albumDetailNavigation';
import { useLiveSearchScopeStore } from '@/store/liveSearchScopeStore';
const ALBUMS_SURFACE: AlbumBrowseSurface = 'albums';
@@ -1,5 +1,5 @@
import { useLayoutEffect, useRef, type RefObject } from 'react';
import type { AlbumBrowseScrollSnapshot } from './useAlbumBrowseFilters';
import type { AlbumBrowseScrollSnapshot } from '@/features/album/hooks/useAlbumBrowseFilters';
type Args = {
scrollSnapshotRef: RefObject<AlbumBrowseScrollSnapshot>;
@@ -6,8 +6,8 @@ import {
peekGenreDetailScrollRestore,
type AlbumBrowseSurface,
useAlbumBrowseSessionStore,
} from '../store/albumBrowseSessionStore';
import { shouldRestoreAlbumBrowseSession } from '../utils/navigation/albumDetailNavigation';
} from '@/features/album/store/albumBrowseSessionStore';
import { shouldRestoreAlbumBrowseSession } from '@/utils/navigation/albumDetailNavigation';
type PendingScroll = {
scrollTop: number;
@@ -3,8 +3,8 @@ import {
ALBUM_YEAR_MAX,
ALBUM_YEAR_MIN,
type AlbumCatalogYearRange,
} from '../utils/library/albumYearFilter';
import { fetchAlbumCatalogYearBounds } from '../utils/library/albumCatalogYearBounds';
} from '@/utils/library/albumYearFilter';
import { fetchAlbumCatalogYearBounds } from '@/utils/library/albumCatalogYearBounds';
const DEFAULT: AlbumCatalogYearRange = { min: ALBUM_YEAR_MIN, max: ALBUM_YEAR_MAX };
@@ -3,11 +3,11 @@ import React from 'react';
import { describe, expect, it, vi, beforeEach } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { MemoryRouter, Route, Routes } from 'react-router-dom';
import { useAlbumDetailBack } from './useAlbumDetailBack';
import { navigateAlbumDetailBack } from '../utils/navigation/albumDetailNavigation';
import { useAlbumDetailBack } from '@/features/album/hooks/useAlbumDetailBack';
import { navigateAlbumDetailBack } from '@/utils/navigation/albumDetailNavigation';
vi.mock('../utils/navigation/albumDetailNavigation', async (importOriginal) => {
const mod = await importOriginal<typeof import('../utils/navigation/albumDetailNavigation')>();
vi.mock('@/utils/navigation/albumDetailNavigation', async (importOriginal) => {
const mod = await importOriginal<typeof import('@/utils/navigation/albumDetailNavigation')>();
return {
...mod,
navigateAlbumDetailBack: vi.fn(mod.navigateAlbumDetailBack),
@@ -3,7 +3,7 @@ import { useLocation, useNavigate } from 'react-router-dom';
import {
navigateAlbumDetailBack,
readAlbumDetailReturnTo,
} from '../utils/navigation/albumDetailNavigation';
} from '@/utils/navigation/albumDetailNavigation';
/** Leave album/artist detail for the page that opened it (or history back as fallback). */
export function useAlbumDetailBack(fallback = '/') {
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import type { SubsonicAlbum } from '../api/subsonicTypes';
import { useAuthStore } from '../store/authStore';
import type { SubsonicAlbum } from '@/api/subsonicTypes';
import { useAuthStore } from '@/store/authStore';
import {
loadAlbumFromLibraryIndex,
loadArtistFromLibraryIndex,
@@ -16,12 +16,12 @@ import {
loadArtistFromLocalPlayback,
offlineLocalBrowseEnabled,
} from '@/features/offline';
import { readDetailServerId } from '../utils/navigation/detailServerScope';
import { libraryIsReady } from '../utils/library/libraryReady';
import { readDetailServerId } from '@/utils/navigation/detailServerScope';
import { libraryIsReady } from '@/utils/library/libraryReady';
import {
shouldAttemptSubsonicForActiveServer,
shouldAttemptSubsonicForServer,
} from '../utils/network/subsonicNetworkGuard';
} from '@/utils/network/subsonicNetworkGuard';
type AlbumPayload = ResolvedAlbum;
@@ -1,5 +1,5 @@
import { useMemo, useState } from 'react';
import type { SubsonicSong } from '../api/subsonicTypes';
import type { SubsonicSong } from '@/api/subsonicTypes';
export type AlbumSortKey = 'natural' | 'title' | 'artist' | 'album' | 'favorite' | 'rating' | 'duration' | 'playCount' | 'lastPlayed' | 'bpm';
@@ -1,6 +1,6 @@
import { useEffect, useRef, useState, type RefObject } from 'react';
import { useLocation, useNavigationType, type NavigationType } from 'react-router-dom';
import type { SubsonicAlbum } from '../api/subsonicTypes';
import type { SubsonicAlbum } from '@/api/subsonicTypes';
import {
DEFAULT_ALBUM_BROWSE_RETURN_FILTERS,
type AlbumBrowseReturnFilters,
@@ -8,14 +8,14 @@ import {
albumBrowseSurfaceForPath,
isAlbumDetailPath,
useAlbumBrowseSessionStore,
} from '../store/albumBrowseSessionStore';
import { shouldRestoreAlbumBrowseSession } from '../utils/navigation/albumDetailNavigation';
import { useLiveSearchScopeStore } from '../store/liveSearchScopeStore';
} from '@/features/album/store/albumBrowseSessionStore';
import { shouldRestoreAlbumBrowseSession } from '@/utils/navigation/albumDetailNavigation';
import { useLiveSearchScopeStore } from '@/store/liveSearchScopeStore';
import {
inpageScrollViewportIdForSurface,
readInpageScrollTop,
} from '../constants/appScroll';
import type { AlbumBrowseScrollSnapshot } from './useAlbumBrowseFilters';
} from '@/constants/appScroll';
import type { AlbumBrowseScrollSnapshot } from '@/features/album/hooks/useAlbumBrowseFilters';
export type AlbumGridBrowseSnapshot = {
albums: SubsonicAlbum[];
@@ -1,7 +1,7 @@
import { beforeEach, describe, expect, it } from 'vitest';
import { renderHook } from '@testing-library/react';
import { useOfflineJobStore } from '@/features/offline';
import { useAlbumOfflineState } from './useAlbumOfflineState';
import { useAlbumOfflineState } from '@/features/album/hooks/useAlbumOfflineState';
describe('useAlbumOfflineState', () => {
beforeEach(() => {
@@ -1,4 +1,4 @@
import { useLocalPlaybackStore } from '../store/localPlaybackStore';
import { useLocalPlaybackStore } from '@/store/localPlaybackStore';
import { useOfflineJobStore } from '@/features/offline';
import { isOfflinePinComplete } from '@/features/offline';
@@ -1,8 +1,8 @@
import React, { useCallback, useEffect, useRef } from 'react';
import type { SubsonicSong } from '../api/subsonicTypes';
import { useSelectionStore } from '../store/selectionStore';
import { useDragDrop } from '../contexts/DragDropContext';
import { songToTrack } from '../utils/playback/songToTrack';
import type { SubsonicSong } from '@/api/subsonicTypes';
import { useSelectionStore } from '@/store/selectionStore';
import { useDragDrop } from '@/contexts/DragDropContext';
import { songToTrack } from '@/utils/playback/songToTrack';
interface UseAlbumTrackListSelectionArgs {
songs: SubsonicSong[];
@@ -1,4 +1,4 @@
import type { SubsonicAlbum } from '../api/subsonicTypes';
import type { SubsonicAlbum } from '@/api/subsonicTypes';
import { useEffect, useRef, useState } from 'react';
import {
BROWSE_TEXT_DEBOUNCE_NETWORK_MS,
@@ -7,7 +7,7 @@ import {
raceBrowseWithLocalFallback,
runLocalBrowseAlbums,
runNetworkBrowseAlbums,
} from '../utils/library/browseTextSearch';
} from '@/utils/library/browseTextSearch';
import { useOfflineBrowseContext } from '@/features/offline';
import { offlineLocalBrowseEnabled, searchOfflineLocalAlbums } from '@/features/offline';
@@ -1,14 +1,14 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { SubsonicAlbum } from '../api/subsonicTypes';
import { dedupeById } from '../utils/dedupeById';
import type { AlbumBrowseSort } from '../utils/library/albumBrowseSort';
import type { SubsonicAlbum } from '@/api/subsonicTypes';
import { dedupeById } from '@/utils/dedupeById';
import type { AlbumBrowseSort } from '@/utils/library/albumBrowseSort';
import {
fetchGenreAlbumPage,
GENRE_ALBUM_CATALOG_CHUNK,
GENRE_ALBUM_FIRST_PAGE,
} from '../utils/library/genreAlbumBrowse';
import { useClientSliceInfiniteScroll } from './useClientSliceInfiniteScroll';
import { useInpageScrollSentinel } from './useInpageScrollSentinel';
} from '@/utils/library/genreAlbumBrowse';
import { useClientSliceInfiniteScroll } from '@/hooks/useClientSliceInfiniteScroll';
import { useInpageScrollSentinel } from '@/hooks/useInpageScrollSentinel';
const CLIENT_SLICE_PAGE_SIZE = GENRE_ALBUM_FIRST_PAGE;
@@ -1,6 +1,6 @@
import { useCallback } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { navigateToAlbumDetail } from '../utils/navigation/albumDetailNavigation';
import { navigateToAlbumDetail } from '@/utils/navigation/albumDetailNavigation';
/** Navigate to album detail, remembering the current page for the back button. */
export function useNavigateToAlbum() {
+40
View File
@@ -0,0 +1,40 @@
/**
* Album feature the Albums/Random/Lossless/Label browse pages + AlbumDetail
* (all lazy via deep `pages/*`, not re-exported), album cards/rows/header, the
* album track list + its mobile/header/row pieces, album detail + browse hooks,
* the album session store, per-album offline status, and album export helpers.
*
* Stays OUT (library-core / cross-cutting, consumed by this feature, not owned):
* the `utils/library/album*` browse query engine (shared with offline + other
* browse surfaces M4), `albumDetailNavigation` (cross-cutting nav M4),
* `playAlbum` (playback action playback), `starredAlbumIndexSync` (index
* sync, core), the album context-menu items (context-menu subsystem), the
* shared `TracklistColumnPicker` (also used by favorites), and `cover/*`.
*/
export * from './api/subsonicAlbumInfo';
export * from './hooks/useAlbumBrowseData';
export * from './hooks/useAlbumBrowseFilters';
export * from './hooks/useAlbumBrowseScrollReset';
export * from './hooks/useAlbumBrowseScrollRestore';
export * from './hooks/useAlbumCatalogYearBounds';
export * from './hooks/useAlbumDetailBack';
export * from './hooks/useAlbumDetailData';
export * from './hooks/useAlbumDetailSort';
export * from './hooks/useAlbumGridBrowseFilters';
export * from './hooks/useAlbumOfflineState';
export * from './hooks/useAlbumTrackListSelection';
export * from './hooks/useBrowseAlbumTextSearch';
export * from './hooks/useGenreAlbumBrowse';
export * from './hooks/useNavigateToAlbum';
export * from './store/albumBrowseSessionStore';
export * from './utils/albumDetailHelpers';
export * from './utils/albumRecency';
export * from './utils/albumTrackListHelpers';
export * from './utils/deriveAlbumHeaderArtistRefs';
export * from './utils/exportAlbumCard';
export * from './utils/exportNewAlbums';
export { default as AlbumCard } from './components/AlbumCard';
export { default as AlbumHeader } from './components/AlbumHeader';
export { default as AlbumRow } from './components/AlbumRow';
export { default as AlbumTrackList } from './components/AlbumTrackList';
export { default as LosslessAlbumsRail } from './components/LosslessAlbumsRail';
@@ -1,51 +1,51 @@
import { buildDownloadUrl } from '../api/subsonicStreamUrl';
import { setRating, star, unstar } from '../api/subsonicStarRating';
import { queueSongStar, queueSongRating } from '../store/pendingStarSync';
import { getAlbumForServer } from '../api/subsonicLibrary';
import { buildDownloadUrl } from '@/api/subsonicStreamUrl';
import { setRating, star, unstar } from '@/api/subsonicStarRating';
import { queueSongStar, queueSongRating } from '@/store/pendingStarSync';
import { getAlbumForServer } from '@/api/subsonicLibrary';
import { getArtistInfo } from '@/features/artist';
import type { SubsonicSong } from '../api/subsonicTypes';
import { songToTrack } from '../utils/playback/songToTrack';
import { shuffleArray } from '../utils/playback/shuffleArray';
import type { SubsonicSong } from '@/api/subsonicTypes';
import { songToTrack } from '@/utils/playback/songToTrack';
import { shuffleArray } from '@/utils/playback/shuffleArray';
import React, { useEffect, useState, useCallback, useMemo } from 'react';
import { useParams, useSearchParams } from 'react-router-dom';
import { invoke } from '@tauri-apps/api/core';
import { usePlayerStore } from '../store/playerStore';
import { useAuthStore } from '../store/authStore';
import { usePlayerStore } from '@/store/playerStore';
import { useAuthStore } from '@/store/authStore';
import { useOrbitSongRowBehavior } from '@/features/orbit';
import { useAlbumDetailData } from '../hooks/useAlbumDetailData';
import { useAlbumOfflineState } from '../hooks/useAlbumOfflineState';
import { useAlbumDetailSort } from '../hooks/useAlbumDetailSort';
import { useAlbumDetailData } from '@/features/album/hooks/useAlbumDetailData';
import { useAlbumOfflineState } from '@/features/album/hooks/useAlbumOfflineState';
import { useAlbumDetailSort } from '@/features/album/hooks/useAlbumDetailSort';
import { useDownloadModalStore } from '@/features/offline';
import { useOfflineStore } from '@/features/offline';
import { useOfflineJobStore } from '@/features/offline';
import { isOfflinePinComplete } from '@/features/offline';
import { dequeueOfflinePin } from '@/features/offline';
import { reconcileLibraryTierForAlbum } from '@/features/offline';
import { shouldAttemptSubsonicForServer } from '../utils/network/subsonicNetworkGuard';
import { shouldAttemptSubsonicForServer } from '@/utils/network/subsonicNetworkGuard';
import { join } from '@tauri-apps/api/path';
import { useZipDownloadStore } from '@/features/offline';
import AlbumCard from '../components/AlbumCard';
import AlbumHeader from '../components/AlbumHeader';
import AlbumTrackList from '../components/AlbumTrackList';
import { AlbumDetailToolbar } from '../components/albumDetail/AlbumDetailToolbar';
import { useCoverArt } from '../cover/useCoverArt';
import AlbumCard from '@/features/album/components/AlbumCard';
import AlbumHeader from '@/features/album/components/AlbumHeader';
import AlbumTrackList from '@/features/album/components/AlbumTrackList';
import { AlbumDetailToolbar } from '@/features/album/components/AlbumDetailToolbar';
import { useCoverArt } from '@/cover/useCoverArt';
import {
forgetAlbumDistinctDiscCovers,
rememberAlbumDistinctDiscCovers,
} from '../cover/ref';
import { useAlbumCoverRef } from '../cover/useLibraryCoverRef';
} from '@/cover/ref';
import { useAlbumCoverRef } from '@/cover/useLibraryCoverRef';
import { useTranslation } from 'react-i18next';
import { showToast } from '../utils/ui/toast';
import { useSelectionStore } from '../store/selectionStore';
import { sanitizeFilename } from '../utils/componentHelpers/albumDetailHelpers';
import { albumArtistDisplayName, deriveAlbumHeaderArtistRefs } from '../utils/album/deriveAlbumHeaderArtistRefs';
import { usePerfProbeFlags } from '../utils/perf/perfFlags';
import { albumGridWarmCovers } from '../cover/layoutSizes';
import { VirtualCardGrid } from '../components/VirtualCardGrid';
import LosslessModeBanner from '../components/LosslessModeBanner';
import { isLosslessSuffix } from '../utils/library/losslessFormats';
import { isLosslessMode } from '../utils/library/losslessMode';
import { readDetailServerId } from '../utils/navigation/detailServerScope';
import { showToast } from '@/utils/ui/toast';
import { useSelectionStore } from '@/store/selectionStore';
import { sanitizeFilename } from '@/features/album/utils/albumDetailHelpers';
import { albumArtistDisplayName, deriveAlbumHeaderArtistRefs } from '@/features/album/utils/deriveAlbumHeaderArtistRefs';
import { usePerfProbeFlags } from '@/utils/perf/perfFlags';
import { albumGridWarmCovers } from '@/cover/layoutSizes';
import { VirtualCardGrid } from '@/components/VirtualCardGrid';
import LosslessModeBanner from '@/components/LosslessModeBanner';
import { isLosslessSuffix } from '@/utils/library/losslessFormats';
import { isLosslessMode } from '@/utils/library/losslessMode';
import { readDetailServerId } from '@/utils/navigation/detailServerScope';
import { useOfflineBrowseContext } from '@/features/offline';
import { offlineActionPolicy } from '@/features/offline';
@@ -1,58 +1,58 @@
import { buildDownloadUrl } from '../api/subsonicStreamUrl';
import { buildDownloadUrl } from '@/api/subsonicStreamUrl';
import { resolveAlbum } from '@/features/offline';
import { songToTrack } from '../utils/playback/songToTrack';
import { songToTrack } from '@/utils/playback/songToTrack';
import { useState, useEffect, useLayoutEffect, useRef, useMemo } from 'react';
import AlbumCard from '../components/AlbumCard';
import { albumGridWarmCovers, coverDisplayCssPxForAlbumGrid } from '../cover/layoutSizes';
import { useLibraryCoverPrefetch } from '../cover/useLibraryCoverPrefetch';
import { useAuthStore } from '../store/authStore';
import { clampLibraryGridMaxColumns } from '../store/authStoreHelpers';
import { computeCardGridColumnCount } from '../utils/cardGridLayout';
import GenreFilterBar from '../components/GenreFilterBar';
import YearFilterButton from '../components/YearFilterButton';
import StarFilterButton from '../components/StarFilterButton';
import LosslessFilterButton from '../components/LosslessFilterButton';
import SortDropdown from '../components/SortDropdown';
import AlbumCard from '@/features/album/components/AlbumCard';
import { albumGridWarmCovers, coverDisplayCssPxForAlbumGrid } from '@/cover/layoutSizes';
import { useLibraryCoverPrefetch } from '@/cover/useLibraryCoverPrefetch';
import { useAuthStore } from '@/store/authStore';
import { clampLibraryGridMaxColumns } from '@/store/authStoreHelpers';
import { computeCardGridColumnCount } from '@/utils/cardGridLayout';
import GenreFilterBar from '@/components/GenreFilterBar';
import YearFilterButton from '@/components/YearFilterButton';
import StarFilterButton from '@/components/StarFilterButton';
import LosslessFilterButton from '@/components/LosslessFilterButton';
import SortDropdown from '@/components/SortDropdown';
import { useTranslation } from 'react-i18next';
import { useLocation, useNavigate } from 'react-router-dom';
import { useOfflineStore } from '@/features/offline';
import { useDownloadModalStore } from '@/features/offline';
import { usePlayerStore } from '../store/playerStore';
import { usePlayerStore } from '@/store/playerStore';
import { invoke } from '@tauri-apps/api/core';
import { join } from '@tauri-apps/api/path';
import { showToast } from '../utils/ui/toast';
import { showToast } from '@/utils/ui/toast';
import { useZipDownloadStore } from '@/features/offline';
import { Download, HardDriveDownload, Disc3, ListPlus } from 'lucide-react';
import SelectionToggleButton from '../components/SelectionToggleButton';
import FilterQuickClear from '../components/FilterQuickClear';
import { usePerfProbeFlags } from '../utils/perf/perfFlags';
import { useRangeSelection } from '../hooks/useRangeSelection';
import { useMainstageInpageHeaderTight } from '../hooks/useMainstageInpageHeaderTight';
import { useInpageScrollViewport } from '../hooks/useInpageScrollViewport';
import InpageScrollSentinel from '../components/InpageScrollSentinel';
import { VirtualCardGrid } from '../components/VirtualCardGrid';
import SelectionToggleButton from '@/components/SelectionToggleButton';
import FilterQuickClear from '@/components/FilterQuickClear';
import { usePerfProbeFlags } from '@/utils/perf/perfFlags';
import { useRangeSelection } from '@/hooks/useRangeSelection';
import { useMainstageInpageHeaderTight } from '@/hooks/useMainstageInpageHeaderTight';
import { useInpageScrollViewport } from '@/hooks/useInpageScrollViewport';
import InpageScrollSentinel from '@/components/InpageScrollSentinel';
import { VirtualCardGrid } from '@/components/VirtualCardGrid';
import OverlayScrollArea from '@/ui/OverlayScrollArea';
import { ALBUMS_INPAGE_SCROLL_VIEWPORT_ID } from '../constants/appScroll';
import { useLibraryIndexStore } from '../store/libraryIndexStore';
import { useAlbumBrowseFilters, useAlbumBrowseScrollSnapshotSync, type AlbumBrowseScrollSnapshot } from '../hooks/useAlbumBrowseFilters';
import { useAlbumBrowseData } from '../hooks/useAlbumBrowseData';
import { useAlbumBrowseScrollRestore } from '../hooks/useAlbumBrowseScrollRestore';
import { useAlbumBrowseScrollReset } from '../hooks/useAlbumBrowseScrollReset';
import { useBrowseAlbumTextSearch } from '../hooks/useBrowseAlbumTextSearch';
import { peekAlbumBrowseScrollRestore } from '../store/albumBrowseSessionStore';
import { readAlbumBrowseRestore } from '../utils/navigation/albumDetailNavigation';
import { albumArtistDisplayName } from '../utils/album/deriveAlbumHeaderArtistRefs';
import { useAlbumCatalogYearBounds } from '../hooks/useAlbumCatalogYearBounds';
import type { AlbumBrowseSort } from '../utils/library/albumBrowseSort';
import { LOSSLESS_MODE_QUERY } from '../utils/library/losslessMode';
import { resolveAlbumYearBounds } from '../utils/library/albumYearFilter';
import { ALBUMS_INPAGE_SCROLL_VIEWPORT_ID } from '@/constants/appScroll';
import { useLibraryIndexStore } from '@/store/libraryIndexStore';
import { useAlbumBrowseFilters, useAlbumBrowseScrollSnapshotSync, type AlbumBrowseScrollSnapshot } from '@/features/album/hooks/useAlbumBrowseFilters';
import { useAlbumBrowseData } from '@/features/album/hooks/useAlbumBrowseData';
import { useAlbumBrowseScrollRestore } from '@/features/album/hooks/useAlbumBrowseScrollRestore';
import { useAlbumBrowseScrollReset } from '@/features/album/hooks/useAlbumBrowseScrollReset';
import { useBrowseAlbumTextSearch } from '@/features/album/hooks/useBrowseAlbumTextSearch';
import { peekAlbumBrowseScrollRestore } from '@/features/album/store/albumBrowseSessionStore';
import { readAlbumBrowseRestore } from '@/utils/navigation/albumDetailNavigation';
import { albumArtistDisplayName } from '@/features/album/utils/deriveAlbumHeaderArtistRefs';
import { useAlbumCatalogYearBounds } from '@/features/album/hooks/useAlbumCatalogYearBounds';
import type { AlbumBrowseSort } from '@/utils/library/albumBrowseSort';
import { LOSSLESS_MODE_QUERY } from '@/utils/library/losslessMode';
import { resolveAlbumYearBounds } from '@/utils/library/albumYearFilter';
import {
filterAlbumsByCompilation,
filterAlbumsByGenres,
filterAlbumsByStarred,
filterAlbumsByYearBounds,
} from '../utils/library/albumBrowseFilters';
import { useScopedBrowseSearchQuery } from '../store/liveSearchScopeStore';
} from '@/utils/library/albumBrowseFilters';
import { useScopedBrowseSearchQuery } from '@/store/liveSearchScopeStore';
type SortType = AlbumBrowseSort;
@@ -1,14 +1,14 @@
import { search } from '../api/subsonicSearch';
import type { SubsonicAlbum } from '../api/subsonicTypes';
import { search } from '@/api/subsonicSearch';
import type { SubsonicAlbum } from '@/api/subsonicTypes';
import React, { useEffect, useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { ChevronLeft } from 'lucide-react';
import AlbumCard from '../components/AlbumCard';
import AlbumCard from '@/features/album/components/AlbumCard';
import { useTranslation } from 'react-i18next';
import { useAuthStore } from '../store/authStore';
import { usePerfProbeFlags } from '../utils/perf/perfFlags';
import { albumGridWarmCovers } from '../cover/layoutSizes';
import { VirtualCardGrid } from '../components/VirtualCardGrid';
import { useAuthStore } from '@/store/authStore';
import { usePerfProbeFlags } from '@/utils/perf/perfFlags';
import { albumGridWarmCovers } from '@/cover/layoutSizes';
import { VirtualCardGrid } from '@/components/VirtualCardGrid';
export default function LabelAlbums() {
const { t } = useTranslation();
@@ -1,44 +1,44 @@
import { buildDownloadUrl } from '../api/subsonicStreamUrl';
import { buildDownloadUrl } from '@/api/subsonicStreamUrl';
import { resolveAlbum } from '@/features/offline';
import type { SubsonicAlbum } from '../api/subsonicTypes';
import { songToTrack } from '../utils/playback/songToTrack';
import type { SubsonicAlbum } from '@/api/subsonicTypes';
import { songToTrack } from '@/utils/playback/songToTrack';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import AlbumCard from '../components/AlbumCard';
import { LOSSLESS_MODE_QUERY } from '../utils/library/losslessMode';
import { ndListLosslessAlbumsPage } from '../api/navidromeBrowse';
import AlbumCard from '@/features/album/components/AlbumCard';
import { LOSSLESS_MODE_QUERY } from '@/utils/library/losslessMode';
import { ndListLosslessAlbumsPage } from '@/api/navidromeBrowse';
import { useTranslation } from 'react-i18next';
import { useAuthStore } from '../store/authStore';
import { useAuthStore } from '@/store/authStore';
import { useOfflineStore } from '@/features/offline';
import { useDownloadModalStore } from '@/features/offline';
import { usePlayerStore } from '../store/playerStore';
import { usePlayerStore } from '@/store/playerStore';
import { useZipDownloadStore } from '@/features/offline';
import { useRangeSelection } from '../hooks/useRangeSelection';
import { useMainstageInpageHeaderTight } from '../hooks/useMainstageInpageHeaderTight';
import { usePerfProbeFlags } from '../utils/perf/perfFlags';
import { showToast } from '../utils/ui/toast';
import { useRangeSelection } from '@/hooks/useRangeSelection';
import { useMainstageInpageHeaderTight } from '@/hooks/useMainstageInpageHeaderTight';
import { usePerfProbeFlags } from '@/utils/perf/perfFlags';
import { showToast } from '@/utils/ui/toast';
import { invoke } from '@tauri-apps/api/core';
import { join } from '@tauri-apps/api/path';
import { Download, HardDriveDownload, ListPlus } from 'lucide-react';
import SelectionToggleButton from '../components/SelectionToggleButton';
import { albumGridWarmCovers } from '../cover/layoutSizes';
import { VirtualCardGrid } from '../components/VirtualCardGrid';
import SelectionToggleButton from '@/components/SelectionToggleButton';
import { albumGridWarmCovers } from '@/cover/layoutSizes';
import { VirtualCardGrid } from '@/components/VirtualCardGrid';
import OverlayScrollArea from '@/ui/OverlayScrollArea';
import { LOSSLESS_ALBUMS_INPAGE_SCROLL_VIEWPORT_ID } from '../constants/appScroll';
import { useInpageScrollSentinel } from '../hooks/useInpageScrollSentinel';
import { useInpageScrollViewport } from '../hooks/useInpageScrollViewport';
import InpageScrollSentinel from '../components/InpageScrollSentinel';
import { useLibraryIndexStore } from '../store/libraryIndexStore';
import SortDropdown from '../components/SortDropdown';
import { albumArtistDisplayName } from '../utils/album/deriveAlbumHeaderArtistRefs';
import { LOSSLESS_ALBUMS_INPAGE_SCROLL_VIEWPORT_ID } from '@/constants/appScroll';
import { useInpageScrollSentinel } from '@/hooks/useInpageScrollSentinel';
import { useInpageScrollViewport } from '@/hooks/useInpageScrollViewport';
import InpageScrollSentinel from '@/components/InpageScrollSentinel';
import { useLibraryIndexStore } from '@/store/libraryIndexStore';
import SortDropdown from '@/components/SortDropdown';
import { albumArtistDisplayName } from '@/features/album/utils/deriveAlbumHeaderArtistRefs';
import {
albumBrowseSortForServer,
useAlbumBrowseSessionStore,
} from '../store/albumBrowseSessionStore';
} from '@/features/album/store/albumBrowseSessionStore';
import {
runLocalAlbumBrowsePage,
sortSubsonicAlbums,
type AlbumBrowseSort,
} from '../utils/library/browseTextSearch';
} from '@/utils/library/browseTextSearch';
/** Local index page size — SQLite is cheap; larger pages than the network walk. */
const LOCAL_PAGE_SIZE = 30;
@@ -1,43 +1,43 @@
import { buildDownloadUrl } from '../api/subsonicStreamUrl';
import { getAlbumsByGenre } from '../api/subsonicGenres';
import { getAlbumList } from '../api/subsonicLibrary';
import { buildDownloadUrl } from '@/api/subsonicStreamUrl';
import { getAlbumsByGenre } from '@/api/subsonicGenres';
import { getAlbumList } from '@/api/subsonicLibrary';
import { resolveAlbum } from '@/features/offline';
import type { SubsonicAlbum } from '../api/subsonicTypes';
import { dedupeById } from '../utils/dedupeById';
import { shuffleArray } from '../utils/playback/shuffleArray';
import type { SubsonicAlbum } from '@/api/subsonicTypes';
import { dedupeById } from '@/utils/dedupeById';
import { shuffleArray } from '@/utils/playback/shuffleArray';
import React, { useEffect, useLayoutEffect, useState, useCallback, useRef } from 'react';
import { RefreshCw, Download, HardDriveDownload } from 'lucide-react';
import SelectionToggleButton from '../components/SelectionToggleButton';
import AlbumCard from '../components/AlbumCard';
import GenreFilterBar from '../components/GenreFilterBar';
import SelectionToggleButton from '@/components/SelectionToggleButton';
import AlbumCard from '@/features/album/components/AlbumCard';
import GenreFilterBar from '@/components/GenreFilterBar';
import { useTranslation } from 'react-i18next';
import { useLocation, useNavigate } from 'react-router-dom';
import { useAuthStore } from '../store/authStore';
import { useLibraryIndexStore } from '../store/libraryIndexStore';
import { filterAlbumsByMixRatings, getMixMinRatingsConfigFromAuth } from '../utils/mix/mixRatingFilter';
import { runLocalRandomAlbums, runLocalAlbumsByGenres } from '../utils/library/browseTextSearch';
import { useAuthStore } from '@/store/authStore';
import { useLibraryIndexStore } from '@/store/libraryIndexStore';
import { filterAlbumsByMixRatings, getMixMinRatingsConfigFromAuth } from '@/utils/mix/mixRatingFilter';
import { runLocalRandomAlbums, runLocalAlbumsByGenres } from '@/utils/library/browseTextSearch';
import { useOfflineStore } from '@/features/offline';
import { useDownloadModalStore } from '@/features/offline';
import { invoke } from '@tauri-apps/api/core';
import { join } from '@tauri-apps/api/path';
import { showToast } from '../utils/ui/toast';
import { showToast } from '@/utils/ui/toast';
import { useZipDownloadStore } from '@/features/offline';
import { useRangeSelection } from '../hooks/useRangeSelection';
import { usePerfProbeFlags } from '../utils/perf/perfFlags';
import { albumGridWarmCovers, COVER_DENSE_GRID_MIN_CELL_CSS_PX } from '../cover/layoutSizes';
import { useRangeSelection } from '@/hooks/useRangeSelection';
import { usePerfProbeFlags } from '@/utils/perf/perfFlags';
import { albumGridWarmCovers, COVER_DENSE_GRID_MIN_CELL_CSS_PX } from '@/cover/layoutSizes';
import {
primeAlbumCoversForDisplay,
} from '../cover/warmDiskPeek';
import { VirtualCardGrid } from '../components/VirtualCardGrid';
} from '@/cover/warmDiskPeek';
import { VirtualCardGrid } from '@/components/VirtualCardGrid';
import OverlayScrollArea from '@/ui/OverlayScrollArea';
import { RANDOM_ALBUMS_INPAGE_SCROLL_VIEWPORT_ID } from '../constants/appScroll';
import { useMainstageInpageHeaderTight } from '../hooks/useMainstageInpageHeaderTight';
import { useInpageScrollViewport } from '../hooks/useInpageScrollViewport';
import { useAlbumGridBrowseFilters, type AlbumGridBrowseSnapshot } from '../hooks/useAlbumGridBrowseFilters';
import { useAlbumBrowseScrollRestore } from '../hooks/useAlbumBrowseScrollRestore';
import { useAlbumBrowseScrollSnapshotSync, type AlbumBrowseScrollSnapshot } from '../hooks/useAlbumBrowseFilters';
import { readAlbumBrowseRestore } from '../utils/navigation/albumDetailNavigation';
import { albumArtistDisplayName } from '../utils/album/deriveAlbumHeaderArtistRefs';
import { RANDOM_ALBUMS_INPAGE_SCROLL_VIEWPORT_ID } from '@/constants/appScroll';
import { useMainstageInpageHeaderTight } from '@/hooks/useMainstageInpageHeaderTight';
import { useInpageScrollViewport } from '@/hooks/useInpageScrollViewport';
import { useAlbumGridBrowseFilters, type AlbumGridBrowseSnapshot } from '@/features/album/hooks/useAlbumGridBrowseFilters';
import { useAlbumBrowseScrollRestore } from '@/features/album/hooks/useAlbumBrowseScrollRestore';
import { useAlbumBrowseScrollSnapshotSync, type AlbumBrowseScrollSnapshot } from '@/features/album/hooks/useAlbumBrowseFilters';
import { readAlbumBrowseRestore } from '@/utils/navigation/albumDetailNavigation';
import { albumArtistDisplayName } from '@/features/album/utils/deriveAlbumHeaderArtistRefs';
const ALBUM_COUNT = 30;
/** Extra pool when mix rating filter is on so we can still fill the grid after filtering. */
@@ -1,5 +1,5 @@
import { describe, expect, it, beforeEach } from 'vitest';
import type { SubsonicAlbum } from '../api/subsonicTypes';
import type { SubsonicAlbum } from '@/api/subsonicTypes';
import {
DEFAULT_ALBUM_BROWSE_RETURN_FILTERS,
DEFAULT_ALBUM_BROWSE_SORT,
@@ -16,7 +16,7 @@ import {
peekGenreDetailScrollRestore,
stashGenreDetailReturnFilters,
useAlbumBrowseSessionStore,
} from './albumBrowseSessionStore';
} from '@/features/album/store/albumBrowseSessionStore';
describe('albumBrowseSessionStore', () => {
beforeEach(() => {
@@ -1,6 +1,6 @@
import { create } from 'zustand';
import type { SubsonicAlbum } from '../api/subsonicTypes';
import type { AlbumBrowseSort } from '../utils/library/browseTextSearch';
import type { SubsonicAlbum } from '@/api/subsonicTypes';
import type { AlbumBrowseSort } from '@/utils/library/browseTextSearch';
export const DEFAULT_ALBUM_BROWSE_SORT: AlbumBrowseSort = 'alphabeticalByName';
@@ -1,4 +1,4 @@
import type { ColDef } from '../useTracklistColumns';
import type { ColDef } from '@/utils/useTracklistColumns';
export function codecLabel(song: { suffix?: string; bitRate?: number }, showBitrate: boolean): string {
const parts: string[] = [];
@@ -3,8 +3,8 @@ import {
albumArtistDisplayName,
deriveAlbumArtistRefs,
deriveAlbumHeaderArtistRefs,
} from './deriveAlbumHeaderArtistRefs';
import type { SubsonicAlbum } from '../../api/subsonicTypes';
} from '@/features/album/utils/deriveAlbumHeaderArtistRefs';
import type { SubsonicAlbum } from '@/api/subsonicTypes';
import { makeSubsonicSong } from '@/test/helpers/factories';
const baseAlbum = (): SubsonicAlbum => ({
@@ -1,4 +1,4 @@
import type { SubsonicAlbum, SubsonicOpenArtistRef, SubsonicSong } from '../../api/subsonicTypes';
import type { SubsonicAlbum, SubsonicOpenArtistRef, SubsonicSong } from '@/api/subsonicTypes';
import { coerceOpenArtistRefs } from '@/features/artist';
function nonEmpty(refs: SubsonicOpenArtistRef[]): refs is SubsonicOpenArtistRef[] {
@@ -1,9 +1,9 @@
import type { SubsonicAlbum } from '../../api/subsonicTypes';
import type { SubsonicAlbum } from '@/api/subsonicTypes';
import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { coverArtRef } from '../../cover/ref';
import { loadCoverBlobForExport } from '../../cover/integrations/export';
import PsysonicLogo from '../../components/PsysonicLogo';
import { coverArtRef } from '@/cover/ref';
import { loadCoverBlobForExport } from '@/cover/integrations/export';
import PsysonicLogo from '@/components/PsysonicLogo';
export type ExportFormat = 'story' | 'square' | 'twitter';
export type ExportGridSize = 3 | 4 | 5;
@@ -1,11 +1,11 @@
import { getAlbumList } from '../../api/subsonicLibrary';
import { coverArtRef } from '../../cover/ref';
import { loadCoverBlobForExport } from '../../cover/integrations/export';
import type { SubsonicAlbum } from '../../api/subsonicTypes';
import { albumArtistDisplayName } from '../album/deriveAlbumHeaderArtistRefs';
import { getAlbumList } from '@/api/subsonicLibrary';
import { coverArtRef } from '@/cover/ref';
import { loadCoverBlobForExport } from '@/cover/integrations/export';
import type { SubsonicAlbum } from '@/api/subsonicTypes';
import { albumArtistDisplayName } from '@/features/album/utils/deriveAlbumHeaderArtistRefs';
import { writeFile } from '@tauri-apps/plugin-fs';
import { downloadDir, join } from '@tauri-apps/api/path';
import { useAuthStore } from '../../store/authStore';
import { useAuthStore } from '@/store/authStore';
// Catppuccin Macchiato palette
const M = {
crust: '#181926',
@@ -1,6 +1,6 @@
import React, { useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useAlbumDetailBack } from '@/hooks/useAlbumDetailBack';
import { useAlbumDetailBack } from '@/features/album';
import {
ArrowLeft, Camera, Check, HardDriveDownload, Heart,
Loader2, Play, Radio, Share2, Shuffle, Users,
@@ -8,7 +8,7 @@ import {
isArtistsBrowsePath,
useArtistBrowseSessionStore,
} from '@/features/artist/store/artistBrowseSessionStore';
import { isArtistDetailPath } from '@/store/albumBrowseSessionStore';
import { isArtistDetailPath } from '@/features/album';
import { shouldRestoreArtistBrowseSession } from '@/utils/navigation/albumDetailNavigation';
import { useLiveSearchScopeStore } from '@/store/liveSearchScopeStore';
+1 -1
View File
@@ -3,7 +3,7 @@ import { useArtistCoverRef } from '@/cover/useLibraryCoverRef';
import type { SubsonicArtist, SubsonicAlbum } from '@/api/subsonicTypes';
import { useEffect, useState, Fragment, useMemo } from 'react';
import { useParams, useSearchParams } from 'react-router-dom';
import AlbumCard from '@/components/AlbumCard';
import { AlbumCard } from '@/features/album';
import { ArrowDownUp } from 'lucide-react';
import { open } from '@tauri-apps/plugin-shell';
import { usePlayerStore } from '@/store/playerStore';
+1 -1
View File
@@ -9,7 +9,7 @@ import { useFavoritesData } from '@/features/favorites/hooks/useFavoritesData';
import { useFavoritesSongFiltering } from '@/features/favorites/hooks/useFavoritesSongFiltering';
import { useFavoritesSelection } from '@/features/favorites/hooks/useFavoritesSelection';
import { useBulkPlPickerOutsideClick } from '@/hooks/useBulkPlPickerOutsideClick';
import AlbumRow from '@/components/AlbumRow';
import { AlbumRow } from '@/features/album';
import { ArtistRow } from '@/features/artist';
import { usePlayerStore } from '@/store/playerStore';
import { useTranslation } from 'react-i18next';
@@ -22,13 +22,13 @@ import {
} from '@/utils/library/libraryDevLog';
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { useNavigateToAlbum } from '@/hooks/useNavigateToAlbum';
import { useNavigateToAlbum } from '@/features/album';
import { Search, Disc3, Users, Music, TextSearch, Database, Globe } from 'lucide-react';
import { usePlayerStore } from '@/store/playerStore';
import { useAuthStore } from '@/store/authStore';
import { useLibraryIndexStore } from '@/store/libraryIndexStore';
import { useTranslation } from 'react-i18next';
import { albumArtistDisplayName } from '@/utils/album/deriveAlbumHeaderArtistRefs';
import { albumArtistDisplayName } from '@/features/album';
import { FETCH_QUEUE_BIAS_SEARCH_ARTIST_OVER_ALBUM } from '@/ui/CachedImage';
import type { SubsonicSong } from '@/api/subsonicTypes';
import { AlbumCoverArtImage } from '@/cover/AlbumCoverArtImage';
@@ -16,7 +16,7 @@ import { ArtistCoverArtImage } from '@/cover/ArtistCoverArtImage';
import { CoverArtImage } from '@/cover/CoverArtImage';
import { albumCoverRefForSong } from '@/cover/ref';
import { showToast } from '@/utils/ui/toast';
import { albumArtistDisplayName } from '@/utils/album/deriveAlbumHeaderArtistRefs';
import { albumArtistDisplayName } from '@/features/album';
import { useShareSearch } from '@/features/search/hooks/useShareSearch';
import ShareSearchResults from '@/features/search/components/ShareSearchResults';
import {
@@ -1,7 +1,7 @@
import type { KeyboardEvent, MouseEvent } from 'react';
import { ALL_NAV_ITEMS } from '@/config/navItems';
import type { LiveSearchScope } from '@/store/liveSearchScopeStore';
import { isAlbumsBrowsePath, isNewReleasesBrowsePath } from '@/store/albumBrowseSessionStore';
import { isAlbumsBrowsePath, isNewReleasesBrowsePath } from '@/features/album';
import { isTracksBrowsePath } from '@/store/advancedSearchSessionStore';
import { isArtistsBrowsePath } from '@/features/artist';
import { isComposersBrowsePath } from '@/store/composerBrowseSessionStore';
@@ -1,6 +1,6 @@
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { isAlbumsBrowsePath, isNewReleasesBrowsePath } from '@/store/albumBrowseSessionStore';
import { isAlbumsBrowsePath, isNewReleasesBrowsePath } from '@/features/album';
import { isArtistsBrowsePath } from '@/features/artist';
import { isTracksBrowsePath } from '@/store/advancedSearchSessionStore';
import { isComposersBrowsePath } from '@/store/composerBrowseSessionStore';
+1 -1
View File
@@ -1,6 +1,6 @@
import { useCallback, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useNavigateToAlbum } from '@/hooks/useNavigateToAlbum';
import { useNavigateToAlbum } from '@/features/album';
import { useTranslation } from 'react-i18next';
import { useAuthStore } from '@/store/authStore';
import {
@@ -6,7 +6,7 @@ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useSta
import { useLocation, useNavigate, useNavigationType, useSearchParams } from 'react-router-dom';
import { SlidersVertical, Search, X } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import AlbumRow from '@/components/AlbumRow';
import { AlbumRow } from '@/features/album';
import { ArtistRow } from '@/features/artist';
import PagedSongList from '@/components/PagedSongList';
import CustomSelect from '@/ui/CustomSelect';
@@ -15,7 +15,7 @@ import { tooltipAttrs } from '@/ui/tooltipAttrs';
import { APP_MAIN_SCROLL_VIEWPORT_ID } from '@/constants/appScroll';
import { useAuthStore } from '@/store/authStore';
import { usePlayerStore } from '@/store/playerStore';
import { isAdvancedSearchLeaveTargetPath } from '@/store/albumBrowseSessionStore';
import { isAdvancedSearchLeaveTargetPath } from '@/features/album';
import {
isAdvancedSearchPath,
isAdvancedSearchPanelPath,
@@ -12,7 +12,7 @@ import {
renderAlbumCardCanvas,
ExportFormat,
ExportGridSize,
} from '@/utils/export/exportAlbumCard';
} from '@/features/album';
interface Props {
open: boolean;
+1 -1
View File
@@ -5,7 +5,7 @@ import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Share2 } from 'lucide-react';
import { formatHumanHoursMinutes } from '@/utils/format/formatHumanDuration';
import AlbumRow from '@/components/AlbumRow';
import { AlbumRow } from '@/features/album';
import StatsExportModal from '@/features/stats/components/StatsExportModal';
import PlayerStatisticsPanel from '@/features/stats/components/PlayerStatisticsPanel';
import StatisticsTabBar from '@/features/stats/components/StatisticsTabBar';
+1 -1
View File
@@ -1,6 +1,6 @@
import { useEffect, useRef, useState, type RefObject } from 'react';
import { useLocation, useNavigationType, type NavigationType } from 'react-router-dom';
import { isComposerDetailPath } from '../store/albumBrowseSessionStore';
import { isComposerDetailPath } from '@/features/album';
import {
DEFAULT_COMPOSER_BROWSE_RETURN_STATE,
type ComposerBrowseReturnState,
+2 -2
View File
@@ -11,9 +11,9 @@ import {
peekGenreDetailScrollRestore,
stashGenreDetailReturnFilters,
useAlbumBrowseSessionStore,
} from '../store/albumBrowseSessionStore';
} from '@/features/album';
import { shouldRestoreAlbumBrowseSession } from '../utils/navigation/albumDetailNavigation';
import type { AlbumBrowseScrollSnapshot } from './useAlbumBrowseFilters';
import type { AlbumBrowseScrollSnapshot } from '@/features/album';
/** Genre detail: locked genre filter + leave/restore session (same contract as All Albums). */
export function useGenreDetailBrowse(
+1 -1
View File
@@ -4,7 +4,7 @@ import type { SubsonicArtist, SubsonicAlbum, SubsonicArtistInfo } from '../api/s
import { useEffect, useState, useMemo } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { ndListAlbumsByArtistRole } from '../api/navidromeBrowse';
import AlbumCard from '../components/AlbumCard';
import { AlbumCard } from '@/features/album';
import { ArtistHeroCover } from '../cover/artistHero';
import { coverArtRef } from '../cover/ref';
import { useCoverLightboxSrc } from '../cover/lightbox';
+4 -4
View File
@@ -2,16 +2,16 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useLocation, useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { ArrowLeft, Disc3, Play, ListPlus, Loader2 } from 'lucide-react';
import AlbumCard from '../components/AlbumCard';
import { AlbumCard } from '@/features/album';
import { LongPressWaveOverlay } from '../components/LongPressWaveOverlay';
import InpageScrollSentinel from '../components/InpageScrollSentinel';
import OverlayScrollArea from '@/ui/OverlayScrollArea';
import { VirtualCardGrid } from '../components/VirtualCardGrid';
import { GENRE_DETAIL_INPAGE_SCROLL_VIEWPORT_ID } from '../constants/appScroll';
import { albumGridWarmCovers } from '../cover/layoutSizes';
import { useAlbumBrowseScrollSnapshotSync, type AlbumBrowseScrollSnapshot } from '../hooks/useAlbumBrowseFilters';
import { useGenreAlbumBrowse } from '../hooks/useGenreAlbumBrowse';
import { useAlbumBrowseScrollRestore } from '../hooks/useAlbumBrowseScrollRestore';
import { useAlbumBrowseScrollSnapshotSync, type AlbumBrowseScrollSnapshot } from '@/features/album';
import { useGenreAlbumBrowse } from '@/features/album';
import { useAlbumBrowseScrollRestore } from '@/features/album';
import { useGenreDetailBrowse } from '../hooks/useGenreDetailBrowse';
import { useInpageScrollViewport } from '../hooks/useInpageScrollViewport';
import { useLongPressAction } from '../hooks/useLongPressAction';
+2 -2
View File
@@ -4,10 +4,10 @@ import type { SubsonicAlbum, SubsonicArtist, SubsonicSong } from '../api/subsoni
import { runLocalRandomSongs } from '../utils/library/browseTextSearch';
import React, { useEffect, useState } from 'react';
import Hero from '../components/Hero';
import AlbumRow from '../components/AlbumRow';
import { AlbumRow } from '@/features/album';
import SongRail from '../components/SongRail';
import BecauseYouLikeRail from '../components/BecauseYouLikeRail';
import LosslessAlbumsRail from '../components/LosslessAlbumsRail';
import { LosslessAlbumsRail } from '@/features/album';
import { useTranslation } from 'react-i18next';
import { NavLink, useNavigate } from 'react-router-dom';
import { ChevronRight } from 'lucide-react';
+1 -1
View File
@@ -13,7 +13,7 @@ import { playAlbum, playAlbumShuffled } from '../utils/playback/playAlbum';
import { useLongPressAction } from '../hooks/useLongPressAction';
import { LongPressWaveOverlay } from '../components/LongPressWaveOverlay';
import { useTranslation } from 'react-i18next';
import { albumArtistDisplayName } from '../utils/album/deriveAlbumHeaderArtistRefs';
import { albumArtistDisplayName } from '@/features/album';
const PAGE_SIZE = 50;
+7 -7
View File
@@ -7,7 +7,7 @@ import { dedupeById } from '../utils/dedupeById';
import { useEffect, useLayoutEffect, useState, useCallback, useRef, useMemo } from 'react';
import { Download, HardDriveDownload } from 'lucide-react';
import SelectionToggleButton from '../components/SelectionToggleButton';
import AlbumCard from '../components/AlbumCard';
import { AlbumCard } from '@/features/album';
import GenreFilterBar from '../components/GenreFilterBar';
import { useTranslation } from 'react-i18next';
import { useLocation, useNavigate } from 'react-router-dom';
@@ -29,13 +29,13 @@ import { useAsyncInpagePagination } from '../hooks/useAsyncInpagePagination';
import { useInpageScrollSentinel } from '../hooks/useInpageScrollSentinel';
import { useInpageScrollViewport } from '../hooks/useInpageScrollViewport';
import InpageScrollSentinel from '../components/InpageScrollSentinel';
import { useAlbumGridBrowseFilters, type AlbumGridBrowseSnapshot } from '../hooks/useAlbumGridBrowseFilters';
import { useAlbumBrowseScrollRestore } from '../hooks/useAlbumBrowseScrollRestore';
import { useAlbumBrowseScrollReset } from '../hooks/useAlbumBrowseScrollReset';
import { useBrowseAlbumTextSearch } from '../hooks/useBrowseAlbumTextSearch';
import { useAlbumBrowseScrollSnapshotSync, type AlbumBrowseScrollSnapshot } from '../hooks/useAlbumBrowseFilters';
import { useAlbumGridBrowseFilters, type AlbumGridBrowseSnapshot } from '@/features/album';
import { useAlbumBrowseScrollRestore } from '@/features/album';
import { useAlbumBrowseScrollReset } from '@/features/album';
import { useBrowseAlbumTextSearch } from '@/features/album';
import { useAlbumBrowseScrollSnapshotSync, type AlbumBrowseScrollSnapshot } from '@/features/album';
import { readAlbumBrowseRestore } from '../utils/navigation/albumDetailNavigation';
import { albumArtistDisplayName } from '../utils/album/deriveAlbumHeaderArtistRefs';
import { albumArtistDisplayName } from '@/features/album';
import { useLibraryIndexStore } from '../store/libraryIndexStore';
import { filterAlbumsByGenres } from '../utils/library/albumBrowseFilters';
import { useScopedBrowseSearchQuery } from '../store/liveSearchScopeStore';
+1 -1
View File
@@ -8,7 +8,7 @@ import { useShallow } from 'zustand/react/shallow';
import { usePlaylistStore } from '../store/playlistStore';
import { useOfflineStore } from '@/features/offline';
import { useLocalPlaybackStore } from '../store/localPlaybackStore';
import { useAlbumOfflineState } from '../hooks/useAlbumOfflineState';
import { useAlbumOfflineState } from '@/features/album';
import { useAuthStore } from '../store/authStore';
import { useDownloadModalStore } from '@/features/offline';
import { useZipDownloadStore } from '@/features/offline';
+1 -1
View File
@@ -47,7 +47,7 @@ vi.mock('@/features/artist', () => ({
getSimilarSongs2: vi.fn(async () => []),
getTopSongs: vi.fn(async () => []),
}));
vi.mock('@/api/subsonicAlbumInfo', () => ({
vi.mock('@/features/album', () => ({
getAlbumInfo2: vi.fn(async () => null),
}));
vi.mock('@/api/subsonicScrobble', () => ({
@@ -7,7 +7,7 @@ import {
isAlbumDetailPath,
isArtistDetailPath,
isComposerDetailPath,
} from '../../store/albumBrowseSessionStore';
} from '@/features/album';
import {
peekPersistedAdvancedSearchLeaveSnapshot,
saveAdvancedSearchLeaveSnapshot,