refactor(radio): co-locate internet radio feature into features/radio

This commit is contained in:
Psychotoxical
2026-06-29 23:04:34 +02:00
parent dbffe59e58
commit 896fe3f407
18 changed files with 61 additions and 47 deletions
+1 -1
View File
@@ -36,7 +36,7 @@ const OfflineLibrary = lazy(() => import('../pages/OfflineLibrary'));
const LabelAlbums = lazy(() => import('../pages/LabelAlbums'));
const SearchBrowsePage = lazy(() => import('../pages/SearchBrowsePage'));
const FolderBrowser = lazy(() => import('../pages/FolderBrowser'));
const InternetRadio = lazy(() => import('../pages/InternetRadio'));
const InternetRadio = lazy(() => import('@/features/radio/pages/InternetRadio'));
const Genres = lazy(() => import('../pages/Genres'));
const GenreDetail = lazy(() => import('../pages/GenreDetail'));
+2 -2
View File
@@ -15,8 +15,8 @@ import { useThemeStore } from '../store/themeStore';
import Equalizer from './Equalizer';
import { useTranslation } from 'react-i18next';
import { usePlaybackLibraryNavigate } from '../hooks/usePlaybackLibraryNavigate';
import { useRadioMetadata } from '../hooks/useRadioMetadata';
import { useRadioMprisSync } from '../hooks/useRadioMprisSync';
import { useRadioMetadata } from '@/features/radio';
import { useRadioMprisSync } from '@/features/radio';
import { usePlaybackDelayPress } from '../hooks/usePlaybackDelayPress';
import PlaybackDelayModal from './PlaybackDelayModal';
import { usePlaybackScheduleRemaining } from '../utils/format/playbackScheduleFormat';
+1 -1
View File
@@ -1,7 +1,7 @@
import React, { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { Cast, Clock, Radio, SkipForward, Users } from 'lucide-react';
import type { useRadioMetadata } from '../../hooks/useRadioMetadata';
import type { useRadioMetadata } from '@/features/radio';
import { usePlayerStore } from '../../store/playerStore';
import { formatTrackTime } from '../../utils/format/formatDuration';
@@ -1,6 +1,6 @@
import { ArrowLeftRight } from 'lucide-react';
import type { TFunction } from 'i18next';
import type { RadioMetadata } from '../../hooks/useRadioMetadata';
import type { RadioMetadata } from '@/features/radio';
import { useThemeStore } from '../../store/themeStore';
import { formatTrackTime, playbarMinuteFieldWidth } from '../../utils/format/formatDuration';
import { WaveformSeek } from '@/features/waveform';
+1 -1
View File
@@ -3,7 +3,7 @@ import type { TFunction } from 'i18next';
import { queueSongRating } from '../../store/pendingStarSync';
import type { InternetRadioStation, SubsonicOpenArtistRef } from '../../api/subsonicTypes';
import type { PlayerState, Track } from '../../store/playerStoreTypes';
import type { RadioMetadata } from '../../hooks/useRadioMetadata';
import type { RadioMetadata } from '@/features/radio';
import type { PreviewingTrack } from '../../store/previewStore';
import { CoverArtImage } from '../../cover/CoverArtImage';
import { albumCoverRef } from '../../cover/ref';
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useState } from 'react';
import { getInternetRadioStations } from '@/api/subsonicRadio';
import { getInternetRadioStations } from '@/features/radio';
import { getStarred } from '@/api/subsonicStarRating';
import type {
InternetRadioStation, SubsonicAlbum, SubsonicArtist, SubsonicSong,
@@ -1,7 +1,7 @@
import { invoke } from '@tauri-apps/api/core';
import { useAuthStore } from '../store/authStore';
import { api } from './subsonicClient';
import type { InternetRadioStation, RadioBrowserStation } from './subsonicTypes';
import { useAuthStore } from '@/store/authStore';
import { api } from '@/api/subsonicClient';
import type { InternetRadioStation, RadioBrowserStation } from '@/api/subsonicTypes';
export async function getInternetRadioStations(): Promise<InternetRadioStation[]> {
try {
@@ -2,12 +2,12 @@ import React, { useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { Cast, Globe, Heart, Square, Trash2, X } from 'lucide-react';
import { open } from '@tauri-apps/plugin-shell';
import type { InternetRadioStation } from '../../api/subsonicTypes';
import { useDragDrop, useDragSource } from '../../contexts/DragDropContext';
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 { useDragDrop, useDragSource } from '@/contexts/DragDropContext';
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 RadioCardProps {
s: InternetRadioStation;
@@ -5,11 +5,11 @@ import { Cast, Check, Loader2, Plus, X } from 'lucide-react';
import {
createInternetRadioStation, fetchUrlBytes, getInternetRadioStations,
getTopRadioStations, searchRadioBrowser, uploadRadioCoverArtBytes,
} from '../../api/subsonicRadio';
} from '@/features/radio/api/subsonicRadio';
import {
type InternetRadioStation, type RadioBrowserStation, RADIO_PAGE_SIZE,
} from '../../api/subsonicTypes';
import { showToast } from '../../utils/ui/toast';
} from '@/api/subsonicTypes';
import { showToast } from '@/utils/ui/toast';
interface RadioDirectoryModalProps {
onClose: () => void;
@@ -2,10 +2,10 @@ import React, { useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next';
import { Camera, Cast, Loader2, 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 type { InternetRadioStation } from '@/api/subsonicTypes';
import { CoverArtImage } from '@/cover/CoverArtImage';
import { albumCoverRef } from '@/cover/ref';
import { coverArtIdFromRadio } from '@/cover/ids';
interface RadioEditModalProps {
station: InternetRadioStation | null; // null = create new
@@ -1,14 +1,14 @@
import type { InternetRadioStation } from '../api/subsonicTypes';
import type { InternetRadioStation } from '@/api/subsonicTypes';
import { useEffect, useRef, useState } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { usePerfProbeFlags } from '../utils/perf/perfFlags';
import { usePerfProbeFlags } from '@/utils/perf/perfFlags';
import {
guessAzuraCastApiUrl,
normaliseAzuraCastHomepageUrl,
fetchAzuraCastNowPlaying,
type AzuraCastNowPlaying,
type AzuraCastSong,
} from '../api/azuracast';
} from '@/api/azuracast';
// ─── Public types ─────────────────────────────────────────────────────────────
@@ -1,9 +1,9 @@
import { renderHook, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { invoke } from '@tauri-apps/api/core';
import { useRadioMprisSync } from './useRadioMprisSync';
import type { RadioMetadata } from './useRadioMetadata';
import type { InternetRadioStation } from '../api/subsonicTypes';
import { useRadioMprisSync } from '@/features/radio/hooks/useRadioMprisSync';
import type { RadioMetadata } from '@/features/radio/hooks/useRadioMetadata';
import type { InternetRadioStation } from '@/api/subsonicTypes';
vi.mock('@tauri-apps/api/core', () => ({ invoke: vi.fn(async () => undefined) }));
const invokeMock = vi.mocked(invoke);
@@ -1,7 +1,7 @@
import { useEffect, useRef } from 'react';
import { invoke } from '@tauri-apps/api/core';
import type { InternetRadioStation } from '../api/subsonicTypes';
import type { RadioMetadata } from './useRadioMetadata';
import type { InternetRadioStation } from '@/api/subsonicTypes';
import type { RadioMetadata } from '@/features/radio/hooks/useRadioMetadata';
/**
* Internet radio OS media controls (MPRIS / SMTC / Now Playing).
+14
View File
@@ -0,0 +1,14 @@
/**
* Internet radio feature — station browse/edit UI (directory, cards, toolbar),
* the InternetRadio page, and the live ICY/AzuraCast metadata hooks. The page is
* lazy-loaded by the router via its deep path, so it is not re-exported here.
*
* Note: radio *playback* state (`store/radioPlayer`, `store/radioSessionState`)
* and the ICY→MPRIS bridge (`audioListenerSetup/radioMprisMetadata`) stay in the
* playback/audio core — the player core drives them, so they are not part of
* this UI feature.
*/
export { getInternetRadioStations } from './api/subsonicRadio';
export { useRadioMetadata } from './hooks/useRadioMetadata';
export type { RadioMetadata } from './hooks/useRadioMetadata';
export { useRadioMprisSync } from './hooks/useRadioMprisSync';
@@ -1,21 +1,21 @@
import { getInternetRadioStations, createInternetRadioStation, updateInternetRadioStation, deleteInternetRadioStation, uploadRadioCoverArt, deleteRadioCoverArt } from '../api/subsonicRadio';
import { type InternetRadioStation } from '../api/subsonicTypes';
import { getInternetRadioStations, createInternetRadioStation, updateInternetRadioStation, deleteInternetRadioStation, uploadRadioCoverArt, deleteRadioCoverArt } from '@/features/radio/api/subsonicRadio';
import { type InternetRadioStation } from '@/api/subsonicTypes';
import React, { useEffect, useState, useMemo, useCallback } from 'react';
import { Plus, Search } from 'lucide-react';
import { usePlayerStore } from '../store/playerStore';
import { setRadioVolume } from '../store/radioPlayer';
import { fadeOut } from '../utils/playback/fadeOut';
import { invalidateCoverArt } from '../utils/imageCache';
import { usePlayerStore } from '@/store/playerStore';
import { setRadioVolume } from '@/store/radioPlayer';
import { fadeOut } from '@/utils/playback/fadeOut';
import { invalidateCoverArt } from '@/utils/imageCache';
import { useTranslation } from 'react-i18next';
import { showToast } from '../utils/ui/toast';
import RadioToolbar from '../components/internetRadio/RadioToolbar';
import AlphabetFilterBar from '../components/internetRadio/AlphabetFilterBar';
import RadioCard from '../components/internetRadio/RadioCard';
import RadioEditModal from '../components/internetRadio/RadioEditModal';
import RadioDirectoryModal from '../components/internetRadio/RadioDirectoryModal';
import { usePerfProbeFlags } from '../utils/perf/perfFlags';
import { VirtualCardGrid } from '../components/VirtualCardGrid';
import { useNavidromeAdminRole, canManageNavidromeRadio } from '../hooks/useNavidromeAdminRole';
import { showToast } from '@/utils/ui/toast';
import RadioToolbar from '@/features/radio/components/RadioToolbar';
import AlphabetFilterBar from '@/features/radio/components/AlphabetFilterBar';
import RadioCard from '@/features/radio/components/RadioCard';
import RadioEditModal from '@/features/radio/components/RadioEditModal';
import RadioDirectoryModal from '@/features/radio/components/RadioDirectoryModal';
import { usePerfProbeFlags } from '@/utils/perf/perfFlags';
import { VirtualCardGrid } from '@/components/VirtualCardGrid';
import { useNavidromeAdminRole, canManageNavidromeRadio } from '@/hooks/useNavidromeAdminRole';
export default function InternetRadio() {
const { t } = useTranslation();
+1 -1
View File
@@ -12,7 +12,7 @@ import { usePlayerStore } from '../store/playerStore';
import { useAuthStore } from '../store/authStore';
import { useLyricsStore } from '../store/lyricsStore';
import { songToTrack } from '../utils/playback/songToTrack';
import { useRadioMetadata } from '../hooks/useRadioMetadata';
import { useRadioMetadata } from '@/features/radio';
import { useDragDrop } from '../contexts/DragDropContext';
import OverlayScrollArea from '@/ui/OverlayScrollArea';
import {