refactor(randomMix): co-locate random/lucky-mix feature into features/randomMix

This commit is contained in:
Psychotoxical
2026-06-30 18:08:15 +02:00
parent f443a60c54
commit f05183d412
20 changed files with 44 additions and 31 deletions
+4 -4
View File
@@ -4,7 +4,7 @@ import MobilePlayerView from '../components/MobilePlayerView';
import { useIsMobile } from '@/lib/hooks/useIsMobile'; import { useIsMobile } from '@/lib/hooks/useIsMobile';
import { useSidebarStore } from '@/features/sidebar'; import { useSidebarStore } from '@/features/sidebar';
import { useAuthStore } from '../store/authStore'; import { useAuthStore } from '../store/authStore';
import { useLuckyMixAvailable } from '../hooks/useLuckyMixAvailable'; import { useLuckyMixAvailable } from '@/features/randomMix';
import { resolveStartRoute } from '@/features/sidebar'; import { resolveStartRoute } from '@/features/sidebar';
// Route-level lazy loading: keeps the non-page graph (shell, player, stores) in // Route-level lazy loading: keeps the non-page graph (shell, player, stores) in
@@ -17,13 +17,13 @@ const Composers = lazy(() => import('@/features/composers/pages/Composers'));
const ComposerDetail = lazy(() => import('@/features/composers/pages/ComposerDetail')); const ComposerDetail = lazy(() => import('@/features/composers/pages/ComposerDetail'));
const NewReleases = lazy(() => import('@/features/album/pages/NewReleases')); const NewReleases = lazy(() => import('@/features/album/pages/NewReleases'));
const Favorites = lazy(() => import('@/features/favorites/pages/Favorites')); const Favorites = lazy(() => import('@/features/favorites/pages/Favorites'));
const RandomMix = lazy(() => import('../pages/RandomMix')); const RandomMix = lazy(() => import('@/features/randomMix/pages/RandomMix'));
const RandomLanding = lazy(() => import('../pages/RandomLanding')); const RandomLanding = lazy(() => import('@/features/randomMix/pages/RandomLanding'));
const AlbumDetail = lazy(() => import('@/features/album/pages/AlbumDetail')); const AlbumDetail = lazy(() => import('@/features/album/pages/AlbumDetail'));
const MostPlayed = lazy(() => import('@/features/album/pages/MostPlayed')); const MostPlayed = lazy(() => import('@/features/album/pages/MostPlayed'));
const LosslessAlbums = lazy(() => import('@/features/album/pages/LosslessAlbums')); const LosslessAlbums = lazy(() => import('@/features/album/pages/LosslessAlbums'));
const RandomAlbums = lazy(() => import('@/features/album/pages/RandomAlbums')); const RandomAlbums = lazy(() => import('@/features/album/pages/RandomAlbums'));
const LuckyMixPage = lazy(() => import('../pages/LuckyMix')); const LuckyMixPage = lazy(() => import('@/features/randomMix/pages/LuckyMix'));
const Playlists = lazy(() => import('@/features/playlist/pages/Playlists')); const Playlists = lazy(() => import('@/features/playlist/pages/Playlists'));
const PlaylistDetail = lazy(() => import('@/features/playlist/pages/PlaylistDetail')); const PlaylistDetail = lazy(() => import('@/features/playlist/pages/PlaylistDetail'));
const NowPlayingPage = lazy(() => import('@/features/nowPlaying/pages/NowPlaying')); const NowPlayingPage = lazy(() => import('@/features/nowPlaying/pages/NowPlaying'));
+1 -1
View File
@@ -5,7 +5,7 @@ import { Settings, HardDriveDownload } from 'lucide-react';
import { useSidebarStore } from '@/features/sidebar'; import { useSidebarStore } from '@/features/sidebar';
import { useAuthStore } from '../store/authStore'; import { useAuthStore } from '../store/authStore';
import { ALL_NAV_ITEMS } from '../config/navItems'; import { ALL_NAV_ITEMS } from '../config/navItems';
import { useLuckyMixAvailable } from '../hooks/useLuckyMixAvailable'; import { useLuckyMixAvailable } from '@/features/randomMix';
import { isOfflineSidebarNavAllowed } from '@/features/offline'; import { isOfflineSidebarNavAllowed } from '@/features/offline';
import { useOfflineBrowseContext } from '@/features/offline'; import { useOfflineBrowseContext } from '@/features/offline';
import { offlineBrowseNavFlags } from '@/features/offline'; import { offlineBrowseNavFlags } from '@/features/offline';
+1 -1
View File
@@ -4,7 +4,7 @@ import { Play } from 'lucide-react';
import type { TFunction } from 'i18next'; import type { TFunction } from 'i18next';
import OverlayScrollArea from '@/ui/OverlayScrollArea'; import OverlayScrollArea from '@/ui/OverlayScrollArea';
import { usePlayerStore } from '@/features/playback/store/playerStore'; import { usePlayerStore } from '@/features/playback/store/playerStore';
import { useLuckyMixStore } from '@/store/luckyMixStore'; import { useLuckyMixStore } from '@/features/randomMix';
import type { QueueItemRef } from '@/lib/media/trackTypes'; import type { QueueItemRef } from '@/lib/media/trackTypes';
import type { PlayerState } from '@/features/playback/store/playerStoreTypes'; import type { PlayerState } from '@/features/playback/store/playerStoreTypes';
import type { QueueDisplayMode } from '@/store/authStoreTypes'; import type { QueueDisplayMode } from '@/store/authStoreTypes';
+1 -1
View File
@@ -20,7 +20,7 @@ import { useThemeStore } from '@/store/themeStore';
import { useLyricsStore } from '@/store/lyricsStore'; import { useLyricsStore } from '@/store/lyricsStore';
import LyricsPane from '@/components/LyricsPane'; import LyricsPane from '@/components/LyricsPane';
import { NowPlayingInfo } from '@/features/nowPlaying'; import { NowPlayingInfo } from '@/features/nowPlaying';
import { useLuckyMixStore } from '@/store/luckyMixStore'; import { useLuckyMixStore } from '@/features/randomMix';
import { useQueueToolbarStore } from '@/store/queueToolbarStore'; import { useQueueToolbarStore } from '@/store/queueToolbarStore';
import { SavePlaylistModal } from '@/features/queue/components/SavePlaylistModal'; import { SavePlaylistModal } from '@/features/queue/components/SavePlaylistModal';
import { LoadPlaylistModal } from '@/features/queue/components/LoadPlaylistModal'; import { LoadPlaylistModal } from '@/features/queue/components/LoadPlaylistModal';
@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ChevronDown, ChevronUp } from 'lucide-react'; import { ChevronDown, ChevronUp } from 'lucide-react';
import { RANDOM_MIX_SIZE_OPTIONS } from '../../store/authStoreDefaults'; import { RANDOM_MIX_SIZE_OPTIONS } from '@/store/authStoreDefaults';
interface Props { interface Props {
isMobile: boolean; isMobile: boolean;
@@ -5,7 +5,7 @@ import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import type { Track } from '@/lib/media/trackTypes'; import type { Track } from '@/lib/media/trackTypes';
import { previewInputFromSong, usePreviewStore } from '@/features/playback/store/previewStore'; import { previewInputFromSong, usePreviewStore } from '@/features/playback/store/previewStore';
import { useDragDrop } from '@/lib/dnd/DragDropContext'; import { useDragDrop } from '@/lib/dnd/DragDropContext';
import { formatRandomMixDuration } from '../../utils/componentHelpers/randomMixHelpers'; import { formatRandomMixDuration } from '@/features/randomMix/utils/randomMixHelpers';
interface Props { interface Props {
song: SubsonicSong; song: SubsonicSong;
@@ -1,4 +1,4 @@
import { useAuthStore } from '../store/authStore'; import { useAuthStore } from '@/store/authStore';
/** /**
* Whether "Lucky Mix" should be exposed as a navigable menu/card entry. * Whether "Lucky Mix" should be exposed as a navigable menu/card entry.
+13
View File
@@ -0,0 +1,13 @@
/**
* Random / Lucky Mix feature — the random landing, random-mix browser, and
* lucky-mix trigger pages, the random-mix panels (filters/genre/header/row),
* the lucky-mix availability hook + session store, and the lucky-mix /
* random-mix queue-build helpers. The pages are lazy-loaded by the router via
* their deep paths, so they are not re-exported here.
*
* Stays OUT (shared infra, consumed by the playback core too, not owned):
* `utils/mix/mixRatingFilter` (a generic rating-window filter used by the
* infinite-queue builder + home/album/CLI → a `lib/` candidate).
*/
export { useLuckyMixAvailable, isLuckyMixAvailable } from './hooks/useLuckyMixAvailable';
export { useLuckyMixStore } from './store/luckyMixStore';
@@ -1,6 +1,6 @@
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { buildAndPlayLuckyMix } from '../utils/mix/luckyMix'; import { buildAndPlayLuckyMix } from '@/features/randomMix/utils/luckyMix';
export default function LuckyMixPage() { export default function LuckyMixPage() {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -2,7 +2,7 @@ import React from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Shuffle, Dices, Sparkles } from 'lucide-react'; import { Shuffle, Dices, Sparkles } from 'lucide-react';
import { useLuckyMixAvailable } from '../hooks/useLuckyMixAvailable'; import { useLuckyMixAvailable } from '@/features/randomMix/hooks/useLuckyMixAvailable';
interface MixCard { interface MixCard {
icon: React.ElementType; icon: React.ElementType;
@@ -4,21 +4,21 @@ import { songToTrack } from '@/lib/media/songToTrack';
import React, { useEffect, useMemo, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { usePlayerStore } from '@/features/playback/store/playerStore'; import { usePlayerStore } from '@/features/playback/store/playerStore';
import { usePreviewStore } from '@/features/playback/store/previewStore'; import { usePreviewStore } from '@/features/playback/store/previewStore';
import { useAuthStore } from '../store/authStore'; import { useAuthStore } from '@/store/authStore';
import { useLibraryIndexStore } from '../store/libraryIndexStore'; import { useLibraryIndexStore } from '@/store/libraryIndexStore';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useIsMobile } from '@/lib/hooks/useIsMobile'; import { useIsMobile } from '@/lib/hooks/useIsMobile';
import { useOrbitSongRowBehavior } from '@/features/orbit'; import { useOrbitSongRowBehavior } from '@/features/orbit';
import { import {
fetchRandomMixSongsUntilFull, fetchRandomMixSongsUntilFull,
getMixMinRatingsConfigFromAuth, getMixMinRatingsConfigFromAuth,
} from '../utils/mix/mixRatingFilter'; } from '@/utils/mix/mixRatingFilter';
import { fetchGenreCatalog } from '@/features/playback/utils/playback/genreBrowsePlayback'; import { fetchGenreCatalog } from '@/features/playback/utils/playback/genreBrowsePlayback';
import { AUDIOBOOK_GENRES, filterRandomMixSongs } from '../utils/componentHelpers/randomMixHelpers'; import { AUDIOBOOK_GENRES, filterRandomMixSongs } from '@/features/randomMix/utils/randomMixHelpers';
import RandomMixHeader from '../components/randomMix/RandomMixHeader'; import RandomMixHeader from '@/features/randomMix/components/RandomMixHeader';
import RandomMixFiltersPanel from '../components/randomMix/RandomMixFiltersPanel'; import RandomMixFiltersPanel from '@/features/randomMix/components/RandomMixFiltersPanel';
import RandomMixGenrePanel from '../components/randomMix/RandomMixGenrePanel'; import RandomMixGenrePanel from '@/features/randomMix/components/RandomMixGenrePanel';
import RandomMixTrackRow from '../components/randomMix/RandomMixTrackRow'; import RandomMixTrackRow from '@/features/randomMix/components/RandomMixTrackRow';
export default function RandomMix() { export default function RandomMix() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -5,12 +5,12 @@ import type { QueueItemRef } from '@/lib/media/trackTypes';
import { songToTrack } from '@/lib/media/songToTrack'; import { songToTrack } from '@/lib/media/songToTrack';
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import i18n from '@/lib/i18n'; import i18n from '@/lib/i18n';
import { useAuthStore } from '../../store/authStore'; import { useAuthStore } from '@/store/authStore';
import { pushQueueUndoFromGetter } from '@/features/playback/store/queueUndo'; import { pushQueueUndoFromGetter } from '@/features/playback/store/queueUndo';
import { usePlayerStore } from '@/features/playback/store/playerStore'; import { usePlayerStore } from '@/features/playback/store/playerStore';
import { useLuckyMixStore } from '../../store/luckyMixStore'; import { useLuckyMixStore } from '@/features/randomMix/store/luckyMixStore';
import { isLuckyMixAvailable } from '../../hooks/useLuckyMixAvailable'; import { isLuckyMixAvailable } from '@/features/randomMix/hooks/useLuckyMixAvailable';
import { showToast } from '../ui/toast'; import { showToast } from '@/utils/ui/toast';
import { import {
bindQueueServerForPlayback, bindQueueServerForPlayback,
playbackServerDiffersFromActive, playbackServerDiffersFromActive,
@@ -21,7 +21,7 @@ import {
filterSongsForLuckyMixRatings, filterSongsForLuckyMixRatings,
filterTopArtistsForMixRatings, filterTopArtistsForMixRatings,
getMixMinRatingsConfigFromAuth, getMixMinRatingsConfigFromAuth,
} from './mixRatingFilter'; } from '@/utils/mix/mixRatingFilter';
import { import {
MIX_TARGET_SIZE, MIX_TARGET_SIZE,
SEED_TARGET_SIZE, SEED_TARGET_SIZE,
@@ -33,7 +33,7 @@ import {
pickSongsForArtist, pickSongsForArtist,
pickSongsForAlbum, pickSongsForAlbum,
pickGoodRatedSongs, pickGoodRatedSongs,
} from './luckyMixHelpers'; } from '@/features/randomMix/utils/luckyMixHelpers';
/** /**
* Sentinel thrown inside the build loop when `useLuckyMixStore.cancelRequested` * Sentinel thrown inside the build loop when `useLuckyMixStore.cancelRequested`
@@ -5,7 +5,7 @@ import type { SubsonicAlbum, SubsonicSong } from '@/lib/api/subsonicTypes';
import { import {
filterSongsForLuckyMixRatings, filterSongsForLuckyMixRatings,
type MixMinRatingsConfig, type MixMinRatingsConfig,
} from './mixRatingFilter'; } from '@/utils/mix/mixRatingFilter';
export interface TopArtist { export interface TopArtist {
id: string; id: string;
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import type { SubsonicSong } from '@/lib/api/subsonicTypes'; import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import { filterRandomMixSongs } from './randomMixHelpers'; import { filterRandomMixSongs } from '@/features/randomMix/utils/randomMixHelpers';
function song(id: string): SubsonicSong { function song(id: string): SubsonicSong {
return { return {
@@ -1,5 +1,5 @@
import type { SubsonicSong } from '@/lib/api/subsonicTypes'; import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import { passesMixMinRatings, type MixMinRatingsConfig } from '../mix/mixRatingFilter'; import { passesMixMinRatings, type MixMinRatingsConfig } from '@/utils/mix/mixRatingFilter';
export const AUDIOBOOK_GENRES = [ export const AUDIOBOOK_GENRES = [
'hörbuch', 'hoerbuch', 'hörspiel', 'hoerspiel', 'hörbuch', 'hoerbuch', 'hörspiel', 'hoerspiel',
@@ -2,7 +2,7 @@ import { useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useAuthStore } from '@/store/authStore'; import { useAuthStore } from '@/store/authStore';
import { useSidebarStore, SidebarItemConfig, CONSERVED_SIDEBAR_NAV_IDS } from '@/features/sidebar'; import { useSidebarStore, SidebarItemConfig, CONSERVED_SIDEBAR_NAV_IDS } from '@/features/sidebar';
import { useLuckyMixAvailable } from '@/hooks/useLuckyMixAvailable'; import { useLuckyMixAvailable } from '@/features/randomMix';
import { ALL_NAV_ITEMS } from '@/config/navItems'; import { ALL_NAV_ITEMS } from '@/config/navItems';
import { applySidebarReorderById } from '@/features/sidebar'; import { applySidebarReorderById } from '@/features/sidebar';
import { useListReorderDnd } from '@/hooks/useListReorderDnd'; import { useListReorderDnd } from '@/hooks/useListReorderDnd';
+1 -1
View File
@@ -17,7 +17,7 @@ import {
getLibraryItemsForReorder, getLibraryItemsForReorder,
getSystemItemsForReorder, getSystemItemsForReorder,
} from '@/features/sidebar/utils/sidebarNavReorder'; } from '@/features/sidebar/utils/sidebarNavReorder';
import { useLuckyMixAvailable } from '@/hooks/useLuckyMixAvailable'; import { useLuckyMixAvailable } from '@/features/randomMix';
import { usePerfProbeFlags } from '@/utils/perf/perfFlags'; import { usePerfProbeFlags } from '@/utils/perf/perfFlags';
import { useSidebarNewReleasesUnread } from '@/features/sidebar/hooks/useSidebarNewReleasesUnread'; import { useSidebarNewReleasesUnread } from '@/features/sidebar/hooks/useSidebarNewReleasesUnread';
import { useSidebarNavDnd } from '@/features/sidebar/hooks/useSidebarNavDnd'; import { useSidebarNavDnd } from '@/features/sidebar/hooks/useSidebarNavDnd';