From 2a425862aee33b7af3580188d37c0e52ff2f1570 Mon Sep 17 00:00:00 2001 From: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com> Date: Tue, 30 Jun 2026 01:33:29 +0200 Subject: [PATCH] refactor(album): co-locate album feature into features/album --- src/api/subsonic.async.test.ts | 2 +- src/app/AppRoutes.tsx | 10 +-- src/app/MainApp.tsx | 2 +- src/components/BecauseYouLikeRail.tsx | 4 +- src/components/Hero.tsx | 4 +- src/components/SongCard.tsx | 2 +- src/components/SongRow.tsx | 2 +- .../contextMenu/SongContextItems.tsx | 2 +- src/components/playlist/PlaylistHero.tsx | 2 +- src/components/tracks/TracksPageChrome.tsx | 2 +- src/cover/warmDiskPeek.ts | 2 +- .../album}/api/subsonicAlbumInfo.ts | 4 +- .../album}/components/AlbumCard.tsx | 40 +++++----- .../album/components}/AlbumDetailToolbar.tsx | 4 +- .../album}/components/AlbumHeader.test.tsx | 20 ++--- .../album}/components/AlbumHeader.tsx | 32 ++++---- .../album}/components/AlbumRow.tsx | 8 +- .../album}/components/AlbumTrackList.tsx | 26 +++---- .../components}/AlbumTrackListMobile.tsx | 8 +- .../album}/components/LosslessAlbumsRail.tsx | 14 ++-- .../album/components}/TrackRow.tsx | 26 +++---- .../album/components}/TracklistHeaderRow.tsx | 4 +- .../album}/hooks/useAlbumBrowseData.test.ts | 2 +- .../album}/hooks/useAlbumBrowseData.ts | 26 +++---- .../album}/hooks/useAlbumBrowseFilters.ts | 10 +-- .../album}/hooks/useAlbumBrowseScrollReset.ts | 2 +- .../hooks/useAlbumBrowseScrollRestore.ts | 4 +- .../album}/hooks/useAlbumCatalogYearBounds.ts | 4 +- .../album}/hooks/useAlbumDetailBack.test.tsx | 8 +- .../album}/hooks/useAlbumDetailBack.ts | 2 +- .../album}/hooks/useAlbumDetailData.ts | 10 +-- .../album}/hooks/useAlbumDetailSort.ts | 2 +- .../album}/hooks/useAlbumGridBrowseFilters.ts | 12 +-- .../album}/hooks/useAlbumOfflineState.test.ts | 2 +- .../album}/hooks/useAlbumOfflineState.ts | 2 +- .../hooks/useAlbumTrackListSelection.ts | 8 +- .../album}/hooks/useBrowseAlbumTextSearch.ts | 4 +- .../album}/hooks/useGenreAlbumBrowse.ts | 12 +-- .../album}/hooks/useNavigateToAlbum.ts | 2 +- src/features/album/index.ts | 40 ++++++++++ .../album}/pages/AlbumDetail.tsx | 62 +++++++-------- src/{ => features/album}/pages/Albums.tsx | 78 +++++++++---------- .../album}/pages/LabelAlbums.tsx | 14 ++-- .../album}/pages/LosslessAlbums.tsx | 48 ++++++------ .../album}/pages/RandomAlbums.tsx | 54 ++++++------- .../store/albumBrowseSessionStore.test.ts | 4 +- .../album}/store/albumBrowseSessionStore.ts | 4 +- .../album/utils}/albumDetailHelpers.ts | 0 .../album}/utils/albumRecency.ts | 0 .../album/utils}/albumTrackListHelpers.ts | 2 +- .../deriveAlbumHeaderArtistRefs.test.ts | 4 +- .../utils}/deriveAlbumHeaderArtistRefs.ts | 2 +- .../album/utils}/exportAlbumCard.ts | 8 +- .../album/utils}/exportNewAlbums.ts | 12 +-- .../artist/components/ArtistDetailHero.tsx | 2 +- .../artist/hooks/useArtistsBrowseFilters.ts | 2 +- src/features/artist/pages/ArtistDetail.tsx | 2 +- src/features/favorites/pages/Favorites.tsx | 2 +- src/features/search/components/LiveSearch.tsx | 4 +- .../search/components/MobileSearchOverlay.tsx | 2 +- .../search/components/liveSearchScope.ts | 2 +- .../search/hooks/useLiveSearchRouteScope.ts | 2 +- src/features/search/hooks/useShareSearch.ts | 2 +- .../search/pages/SearchBrowsePage.tsx | 4 +- .../stats/components/StatsExportModal.tsx | 2 +- src/features/stats/pages/Statistics.tsx | 2 +- src/hooks/useComposersBrowseFilters.ts | 2 +- src/hooks/useGenreDetailBrowse.ts | 4 +- src/pages/ComposerDetail.tsx | 2 +- src/pages/GenreDetail.tsx | 8 +- src/pages/Home.tsx | 4 +- src/pages/MostPlayed.tsx | 2 +- src/pages/NewReleases.tsx | 14 ++-- src/pages/PlaylistDetail.tsx | 2 +- src/store/playerStore.persistence.test.ts | 2 +- src/utils/navigation/albumDetailNavigation.ts | 2 +- 76 files changed, 387 insertions(+), 347 deletions(-) rename src/{ => features/album}/api/subsonicAlbumInfo.ts (72%) rename src/{ => features/album}/components/AlbumCard.tsx (87%) rename src/{components/albumDetail => features/album/components}/AlbumDetailToolbar.tsx (96%) rename src/{ => features/album}/components/AlbumHeader.test.tsx (85%) rename src/{ => features/album}/components/AlbumHeader.tsx (96%) rename src/{ => features/album}/components/AlbumRow.tsx (97%) rename src/{ => features/album}/components/AlbumTrackList.tsx (88%) rename src/{components/albumTrackList => features/album/components}/AlbumTrackListMobile.tsx (91%) rename src/{ => features/album}/components/LosslessAlbumsRail.tsx (79%) rename src/{components/albumTrackList => features/album/components}/TrackRow.tsx (91%) rename src/{components/albumTrackList => features/album/components}/TracklistHeaderRow.tsx (97%) rename src/{ => features/album}/hooks/useAlbumBrowseData.test.ts (94%) rename src/{ => features/album}/hooks/useAlbumBrowseData.ts (95%) rename src/{ => features/album}/hooks/useAlbumBrowseFilters.ts (95%) rename src/{ => features/album}/hooks/useAlbumBrowseScrollReset.ts (91%) rename src/{ => features/album}/hooks/useAlbumBrowseScrollRestore.ts (96%) rename src/{ => features/album}/hooks/useAlbumCatalogYearBounds.ts (84%) rename src/{ => features/album}/hooks/useAlbumDetailBack.test.tsx (83%) rename src/{ => features/album}/hooks/useAlbumDetailBack.ts (96%) rename src/{ => features/album}/hooks/useAlbumDetailData.ts (95%) rename src/{ => features/album}/hooks/useAlbumDetailSort.ts (98%) rename src/{ => features/album}/hooks/useAlbumGridBrowseFilters.ts (91%) rename src/{ => features/album}/hooks/useAlbumOfflineState.test.ts (94%) rename src/{ => features/album}/hooks/useAlbumOfflineState.ts (97%) rename src/{ => features/album}/hooks/useAlbumTrackListSelection.ts (94%) rename src/{ => features/album}/hooks/useBrowseAlbumTextSearch.ts (96%) rename src/{ => features/album}/hooks/useGenreAlbumBrowse.ts (94%) rename src/{ => features/album}/hooks/useNavigateToAlbum.ts (84%) create mode 100644 src/features/album/index.ts rename src/{ => features/album}/pages/AlbumDetail.tsx (88%) rename src/{ => features/album}/pages/Albums.tsx (88%) rename src/{ => features/album}/pages/LabelAlbums.tsx (87%) rename src/{ => features/album}/pages/LosslessAlbums.tsx (90%) rename src/{ => features/album}/pages/RandomAlbums.tsx (90%) rename src/{ => features/album}/store/albumBrowseSessionStore.test.ts (98%) rename src/{ => features/album}/store/albumBrowseSessionStore.ts (98%) rename src/{utils/componentHelpers => features/album/utils}/albumDetailHelpers.ts (100%) rename src/{ => features/album}/utils/albumRecency.ts (100%) rename src/{utils/componentHelpers => features/album/utils}/albumTrackListHelpers.ts (97%) rename src/{utils/album => features/album/utils}/deriveAlbumHeaderArtistRefs.test.ts (96%) rename src/{utils/album => features/album/utils}/deriveAlbumHeaderArtistRefs.ts (98%) rename src/{utils/export => features/album/utils}/exportAlbumCard.ts (98%) rename src/{utils/export => features/album/utils}/exportNewAlbums.ts (96%) diff --git a/src/api/subsonic.async.test.ts b/src/api/subsonic.async.test.ts index cad59075..3680f3bd 100644 --- a/src/api/subsonic.async.test.ts +++ b/src/api/subsonic.async.test.ts @@ -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'; diff --git a/src/app/AppRoutes.tsx b/src/app/AppRoutes.tsx index 4b418f99..d22d07d1 100644 --- a/src/app/AppRoutes.tsx +++ b/src/app/AppRoutes.tsx @@ -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')); diff --git a/src/app/MainApp.tsx b/src/app/MainApp.tsx index a493480a..a960f51f 100644 --- a/src/app/MainApp.tsx +++ b/src/app/MainApp.tsx @@ -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)` : ''; diff --git a/src/components/BecauseYouLikeRail.tsx b/src/components/BecauseYouLikeRail.tsx index 4bfdf938..f6d4f989 100644 --- a/src/components/BecauseYouLikeRail.tsx +++ b/src/components/BecauseYouLikeRail.tsx @@ -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:'; diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx index 1c2717d7..0760ea57 100644 --- a/src/components/Hero.tsx +++ b/src/components/Hero.tsx @@ -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; diff --git a/src/components/SongCard.tsx b/src/components/SongCard.tsx index 3d0c0490..8e9f0579 100644 --- a/src/components/SongCard.tsx +++ b/src/components/SongCard.tsx @@ -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'; diff --git a/src/components/SongRow.tsx b/src/components/SongRow.tsx index 7af2bf76..b23b2c52 100644 --- a/src/components/SongRow.tsx +++ b/src/components/SongRow.tsx @@ -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'; diff --git a/src/components/contextMenu/SongContextItems.tsx b/src/components/contextMenu/SongContextItems.tsx index 066c44de..aa0433d3 100644 --- a/src/components/contextMenu/SongContextItems.tsx +++ b/src/components/contextMenu/SongContextItems.tsx @@ -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'; diff --git a/src/components/playlist/PlaylistHero.tsx b/src/components/playlist/PlaylistHero.tsx index eda40d97..5facfd04 100644 --- a/src/components/playlist/PlaylistHero.tsx +++ b/src/components/playlist/PlaylistHero.tsx @@ -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'; diff --git a/src/components/tracks/TracksPageChrome.tsx b/src/components/tracks/TracksPageChrome.tsx index c9a7a5d9..ffb45801 100644 --- a/src/components/tracks/TracksPageChrome.tsx +++ b/src/components/tracks/TracksPageChrome.tsx @@ -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'; diff --git a/src/cover/warmDiskPeek.ts b/src/cover/warmDiskPeek.ts index 6f65862e..93097c4d 100644 --- a/src/cover/warmDiskPeek.ts +++ b/src/cover/warmDiskPeek.ts @@ -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 = { diff --git a/src/api/subsonicAlbumInfo.ts b/src/features/album/api/subsonicAlbumInfo.ts similarity index 72% rename from src/api/subsonicAlbumInfo.ts rename to src/features/album/api/subsonicAlbumInfo.ts index 7686e2c6..0fae46ab 100644 --- a/src/api/subsonicAlbumInfo.ts +++ b/src/features/album/api/subsonicAlbumInfo.ts @@ -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 { try { diff --git a/src/components/AlbumCard.tsx b/src/features/album/components/AlbumCard.tsx similarity index 87% rename from src/components/AlbumCard.tsx rename to src/features/album/components/AlbumCard.tsx index 67f93556..18d92a35 100644 --- a/src/components/AlbumCard.tsx +++ b/src/features/album/components/AlbumCard.tsx @@ -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; diff --git a/src/components/albumDetail/AlbumDetailToolbar.tsx b/src/features/album/components/AlbumDetailToolbar.tsx similarity index 96% rename from src/components/albumDetail/AlbumDetailToolbar.tsx rename to src/features/album/components/AlbumDetailToolbar.tsx index 10149d9f..15fe4e8b 100644 --- a/src/components/albumDetail/AlbumDetailToolbar.tsx +++ b/src/features/album/components/AlbumDetailToolbar.tsx @@ -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 { diff --git a/src/components/AlbumHeader.test.tsx b/src/features/album/components/AlbumHeader.test.tsx similarity index 85% rename from src/components/AlbumHeader.test.tsx rename to src/features/album/components/AlbumHeader.test.tsx index f7c489c6..8dea49ab 100644 --- a/src/components/AlbumHeader.test.tsx +++ b/src/features/album/components/AlbumHeader.test.tsx @@ -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 { diff --git a/src/components/AlbumHeader.tsx b/src/features/album/components/AlbumHeader.tsx similarity index 96% rename from src/components/AlbumHeader.tsx rename to src/features/album/components/AlbumHeader.tsx index b477169f..60db1e87 100644 --- a/src/components/AlbumHeader.tsx +++ b/src/features/album/components/AlbumHeader.tsx @@ -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. diff --git a/src/components/AlbumRow.tsx b/src/features/album/components/AlbumRow.tsx similarity index 97% rename from src/components/AlbumRow.tsx rename to src/features/album/components/AlbumRow.tsx index b156a0f3..142f5588 100644 --- a/src/components/AlbumRow.tsx +++ b/src/features/album/components/AlbumRow.tsx @@ -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; diff --git a/src/components/AlbumTrackList.tsx b/src/features/album/components/AlbumTrackList.tsx similarity index 88% rename from src/components/AlbumTrackList.tsx rename to src/features/album/components/AlbumTrackList.tsx index dc11df1e..46364176 100644 --- a/src/components/AlbumTrackList.tsx +++ b/src/features/album/components/AlbumTrackList.tsx @@ -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[]; diff --git a/src/components/albumTrackList/AlbumTrackListMobile.tsx b/src/features/album/components/AlbumTrackListMobile.tsx similarity index 91% rename from src/components/albumTrackList/AlbumTrackListMobile.tsx rename to src/features/album/components/AlbumTrackListMobile.tsx index 9aac94dc..b67f5ce4 100644 --- a/src/components/albumTrackList/AlbumTrackListMobile.tsx +++ b/src/features/album/components/AlbumTrackListMobile.tsx @@ -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[]; diff --git a/src/components/LosslessAlbumsRail.tsx b/src/features/album/components/LosslessAlbumsRail.tsx similarity index 79% rename from src/components/LosslessAlbumsRail.tsx rename to src/features/album/components/LosslessAlbumsRail.tsx index 1d90d0ba..2f147585 100644 --- a/src/components/LosslessAlbumsRail.tsx +++ b/src/features/album/components/LosslessAlbumsRail.tsx @@ -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; diff --git a/src/components/albumTrackList/TrackRow.tsx b/src/features/album/components/TrackRow.tsx similarity index 91% rename from src/components/albumTrackList/TrackRow.tsx rename to src/features/album/components/TrackRow.tsx index 69c50213..b1c6dc35 100644 --- a/src/components/albumTrackList/TrackRow.tsx +++ b/src/features/album/components/TrackRow.tsx @@ -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, diff --git a/src/components/albumTrackList/TracklistHeaderRow.tsx b/src/features/album/components/TracklistHeaderRow.tsx similarity index 97% rename from src/components/albumTrackList/TracklistHeaderRow.tsx rename to src/features/album/components/TracklistHeaderRow.tsx index fa744212..de3bcb44 100644 --- a/src/components/albumTrackList/TracklistHeaderRow.tsx +++ b/src/features/album/components/TracklistHeaderRow.tsx @@ -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[]; diff --git a/src/hooks/useAlbumBrowseData.test.ts b/src/features/album/hooks/useAlbumBrowseData.test.ts similarity index 94% rename from src/hooks/useAlbumBrowseData.test.ts rename to src/features/album/hooks/useAlbumBrowseData.test.ts index 523dbdad..a76412d4 100644 --- a/src/hooks/useAlbumBrowseData.test.ts +++ b/src/features/album/hooks/useAlbumBrowseData.test.ts @@ -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( diff --git a/src/hooks/useAlbumBrowseData.ts b/src/features/album/hooks/useAlbumBrowseData.ts similarity index 95% rename from src/hooks/useAlbumBrowseData.ts rename to src/features/album/hooks/useAlbumBrowseData.ts index 9a509032..64261f51 100644 --- a/src/hooks/useAlbumBrowseData.ts +++ b/src/features/album/hooks/useAlbumBrowseData.ts @@ -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; diff --git a/src/hooks/useAlbumBrowseFilters.ts b/src/features/album/hooks/useAlbumBrowseFilters.ts similarity index 95% rename from src/hooks/useAlbumBrowseFilters.ts rename to src/features/album/hooks/useAlbumBrowseFilters.ts index c5a556e5..3afb84d8 100644 --- a/src/hooks/useAlbumBrowseFilters.ts +++ b/src/features/album/hooks/useAlbumBrowseFilters.ts @@ -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'; diff --git a/src/hooks/useAlbumBrowseScrollReset.ts b/src/features/album/hooks/useAlbumBrowseScrollReset.ts similarity index 91% rename from src/hooks/useAlbumBrowseScrollReset.ts rename to src/features/album/hooks/useAlbumBrowseScrollReset.ts index f95752e0..bb25215b 100644 --- a/src/hooks/useAlbumBrowseScrollReset.ts +++ b/src/features/album/hooks/useAlbumBrowseScrollReset.ts @@ -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; diff --git a/src/hooks/useAlbumBrowseScrollRestore.ts b/src/features/album/hooks/useAlbumBrowseScrollRestore.ts similarity index 96% rename from src/hooks/useAlbumBrowseScrollRestore.ts rename to src/features/album/hooks/useAlbumBrowseScrollRestore.ts index 32a7e907..b42a3b94 100644 --- a/src/hooks/useAlbumBrowseScrollRestore.ts +++ b/src/features/album/hooks/useAlbumBrowseScrollRestore.ts @@ -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; diff --git a/src/hooks/useAlbumCatalogYearBounds.ts b/src/features/album/hooks/useAlbumCatalogYearBounds.ts similarity index 84% rename from src/hooks/useAlbumCatalogYearBounds.ts rename to src/features/album/hooks/useAlbumCatalogYearBounds.ts index 29bf6b32..7a80abb6 100644 --- a/src/hooks/useAlbumCatalogYearBounds.ts +++ b/src/features/album/hooks/useAlbumCatalogYearBounds.ts @@ -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 }; diff --git a/src/hooks/useAlbumDetailBack.test.tsx b/src/features/album/hooks/useAlbumDetailBack.test.tsx similarity index 83% rename from src/hooks/useAlbumDetailBack.test.tsx rename to src/features/album/hooks/useAlbumDetailBack.test.tsx index d063f88a..7ddc737e 100644 --- a/src/hooks/useAlbumDetailBack.test.tsx +++ b/src/features/album/hooks/useAlbumDetailBack.test.tsx @@ -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(); +vi.mock('@/utils/navigation/albumDetailNavigation', async (importOriginal) => { + const mod = await importOriginal(); return { ...mod, navigateAlbumDetailBack: vi.fn(mod.navigateAlbumDetailBack), diff --git a/src/hooks/useAlbumDetailBack.ts b/src/features/album/hooks/useAlbumDetailBack.ts similarity index 96% rename from src/hooks/useAlbumDetailBack.ts rename to src/features/album/hooks/useAlbumDetailBack.ts index 91e343f2..26e07a06 100644 --- a/src/hooks/useAlbumDetailBack.ts +++ b/src/features/album/hooks/useAlbumDetailBack.ts @@ -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 = '/') { diff --git a/src/hooks/useAlbumDetailData.ts b/src/features/album/hooks/useAlbumDetailData.ts similarity index 95% rename from src/hooks/useAlbumDetailData.ts rename to src/features/album/hooks/useAlbumDetailData.ts index 76b064c3..9c6c1e20 100644 --- a/src/hooks/useAlbumDetailData.ts +++ b/src/features/album/hooks/useAlbumDetailData.ts @@ -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; diff --git a/src/hooks/useAlbumDetailSort.ts b/src/features/album/hooks/useAlbumDetailSort.ts similarity index 98% rename from src/hooks/useAlbumDetailSort.ts rename to src/features/album/hooks/useAlbumDetailSort.ts index ae0a5db7..6b475464 100644 --- a/src/hooks/useAlbumDetailSort.ts +++ b/src/features/album/hooks/useAlbumDetailSort.ts @@ -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'; diff --git a/src/hooks/useAlbumGridBrowseFilters.ts b/src/features/album/hooks/useAlbumGridBrowseFilters.ts similarity index 91% rename from src/hooks/useAlbumGridBrowseFilters.ts rename to src/features/album/hooks/useAlbumGridBrowseFilters.ts index 7e288499..720daad8 100644 --- a/src/hooks/useAlbumGridBrowseFilters.ts +++ b/src/features/album/hooks/useAlbumGridBrowseFilters.ts @@ -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[]; diff --git a/src/hooks/useAlbumOfflineState.test.ts b/src/features/album/hooks/useAlbumOfflineState.test.ts similarity index 94% rename from src/hooks/useAlbumOfflineState.test.ts rename to src/features/album/hooks/useAlbumOfflineState.test.ts index d288bbaa..f4fbb403 100644 --- a/src/hooks/useAlbumOfflineState.test.ts +++ b/src/features/album/hooks/useAlbumOfflineState.test.ts @@ -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(() => { diff --git a/src/hooks/useAlbumOfflineState.ts b/src/features/album/hooks/useAlbumOfflineState.ts similarity index 97% rename from src/hooks/useAlbumOfflineState.ts rename to src/features/album/hooks/useAlbumOfflineState.ts index 3f2c79dd..cf7c0903 100644 --- a/src/hooks/useAlbumOfflineState.ts +++ b/src/features/album/hooks/useAlbumOfflineState.ts @@ -1,4 +1,4 @@ -import { useLocalPlaybackStore } from '../store/localPlaybackStore'; +import { useLocalPlaybackStore } from '@/store/localPlaybackStore'; import { useOfflineJobStore } from '@/features/offline'; import { isOfflinePinComplete } from '@/features/offline'; diff --git a/src/hooks/useAlbumTrackListSelection.ts b/src/features/album/hooks/useAlbumTrackListSelection.ts similarity index 94% rename from src/hooks/useAlbumTrackListSelection.ts rename to src/features/album/hooks/useAlbumTrackListSelection.ts index c5bf1eec..bde46042 100644 --- a/src/hooks/useAlbumTrackListSelection.ts +++ b/src/features/album/hooks/useAlbumTrackListSelection.ts @@ -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[]; diff --git a/src/hooks/useBrowseAlbumTextSearch.ts b/src/features/album/hooks/useBrowseAlbumTextSearch.ts similarity index 96% rename from src/hooks/useBrowseAlbumTextSearch.ts rename to src/features/album/hooks/useBrowseAlbumTextSearch.ts index d1c80b43..7842dd0f 100644 --- a/src/hooks/useBrowseAlbumTextSearch.ts +++ b/src/features/album/hooks/useBrowseAlbumTextSearch.ts @@ -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'; diff --git a/src/hooks/useGenreAlbumBrowse.ts b/src/features/album/hooks/useGenreAlbumBrowse.ts similarity index 94% rename from src/hooks/useGenreAlbumBrowse.ts rename to src/features/album/hooks/useGenreAlbumBrowse.ts index 991b0e90..9dd15fe6 100644 --- a/src/hooks/useGenreAlbumBrowse.ts +++ b/src/features/album/hooks/useGenreAlbumBrowse.ts @@ -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; diff --git a/src/hooks/useNavigateToAlbum.ts b/src/features/album/hooks/useNavigateToAlbum.ts similarity index 84% rename from src/hooks/useNavigateToAlbum.ts rename to src/features/album/hooks/useNavigateToAlbum.ts index 1b802ea7..203044eb 100644 --- a/src/hooks/useNavigateToAlbum.ts +++ b/src/features/album/hooks/useNavigateToAlbum.ts @@ -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() { diff --git a/src/features/album/index.ts b/src/features/album/index.ts new file mode 100644 index 00000000..c063e08e --- /dev/null +++ b/src/features/album/index.ts @@ -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'; diff --git a/src/pages/AlbumDetail.tsx b/src/features/album/pages/AlbumDetail.tsx similarity index 88% rename from src/pages/AlbumDetail.tsx rename to src/features/album/pages/AlbumDetail.tsx index 92c82e26..0e5d3ec6 100644 --- a/src/pages/AlbumDetail.tsx +++ b/src/features/album/pages/AlbumDetail.tsx @@ -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'; diff --git a/src/pages/Albums.tsx b/src/features/album/pages/Albums.tsx similarity index 88% rename from src/pages/Albums.tsx rename to src/features/album/pages/Albums.tsx index ff0d6a42..6650677c 100644 --- a/src/pages/Albums.tsx +++ b/src/features/album/pages/Albums.tsx @@ -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; diff --git a/src/pages/LabelAlbums.tsx b/src/features/album/pages/LabelAlbums.tsx similarity index 87% rename from src/pages/LabelAlbums.tsx rename to src/features/album/pages/LabelAlbums.tsx index 5982ea57..b08aae0c 100644 --- a/src/pages/LabelAlbums.tsx +++ b/src/features/album/pages/LabelAlbums.tsx @@ -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(); diff --git a/src/pages/LosslessAlbums.tsx b/src/features/album/pages/LosslessAlbums.tsx similarity index 90% rename from src/pages/LosslessAlbums.tsx rename to src/features/album/pages/LosslessAlbums.tsx index 8f7b9fc2..4a205278 100644 --- a/src/pages/LosslessAlbums.tsx +++ b/src/features/album/pages/LosslessAlbums.tsx @@ -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; diff --git a/src/pages/RandomAlbums.tsx b/src/features/album/pages/RandomAlbums.tsx similarity index 90% rename from src/pages/RandomAlbums.tsx rename to src/features/album/pages/RandomAlbums.tsx index 17cfd35f..b4e5df75 100644 --- a/src/pages/RandomAlbums.tsx +++ b/src/features/album/pages/RandomAlbums.tsx @@ -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. */ diff --git a/src/store/albumBrowseSessionStore.test.ts b/src/features/album/store/albumBrowseSessionStore.test.ts similarity index 98% rename from src/store/albumBrowseSessionStore.test.ts rename to src/features/album/store/albumBrowseSessionStore.test.ts index ea605faa..7252aad9 100644 --- a/src/store/albumBrowseSessionStore.test.ts +++ b/src/features/album/store/albumBrowseSessionStore.test.ts @@ -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(() => { diff --git a/src/store/albumBrowseSessionStore.ts b/src/features/album/store/albumBrowseSessionStore.ts similarity index 98% rename from src/store/albumBrowseSessionStore.ts rename to src/features/album/store/albumBrowseSessionStore.ts index ed634e7e..a7cbd0b5 100644 --- a/src/store/albumBrowseSessionStore.ts +++ b/src/features/album/store/albumBrowseSessionStore.ts @@ -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'; diff --git a/src/utils/componentHelpers/albumDetailHelpers.ts b/src/features/album/utils/albumDetailHelpers.ts similarity index 100% rename from src/utils/componentHelpers/albumDetailHelpers.ts rename to src/features/album/utils/albumDetailHelpers.ts diff --git a/src/utils/albumRecency.ts b/src/features/album/utils/albumRecency.ts similarity index 100% rename from src/utils/albumRecency.ts rename to src/features/album/utils/albumRecency.ts diff --git a/src/utils/componentHelpers/albumTrackListHelpers.ts b/src/features/album/utils/albumTrackListHelpers.ts similarity index 97% rename from src/utils/componentHelpers/albumTrackListHelpers.ts rename to src/features/album/utils/albumTrackListHelpers.ts index caf04220..e360b2f5 100644 --- a/src/utils/componentHelpers/albumTrackListHelpers.ts +++ b/src/features/album/utils/albumTrackListHelpers.ts @@ -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[] = []; diff --git a/src/utils/album/deriveAlbumHeaderArtistRefs.test.ts b/src/features/album/utils/deriveAlbumHeaderArtistRefs.test.ts similarity index 96% rename from src/utils/album/deriveAlbumHeaderArtistRefs.test.ts rename to src/features/album/utils/deriveAlbumHeaderArtistRefs.test.ts index efefc5ec..f4d900a9 100644 --- a/src/utils/album/deriveAlbumHeaderArtistRefs.test.ts +++ b/src/features/album/utils/deriveAlbumHeaderArtistRefs.test.ts @@ -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 => ({ diff --git a/src/utils/album/deriveAlbumHeaderArtistRefs.ts b/src/features/album/utils/deriveAlbumHeaderArtistRefs.ts similarity index 98% rename from src/utils/album/deriveAlbumHeaderArtistRefs.ts rename to src/features/album/utils/deriveAlbumHeaderArtistRefs.ts index 1314d820..acd9c56c 100644 --- a/src/utils/album/deriveAlbumHeaderArtistRefs.ts +++ b/src/features/album/utils/deriveAlbumHeaderArtistRefs.ts @@ -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[] { diff --git a/src/utils/export/exportAlbumCard.ts b/src/features/album/utils/exportAlbumCard.ts similarity index 98% rename from src/utils/export/exportAlbumCard.ts rename to src/features/album/utils/exportAlbumCard.ts index b728fc14..6b031643 100644 --- a/src/utils/export/exportAlbumCard.ts +++ b/src/features/album/utils/exportAlbumCard.ts @@ -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; diff --git a/src/utils/export/exportNewAlbums.ts b/src/features/album/utils/exportNewAlbums.ts similarity index 96% rename from src/utils/export/exportNewAlbums.ts rename to src/features/album/utils/exportNewAlbums.ts index baddbba9..3a7df326 100644 --- a/src/utils/export/exportNewAlbums.ts +++ b/src/features/album/utils/exportNewAlbums.ts @@ -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', diff --git a/src/features/artist/components/ArtistDetailHero.tsx b/src/features/artist/components/ArtistDetailHero.tsx index dc88a0bb..01d4736d 100644 --- a/src/features/artist/components/ArtistDetailHero.tsx +++ b/src/features/artist/components/ArtistDetailHero.tsx @@ -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, diff --git a/src/features/artist/hooks/useArtistsBrowseFilters.ts b/src/features/artist/hooks/useArtistsBrowseFilters.ts index 30735609..ff2de615 100644 --- a/src/features/artist/hooks/useArtistsBrowseFilters.ts +++ b/src/features/artist/hooks/useArtistsBrowseFilters.ts @@ -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'; diff --git a/src/features/artist/pages/ArtistDetail.tsx b/src/features/artist/pages/ArtistDetail.tsx index 2d065d3d..14083b10 100644 --- a/src/features/artist/pages/ArtistDetail.tsx +++ b/src/features/artist/pages/ArtistDetail.tsx @@ -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'; diff --git a/src/features/favorites/pages/Favorites.tsx b/src/features/favorites/pages/Favorites.tsx index ebf6b9d0..8d24df18 100644 --- a/src/features/favorites/pages/Favorites.tsx +++ b/src/features/favorites/pages/Favorites.tsx @@ -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'; diff --git a/src/features/search/components/LiveSearch.tsx b/src/features/search/components/LiveSearch.tsx index 68276c0f..faac41f0 100644 --- a/src/features/search/components/LiveSearch.tsx +++ b/src/features/search/components/LiveSearch.tsx @@ -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'; diff --git a/src/features/search/components/MobileSearchOverlay.tsx b/src/features/search/components/MobileSearchOverlay.tsx index eff6f332..6186b6d8 100644 --- a/src/features/search/components/MobileSearchOverlay.tsx +++ b/src/features/search/components/MobileSearchOverlay.tsx @@ -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 { diff --git a/src/features/search/components/liveSearchScope.ts b/src/features/search/components/liveSearchScope.ts index 73407867..efebaeb6 100644 --- a/src/features/search/components/liveSearchScope.ts +++ b/src/features/search/components/liveSearchScope.ts @@ -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'; diff --git a/src/features/search/hooks/useLiveSearchRouteScope.ts b/src/features/search/hooks/useLiveSearchRouteScope.ts index 755e6369..2daf00e6 100644 --- a/src/features/search/hooks/useLiveSearchRouteScope.ts +++ b/src/features/search/hooks/useLiveSearchRouteScope.ts @@ -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'; diff --git a/src/features/search/hooks/useShareSearch.ts b/src/features/search/hooks/useShareSearch.ts index cf88d0e8..bd4221cd 100644 --- a/src/features/search/hooks/useShareSearch.ts +++ b/src/features/search/hooks/useShareSearch.ts @@ -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 { diff --git a/src/features/search/pages/SearchBrowsePage.tsx b/src/features/search/pages/SearchBrowsePage.tsx index 524df6ca..2638df04 100644 --- a/src/features/search/pages/SearchBrowsePage.tsx +++ b/src/features/search/pages/SearchBrowsePage.tsx @@ -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, diff --git a/src/features/stats/components/StatsExportModal.tsx b/src/features/stats/components/StatsExportModal.tsx index 2f02aeea..e13b0fd8 100644 --- a/src/features/stats/components/StatsExportModal.tsx +++ b/src/features/stats/components/StatsExportModal.tsx @@ -12,7 +12,7 @@ import { renderAlbumCardCanvas, ExportFormat, ExportGridSize, -} from '@/utils/export/exportAlbumCard'; +} from '@/features/album'; interface Props { open: boolean; diff --git a/src/features/stats/pages/Statistics.tsx b/src/features/stats/pages/Statistics.tsx index 974c86f9..88990219 100644 --- a/src/features/stats/pages/Statistics.tsx +++ b/src/features/stats/pages/Statistics.tsx @@ -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'; diff --git a/src/hooks/useComposersBrowseFilters.ts b/src/hooks/useComposersBrowseFilters.ts index 6ea45c97..ae63c9e6 100644 --- a/src/hooks/useComposersBrowseFilters.ts +++ b/src/hooks/useComposersBrowseFilters.ts @@ -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, diff --git a/src/hooks/useGenreDetailBrowse.ts b/src/hooks/useGenreDetailBrowse.ts index ecdbb3d6..a46893dc 100644 --- a/src/hooks/useGenreDetailBrowse.ts +++ b/src/hooks/useGenreDetailBrowse.ts @@ -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( diff --git a/src/pages/ComposerDetail.tsx b/src/pages/ComposerDetail.tsx index 3a3e38ed..1dd7d6c7 100644 --- a/src/pages/ComposerDetail.tsx +++ b/src/pages/ComposerDetail.tsx @@ -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'; diff --git a/src/pages/GenreDetail.tsx b/src/pages/GenreDetail.tsx index 53c31cf5..bcbcf47b 100644 --- a/src/pages/GenreDetail.tsx +++ b/src/pages/GenreDetail.tsx @@ -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'; diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index f16ae818..ce80f31d 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -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'; diff --git a/src/pages/MostPlayed.tsx b/src/pages/MostPlayed.tsx index fdb9fce4..2d6be71a 100644 --- a/src/pages/MostPlayed.tsx +++ b/src/pages/MostPlayed.tsx @@ -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; diff --git a/src/pages/NewReleases.tsx b/src/pages/NewReleases.tsx index 9e399d65..7be78bf3 100644 --- a/src/pages/NewReleases.tsx +++ b/src/pages/NewReleases.tsx @@ -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'; diff --git a/src/pages/PlaylistDetail.tsx b/src/pages/PlaylistDetail.tsx index 144067fb..babca984 100644 --- a/src/pages/PlaylistDetail.tsx +++ b/src/pages/PlaylistDetail.tsx @@ -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'; diff --git a/src/store/playerStore.persistence.test.ts b/src/store/playerStore.persistence.test.ts index 4051c5ae..a0a3dd98 100644 --- a/src/store/playerStore.persistence.test.ts +++ b/src/store/playerStore.persistence.test.ts @@ -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', () => ({ diff --git a/src/utils/navigation/albumDetailNavigation.ts b/src/utils/navigation/albumDetailNavigation.ts index 8c432ae0..9a165ad3 100644 --- a/src/utils/navigation/albumDetailNavigation.ts +++ b/src/utils/navigation/albumDetailNavigation.ts @@ -7,7 +7,7 @@ import { isAlbumDetailPath, isArtistDetailPath, isComposerDetailPath, -} from '../../store/albumBrowseSessionStore'; +} from '@/features/album'; import { peekPersistedAdvancedSearchLeaveSnapshot, saveAdvancedSearchLeaveSnapshot,