diff --git a/src/app/AppRoutes.tsx b/src/app/AppRoutes.tsx index 776bcbcd..8822dfa0 100644 --- a/src/app/AppRoutes.tsx +++ b/src/app/AppRoutes.tsx @@ -16,7 +16,7 @@ const ArtistDetail = lazy(() => import('../pages/ArtistDetail')); const Composers = lazy(() => import('../pages/Composers')); const ComposerDetail = lazy(() => import('../pages/ComposerDetail')); const NewReleases = lazy(() => import('../pages/NewReleases')); -const Favorites = lazy(() => import('../pages/Favorites')); +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')); diff --git a/src/components/favorites/FavoriteSongRow.tsx b/src/features/favorites/components/FavoriteSongRow.tsx similarity index 92% rename from src/components/favorites/FavoriteSongRow.tsx rename to src/features/favorites/components/FavoriteSongRow.tsx index 9ca2bff7..c4924cc8 100644 --- a/src/components/favorites/FavoriteSongRow.tsx +++ b/src/features/favorites/components/FavoriteSongRow.tsx @@ -1,15 +1,15 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { AudioLines, ChevronRight, Play, Square, X } from 'lucide-react'; -import type { ColDef } from '../../utils/useTracklistColumns'; -import type { SubsonicSong } from '../../api/subsonicTypes'; -import { codecLabel } from '../../utils/componentHelpers/playlistDetailHelpers'; -import { formatLastSeen } from '../../utils/componentHelpers/userMgmtHelpers'; -import i18n from '../../i18n'; -import { formatTrackTime } from '../../utils/format/formatDuration'; -import StarRating from '../StarRating'; -import { OpenArtistRefInline } from '../OpenArtistRefInline'; -import { resolveTrackArtistRefs } from '../../utils/playback/trackArtistRefs'; +import type { ColDef } from '@/utils/useTracklistColumns'; +import type { SubsonicSong } from '@/api/subsonicTypes'; +import { codecLabel } from '@/utils/componentHelpers/playlistDetailHelpers'; +import { formatLastSeen } from '@/utils/componentHelpers/userMgmtHelpers'; +import i18n from '@/i18n'; +import { formatTrackTime } from '@/utils/format/formatDuration'; +import StarRating from '@/components/StarRating'; +import { OpenArtistRefInline } from '@/components/OpenArtistRefInline'; +import { resolveTrackArtistRefs } from '@/utils/playback/trackArtistRefs'; export interface FavoriteSongRowCallbacks { activate: (song: SubsonicSong, index: number, e: React.MouseEvent) => void; diff --git a/src/components/favorites/FavoritesOfflineHeader.tsx b/src/features/favorites/components/FavoritesOfflineHeader.tsx similarity index 93% rename from src/components/favorites/FavoritesOfflineHeader.tsx rename to src/features/favorites/components/FavoritesOfflineHeader.tsx index 83d22f19..3e7875d5 100644 --- a/src/components/favorites/FavoritesOfflineHeader.tsx +++ b/src/features/favorites/components/FavoritesOfflineHeader.tsx @@ -1,14 +1,14 @@ import { HardDrive } from 'lucide-react'; import { useTranslation } from 'react-i18next'; -import { useAuthStore } from '../../store/authStore'; +import { useAuthStore } from '@/store/authStore'; import { useFavoritesOfflineStatus, type FavoritesOfflineSemaphore, -} from '../../hooks/useFavoritesOfflineStatus'; +} from '@/features/favorites/hooks/useFavoritesOfflineStatus'; import { disableFavoritesOfflineSync, scheduleFavoritesOfflineSync, -} from '../../utils/offline/favoritesOfflineSync'; +} from '@/utils/offline/favoritesOfflineSync'; function semaphoreTooltipKey(semaphore: FavoritesOfflineSemaphore): string { switch (semaphore) { diff --git a/src/components/favorites/FavoritesSongsSectionHeader.tsx b/src/features/favorites/components/FavoritesSongsSectionHeader.tsx similarity index 96% rename from src/components/favorites/FavoritesSongsSectionHeader.tsx rename to src/features/favorites/components/FavoritesSongsSectionHeader.tsx index 698ffff0..87e0ffe7 100644 --- a/src/components/favorites/FavoritesSongsSectionHeader.tsx +++ b/src/features/favorites/components/FavoritesSongsSectionHeader.tsx @@ -1,12 +1,12 @@ import React, { useMemo, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { ListPlus, Play, SlidersHorizontal, X } from 'lucide-react'; -import type { SubsonicSong } from '../../api/subsonicTypes'; -import { usePlayerStore } from '../../store/playerStore'; -import { useSelectionStore } from '../../store/selectionStore'; -import { songToTrack } from '../../utils/playback/songToTrack'; -import { AddToPlaylistSubmenu } from '../ContextMenu'; -import GenreFilterBar from '../GenreFilterBar'; +import type { SubsonicSong } from '@/api/subsonicTypes'; +import { usePlayerStore } from '@/store/playerStore'; +import { useSelectionStore } from '@/store/selectionStore'; +import { songToTrack } from '@/utils/playback/songToTrack'; +import { AddToPlaylistSubmenu } from '@/components/ContextMenu'; +import GenreFilterBar from '@/components/GenreFilterBar'; interface Props { visibleSongs: SubsonicSong[]; diff --git a/src/components/favorites/FavoritesSongsTracklist.tsx b/src/features/favorites/components/FavoritesSongsTracklist.tsx similarity index 93% rename from src/components/favorites/FavoritesSongsTracklist.tsx rename to src/features/favorites/components/FavoritesSongsTracklist.tsx index c3ca551e..e0e28361 100644 --- a/src/components/favorites/FavoritesSongsTracklist.tsx +++ b/src/features/favorites/components/FavoritesSongsTracklist.tsx @@ -1,22 +1,22 @@ import React, { useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useVirtualizer } from '@tanstack/react-virtual'; -import FavoriteSongRow, { type FavoriteSongRowCallbacks } from './FavoriteSongRow'; -import { TracklistColumnPicker } from '../albumTrackList/TracklistColumnPicker'; +import FavoriteSongRow, { type FavoriteSongRowCallbacks } from '@/features/favorites/components/FavoriteSongRow'; +import { TracklistColumnPicker } from '@/components/albumTrackList/TracklistColumnPicker'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; -import type { ColDef } from '../../utils/useTracklistColumns'; -import type { SubsonicSong } from '../../api/subsonicTypes'; -import { usePlayerStore } from '../../store/playerStore'; -import { previewInputFromSong, usePreviewStore } from '../../store/previewStore'; -import { useSelectionStore } from '../../store/selectionStore'; -import { useThemeStore } from '../../store/themeStore'; -import { useDragDrop } from '../../contexts/DragDropContext'; -import { useOrbitSongRowBehavior } from '../../hooks/useOrbitSongRowBehavior'; -import { songToTrack } from '../../utils/playback/songToTrack'; -import { appendServerQuery } from '../../utils/navigation/detailServerScope'; -import { APP_MAIN_SCROLL_VIEWPORT_ID } from '../../constants/appScroll'; -import { useElementClientHeightById } from '../../hooks/useResizeClientHeight'; -import { SORTABLE_COLUMNS } from '../../hooks/useFavoritesSongFiltering'; +import type { ColDef } from '@/utils/useTracklistColumns'; +import type { SubsonicSong } from '@/api/subsonicTypes'; +import { usePlayerStore } from '@/store/playerStore'; +import { previewInputFromSong, usePreviewStore } from '@/store/previewStore'; +import { useSelectionStore } from '@/store/selectionStore'; +import { useThemeStore } from '@/store/themeStore'; +import { useDragDrop } from '@/contexts/DragDropContext'; +import { useOrbitSongRowBehavior } from '@/hooks/useOrbitSongRowBehavior'; +import { songToTrack } from '@/utils/playback/songToTrack'; +import { appendServerQuery } from '@/utils/navigation/detailServerScope'; +import { APP_MAIN_SCROLL_VIEWPORT_ID } from '@/constants/appScroll'; +import { useElementClientHeightById } from '@/hooks/useResizeClientHeight'; +import { SORTABLE_COLUMNS } from '@/features/favorites/hooks/useFavoritesSongFiltering'; interface Props { visibleSongs: SubsonicSong[]; diff --git a/src/components/favorites/RadioFavorites.tsx b/src/features/favorites/components/RadioFavorites.tsx similarity index 93% rename from src/components/favorites/RadioFavorites.tsx rename to src/features/favorites/components/RadioFavorites.tsx index 357f930f..abbb40e8 100644 --- a/src/components/favorites/RadioFavorites.tsx +++ b/src/features/favorites/components/RadioFavorites.tsx @@ -1,11 +1,11 @@ import React, { useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Cast, ChevronLeft, ChevronRight, Heart, X } from 'lucide-react'; -import type { InternetRadioStation } from '../../api/subsonicTypes'; -import { CoverArtImage } from '../../cover/CoverArtImage'; -import { albumCoverRef } from '../../cover/ref'; -import { coverArtIdFromRadio } from '../../cover/ids'; -import { COVER_DENSE_GRID_MIN_CELL_CSS_PX } from '../../cover/layoutSizes'; +import type { InternetRadioStation } from '@/api/subsonicTypes'; +import { CoverArtImage } from '@/cover/CoverArtImage'; +import { albumCoverRef } from '@/cover/ref'; +import { coverArtIdFromRadio } from '@/cover/ids'; +import { COVER_DENSE_GRID_MIN_CELL_CSS_PX } from '@/cover/layoutSizes'; interface RadioStationRowProps { title: string; diff --git a/src/components/favorites/TopFavoriteArtists.tsx b/src/features/favorites/components/TopFavoriteArtists.tsx similarity index 95% rename from src/components/favorites/TopFavoriteArtists.tsx rename to src/features/favorites/components/TopFavoriteArtists.tsx index 18b89b98..7642a276 100644 --- a/src/components/favorites/TopFavoriteArtists.tsx +++ b/src/features/favorites/components/TopFavoriteArtists.tsx @@ -1,9 +1,9 @@ import React, { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ChevronLeft, ChevronRight, Users } from 'lucide-react'; -import { ArtistCoverArtImage } from '../../cover/ArtistCoverArtImage'; -import { COVER_DENSE_GRID_MIN_CELL_CSS_PX } from '../../cover/layoutSizes'; -import { coverServerScopeForServerId } from '../../cover/serverScope'; +import { ArtistCoverArtImage } from '@/cover/ArtistCoverArtImage'; +import { COVER_DENSE_GRID_MIN_CELL_CSS_PX } from '@/cover/layoutSizes'; +import { coverServerScopeForServerId } from '@/cover/serverScope'; export interface TopFavoriteArtist { id: string; diff --git a/src/hooks/useFavoritesData.ts b/src/features/favorites/hooks/useFavoritesData.ts similarity index 87% rename from src/hooks/useFavoritesData.ts rename to src/features/favorites/hooks/useFavoritesData.ts index 11bd9222..010e9f70 100644 --- a/src/hooks/useFavoritesData.ts +++ b/src/features/favorites/hooks/useFavoritesData.ts @@ -1,20 +1,20 @@ import { useEffect, useMemo, useState } from 'react'; -import { getInternetRadioStations } from '../api/subsonicRadio'; -import { getStarred } from '../api/subsonicStarRating'; +import { getInternetRadioStations } from '@/api/subsonicRadio'; +import { getStarred } from '@/api/subsonicStarRating'; import type { InternetRadioStation, SubsonicAlbum, SubsonicArtist, SubsonicSong, -} from '../api/subsonicTypes'; -import { useAuthStore } from '../store/authStore'; -import { usePlayerStore } from '../store/playerStore'; -import type { TopFavoriteArtist } from '../components/favorites/TopFavoriteArtists'; -import { useConnectionStatus } from './useConnectionStatus'; -import { isActiveServerReachable } from '../utils/network/activeServerReachability'; -import { useOfflineBrowseContext } from './useOfflineBrowseContext'; -import { useOfflineBrowseReloadToken } from './useOfflineBrowseReloadToken'; +} from '@/api/subsonicTypes'; +import { useAuthStore } from '@/store/authStore'; +import { usePlayerStore } from '@/store/playerStore'; +import type { TopFavoriteArtist } from '@/features/favorites/components/TopFavoriteArtists'; +import { useConnectionStatus } from '@/hooks/useConnectionStatus'; +import { isActiveServerReachable } from '@/utils/network/activeServerReachability'; +import { useOfflineBrowseContext } from '@/hooks/useOfflineBrowseContext'; +import { useOfflineBrowseReloadToken } from '@/hooks/useOfflineBrowseReloadToken'; import { loadStarredFromAllLibraryIndexes, loadStarredFromAllServersOnline, -} from '../utils/offline/offlineStarredLoad'; +} from '@/utils/offline/offlineStarredLoad'; export interface FavoritesDataResult { albums: SubsonicAlbum[]; diff --git a/src/hooks/useFavoritesOfflineStatus.ts b/src/features/favorites/hooks/useFavoritesOfflineStatus.ts similarity index 86% rename from src/hooks/useFavoritesOfflineStatus.ts rename to src/features/favorites/hooks/useFavoritesOfflineStatus.ts index 96c315bf..c35b7869 100644 --- a/src/hooks/useFavoritesOfflineStatus.ts +++ b/src/features/favorites/hooks/useFavoritesOfflineStatus.ts @@ -1,10 +1,10 @@ import { useMemo } from 'react'; -import { useAuthStore } from '../store/authStore'; -import { useFavoritesOfflineSyncStore } from '../store/favoritesOfflineSyncStore'; -import { useLocalPlaybackStore } from '../store/localPlaybackStore'; -import { useOfflineJobStore } from '../store/offlineJobStore'; -import { FAVORITES_OFFLINE_JOB_ID } from '../utils/offline/favoritesOfflineConstants'; -import { entryBelongsToServer } from '../utils/offline/offlineLibraryHelpers'; +import { useAuthStore } from '@/store/authStore'; +import { useFavoritesOfflineSyncStore } from '@/store/favoritesOfflineSyncStore'; +import { useLocalPlaybackStore } from '@/store/localPlaybackStore'; +import { useOfflineJobStore } from '@/store/offlineJobStore'; +import { FAVORITES_OFFLINE_JOB_ID } from '@/utils/offline/favoritesOfflineConstants'; +import { entryBelongsToServer } from '@/utils/offline/offlineLibraryHelpers'; export type FavoritesOfflineUiStatus = | 'disabled' diff --git a/src/hooks/useFavoritesSelection.ts b/src/features/favorites/hooks/useFavoritesSelection.ts similarity index 94% rename from src/hooks/useFavoritesSelection.ts rename to src/features/favorites/hooks/useFavoritesSelection.ts index 8464066e..b209730d 100644 --- a/src/hooks/useFavoritesSelection.ts +++ b/src/features/favorites/hooks/useFavoritesSelection.ts @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useRef } from 'react'; -import type { SubsonicSong } from '../api/subsonicTypes'; -import { useSelectionStore } from '../store/selectionStore'; +import type { SubsonicSong } from '@/api/subsonicTypes'; +import { useSelectionStore } from '@/store/selectionStore'; export interface FavoritesSelectionResult { toggleSelect: (id: string, idx: number, shift: boolean) => void; diff --git a/src/hooks/useFavoritesSongFiltering.tsx b/src/features/favorites/hooks/useFavoritesSongFiltering.tsx similarity index 98% rename from src/hooks/useFavoritesSongFiltering.tsx rename to src/features/favorites/hooks/useFavoritesSongFiltering.tsx index 85f9333e..5556480b 100644 --- a/src/hooks/useFavoritesSongFiltering.tsx +++ b/src/features/favorites/hooks/useFavoritesSongFiltering.tsx @@ -1,7 +1,7 @@ import React, { useMemo } from 'react'; import { ArrowDown, ArrowUp } from 'lucide-react'; -import type { SubsonicSong } from '../api/subsonicTypes'; -import { usePlayerStore } from '../store/playerStore'; +import type { SubsonicSong } from '@/api/subsonicTypes'; +import { usePlayerStore } from '@/store/playerStore'; const CURRENT_YEAR = new Date().getFullYear(); const MIN_YEAR = 1950; diff --git a/src/features/favorites/index.ts b/src/features/favorites/index.ts new file mode 100644 index 00000000..f4db4d38 --- /dev/null +++ b/src/features/favorites/index.ts @@ -0,0 +1,13 @@ +/** + * Favorites feature — the Favorites page (starred songs/artists + favourite + * radio stations) and its data/selection/filtering hooks. The `Favorites` page + * is lazy-loaded by the router via its deep path, so it is not re-exported here. + * + * Note: the offline integration layer (`utils/offline/favorites*`, + * `store/favoritesOfflineSyncStore`) stays in the offline layer — the offline + * core imports it, so it cannot live under this feature without inverting the + * core→feature dependency. + */ +export { useFavoritesData } from './hooks/useFavoritesData'; +export { useFavoritesSelection } from './hooks/useFavoritesSelection'; +export { useFavoritesOfflineStatus } from './hooks/useFavoritesOfflineStatus'; diff --git a/src/pages/Favorites.tsx b/src/features/favorites/pages/Favorites.tsx similarity index 89% rename from src/pages/Favorites.tsx rename to src/features/favorites/pages/Favorites.tsx index f0c2d20e..44e979d4 100644 --- a/src/pages/Favorites.tsx +++ b/src/features/favorites/pages/Favorites.tsx @@ -1,20 +1,20 @@ -import { queueSongStar, queueSongRating } from '../store/pendingStarSync'; +import { queueSongStar, queueSongRating } from '@/store/pendingStarSync'; import React, { useEffect, useMemo, useState } from 'react'; -import { useTracklistColumns, type ColDef } from '../utils/useTracklistColumns'; -import { TopFavoriteArtistsRow } from '../components/favorites/TopFavoriteArtists'; -import { RadioStationRow } from '../components/favorites/RadioFavorites'; -import FavoritesSongsSectionHeader from '../components/favorites/FavoritesSongsSectionHeader'; -import FavoritesSongsTracklist from '../components/favorites/FavoritesSongsTracklist'; -import { useFavoritesData } from '../hooks/useFavoritesData'; -import { useFavoritesSongFiltering } from '../hooks/useFavoritesSongFiltering'; -import { useFavoritesSelection } from '../hooks/useFavoritesSelection'; -import { useBulkPlPickerOutsideClick } from '../hooks/useBulkPlPickerOutsideClick'; -import AlbumRow from '../components/AlbumRow'; -import ArtistRow from '../components/ArtistRow'; -import { usePlayerStore } from '../store/playerStore'; +import { useTracklistColumns, type ColDef } from '@/utils/useTracklistColumns'; +import { TopFavoriteArtistsRow } from '@/features/favorites/components/TopFavoriteArtists'; +import { RadioStationRow } from '@/features/favorites/components/RadioFavorites'; +import FavoritesSongsSectionHeader from '@/features/favorites/components/FavoritesSongsSectionHeader'; +import FavoritesSongsTracklist from '@/features/favorites/components/FavoritesSongsTracklist'; +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 ArtistRow from '@/components/ArtistRow'; +import { usePlayerStore } from '@/store/playerStore'; import { useTranslation } from 'react-i18next'; -import { useSelectionStore } from '../store/selectionStore'; -import FavoritesOfflineHeader from '../components/favorites/FavoritesOfflineHeader'; +import { useSelectionStore } from '@/store/selectionStore'; +import FavoritesOfflineHeader from '@/features/favorites/components/FavoritesOfflineHeader'; const FAV_COLUMNS: readonly ColDef[] = [ { key: 'num', i18nKey: null, minWidth: 60, defaultWidth: 60, required: true },