From 896fe3f4078aae6b2af33d6770e6c1e0c040f069 Mon Sep 17 00:00:00 2001 From: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com> Date: Mon, 29 Jun 2026 23:04:34 +0200 Subject: [PATCH] refactor(radio): co-locate internet radio feature into features/radio --- src/app/AppRoutes.tsx | 2 +- src/components/PlayerBar.tsx | 4 +-- src/components/nowPlaying/RadioView.tsx | 2 +- .../playerBar/PlayerSeekbarSection.tsx | 2 +- src/components/playerBar/PlayerTrackInfo.tsx | 2 +- .../favorites/hooks/useFavoritesData.ts | 2 +- src/{ => features/radio}/api/subsonicRadio.ts | 6 ++-- .../radio/components}/AlphabetFilterBar.tsx | 0 .../radio/components}/RadioCard.tsx | 12 ++++---- .../radio/components}/RadioDirectoryModal.tsx | 6 ++-- .../radio/components}/RadioEditModal.tsx | 8 ++--- .../radio/components}/RadioToolbar.tsx | 0 .../radio}/hooks/useRadioMetadata.ts | 6 ++-- .../radio}/hooks/useRadioMprisSync.test.ts | 6 ++-- .../radio}/hooks/useRadioMprisSync.ts | 4 +-- src/features/radio/index.ts | 14 +++++++++ .../radio}/pages/InternetRadio.tsx | 30 +++++++++---------- src/pages/NowPlaying.tsx | 2 +- 18 files changed, 61 insertions(+), 47 deletions(-) rename src/{ => features/radio}/api/subsonicRadio.ts (96%) rename src/{components/internetRadio => features/radio/components}/AlphabetFilterBar.tsx (100%) rename src/{components/internetRadio => features/radio/components}/RadioCard.tsx (93%) rename src/{components/internetRadio => features/radio/components}/RadioDirectoryModal.tsx (98%) rename src/{components/internetRadio => features/radio/components}/RadioEditModal.tsx (96%) rename src/{components/internetRadio => features/radio/components}/RadioToolbar.tsx (100%) rename src/{ => features/radio}/hooks/useRadioMetadata.ts (98%) rename src/{ => features/radio}/hooks/useRadioMprisSync.test.ts (93%) rename src/{ => features/radio}/hooks/useRadioMprisSync.ts (95%) create mode 100644 src/features/radio/index.ts rename src/{ => features/radio}/pages/InternetRadio.tsx (93%) diff --git a/src/app/AppRoutes.tsx b/src/app/AppRoutes.tsx index 8822dfa0..ab7b5205 100644 --- a/src/app/AppRoutes.tsx +++ b/src/app/AppRoutes.tsx @@ -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')); diff --git a/src/components/PlayerBar.tsx b/src/components/PlayerBar.tsx index 95b03f1d..8ea006ef 100644 --- a/src/components/PlayerBar.tsx +++ b/src/components/PlayerBar.tsx @@ -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'; diff --git a/src/components/nowPlaying/RadioView.tsx b/src/components/nowPlaying/RadioView.tsx index 96cc9b03..fb7ed9d2 100644 --- a/src/components/nowPlaying/RadioView.tsx +++ b/src/components/nowPlaying/RadioView.tsx @@ -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'; diff --git a/src/components/playerBar/PlayerSeekbarSection.tsx b/src/components/playerBar/PlayerSeekbarSection.tsx index 004c5cdd..9fbbc6d3 100644 --- a/src/components/playerBar/PlayerSeekbarSection.tsx +++ b/src/components/playerBar/PlayerSeekbarSection.tsx @@ -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'; diff --git a/src/components/playerBar/PlayerTrackInfo.tsx b/src/components/playerBar/PlayerTrackInfo.tsx index fa7725d2..2b9c3ff9 100644 --- a/src/components/playerBar/PlayerTrackInfo.tsx +++ b/src/components/playerBar/PlayerTrackInfo.tsx @@ -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'; diff --git a/src/features/favorites/hooks/useFavoritesData.ts b/src/features/favorites/hooks/useFavoritesData.ts index 010e9f70..add631be 100644 --- a/src/features/favorites/hooks/useFavoritesData.ts +++ b/src/features/favorites/hooks/useFavoritesData.ts @@ -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, diff --git a/src/api/subsonicRadio.ts b/src/features/radio/api/subsonicRadio.ts similarity index 96% rename from src/api/subsonicRadio.ts rename to src/features/radio/api/subsonicRadio.ts index cbcbb214..f3f27e5a 100644 --- a/src/api/subsonicRadio.ts +++ b/src/features/radio/api/subsonicRadio.ts @@ -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 { try { diff --git a/src/components/internetRadio/AlphabetFilterBar.tsx b/src/features/radio/components/AlphabetFilterBar.tsx similarity index 100% rename from src/components/internetRadio/AlphabetFilterBar.tsx rename to src/features/radio/components/AlphabetFilterBar.tsx diff --git a/src/components/internetRadio/RadioCard.tsx b/src/features/radio/components/RadioCard.tsx similarity index 93% rename from src/components/internetRadio/RadioCard.tsx rename to src/features/radio/components/RadioCard.tsx index 3cc994ba..a4ec5374 100644 --- a/src/components/internetRadio/RadioCard.tsx +++ b/src/features/radio/components/RadioCard.tsx @@ -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; diff --git a/src/components/internetRadio/RadioDirectoryModal.tsx b/src/features/radio/components/RadioDirectoryModal.tsx similarity index 98% rename from src/components/internetRadio/RadioDirectoryModal.tsx rename to src/features/radio/components/RadioDirectoryModal.tsx index d768faa9..97b8f67f 100644 --- a/src/components/internetRadio/RadioDirectoryModal.tsx +++ b/src/features/radio/components/RadioDirectoryModal.tsx @@ -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; diff --git a/src/components/internetRadio/RadioEditModal.tsx b/src/features/radio/components/RadioEditModal.tsx similarity index 96% rename from src/components/internetRadio/RadioEditModal.tsx rename to src/features/radio/components/RadioEditModal.tsx index 56acc15e..8169776f 100644 --- a/src/components/internetRadio/RadioEditModal.tsx +++ b/src/features/radio/components/RadioEditModal.tsx @@ -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 diff --git a/src/components/internetRadio/RadioToolbar.tsx b/src/features/radio/components/RadioToolbar.tsx similarity index 100% rename from src/components/internetRadio/RadioToolbar.tsx rename to src/features/radio/components/RadioToolbar.tsx diff --git a/src/hooks/useRadioMetadata.ts b/src/features/radio/hooks/useRadioMetadata.ts similarity index 98% rename from src/hooks/useRadioMetadata.ts rename to src/features/radio/hooks/useRadioMetadata.ts index 64e102d5..c530d82b 100644 --- a/src/hooks/useRadioMetadata.ts +++ b/src/features/radio/hooks/useRadioMetadata.ts @@ -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 ───────────────────────────────────────────────────────────── diff --git a/src/hooks/useRadioMprisSync.test.ts b/src/features/radio/hooks/useRadioMprisSync.test.ts similarity index 93% rename from src/hooks/useRadioMprisSync.test.ts rename to src/features/radio/hooks/useRadioMprisSync.test.ts index 9ca26570..863ef1cf 100644 --- a/src/hooks/useRadioMprisSync.test.ts +++ b/src/features/radio/hooks/useRadioMprisSync.test.ts @@ -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); diff --git a/src/hooks/useRadioMprisSync.ts b/src/features/radio/hooks/useRadioMprisSync.ts similarity index 95% rename from src/hooks/useRadioMprisSync.ts rename to src/features/radio/hooks/useRadioMprisSync.ts index 6ccb0728..20f4bf0f 100644 --- a/src/hooks/useRadioMprisSync.ts +++ b/src/features/radio/hooks/useRadioMprisSync.ts @@ -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). diff --git a/src/features/radio/index.ts b/src/features/radio/index.ts new file mode 100644 index 00000000..6102acbf --- /dev/null +++ b/src/features/radio/index.ts @@ -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'; diff --git a/src/pages/InternetRadio.tsx b/src/features/radio/pages/InternetRadio.tsx similarity index 93% rename from src/pages/InternetRadio.tsx rename to src/features/radio/pages/InternetRadio.tsx index c66d474d..b0da101e 100644 --- a/src/pages/InternetRadio.tsx +++ b/src/features/radio/pages/InternetRadio.tsx @@ -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(); diff --git a/src/pages/NowPlaying.tsx b/src/pages/NowPlaying.tsx index 0cec34fe..3179cecc 100644 --- a/src/pages/NowPlaying.tsx +++ b/src/pages/NowPlaying.tsx @@ -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 {