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

This commit is contained in:
Psychotoxical
2026-06-29 22:59:21 +02:00
parent ea3e3f1adf
commit dbffe59e58
13 changed files with 91 additions and 78 deletions
+1 -1
View File
@@ -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'));
@@ -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;
@@ -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) {
@@ -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[];
@@ -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[];
@@ -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;
@@ -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;
@@ -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[];
@@ -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'
@@ -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;
@@ -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;
+13
View File
@@ -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';
@@ -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 },