diff --git a/src/renderer/features/discord-rpc/use-discord-rpc.ts b/src/renderer/features/discord-rpc/use-discord-rpc.ts index 633f2ad7a..fdcd7d9cd 100644 --- a/src/renderer/features/discord-rpc/use-discord-rpc.ts +++ b/src/renderer/features/discord-rpc/use-discord-rpc.ts @@ -4,6 +4,10 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { api } from '/@/renderer/api'; import { useItemImageUrl } from '/@/renderer/components/item-image/item-image'; +import { + useIsRadioActive, + useRadioPlayer, +} from '/@/renderer/features/radio/hooks/use-radio-player'; import { DiscordDisplayType, DiscordLinkType, @@ -37,6 +41,9 @@ export const useDiscordRpc = () => { const privateMode = useAppStore((state) => state.privateMode); const [lastUniqueId, setlastUniqueId] = useState(''); + const isRadioActive = useIsRadioActive(); + const { isPlaying: isRadioPlaying, metadata: radioMetadata, stationName } = useRadioPlayer(); + const currentSong = usePlayerSong(); const imageUrl = useItemImageUrl({ id: currentSong?.imageId || undefined, @@ -67,14 +74,17 @@ export const useDiscordRpc = () => { : song !== previousSong; const trackChanged = song ? lastUniqueId !== song._uniqueId : false; + const isPlayingRadio = isRadioActive && isRadioPlaying; + const hasTrackOrRadio = Boolean(current[0]) || isPlayingRadio; + if ( - !current[0] || // No track - (current[2] === 'paused' && !discordSettings.showPaused) // Track paused with show paused setting disabled + !hasTrackOrRadio || // No track and not playing radio + (current[2] === 'paused' && !discordSettings.showPaused) // Paused with show paused setting disabled ) { let reason: string; - if (!current[0]) { - reason = 'no_track'; - } else if (current[1] === 0) { + if (!hasTrackOrRadio) { + reason = current[0] ? 'no_track' : 'no_track_or_radio'; + } else if (current[1] === 0 && !isPlayingRadio) { reason = 'start_of_track'; } else { reason = 'paused_with_show_paused_disabled'; @@ -90,6 +100,46 @@ export const useDiscordRpc = () => { return discordRpc?.clearActivity(); } + if (isPlayingRadio) { + const title = radioMetadata?.title || stationName || 'Radio'; + const artist = radioMetadata?.artist || stationName || ''; + + const activity: SetActivity = { + details: truncate(title), + instance: false, + largeImageKey: 'icon', + largeImageText: truncate(stationName || 'Radio'), + smallImageKey: current[2] === PlayerStatus.PLAYING ? 'playing' : 'paused', + smallImageText: sentenceCase(current[2]), + state: truncate(artist), + statusDisplayType: StatusDisplayType.STATE, + type: discordSettings.showAsListening ? 2 : 0, + }; + + const isConnected = await discordRpc?.isConnected(); + if (!isConnected) { + logFn.debug(logMsg[LogCategory.EXTERNAL].discordRpcInitialized, { + category: LogCategory.EXTERNAL, + meta: { clientId: discordSettings.clientId }, + }); + previousEnabledRef.current = true; + await discordRpc?.initialize(discordSettings.clientId); + } + + logFn.debug(logMsg[LogCategory.EXTERNAL].discordRpcSetActivity, { + category: LogCategory.EXTERNAL, + meta: { + currentStatus: current[2], + reason: 'radio', + showAsListening: discordSettings.showAsListening, + stationName: stationName || 'Radio', + title, + }, + }); + discordRpc?.setActivity(activity); + return; + } + if (!song) { return; } @@ -306,6 +356,11 @@ export const useDiscordRpc = () => { discordSettings.linkType, lastUniqueId, currentSong?._uniqueId, + isRadioActive, + isRadioPlaying, + radioMetadata?.artist, + radioMetadata?.title, + stationName, ], ); diff --git a/src/renderer/features/player/components/full-screen-player-image.tsx b/src/renderer/features/player/components/full-screen-player-image.tsx index 29793af00..eb830f832 100644 --- a/src/renderer/features/player/components/full-screen-player-image.tsx +++ b/src/renderer/features/player/components/full-screen-player-image.tsx @@ -6,6 +6,10 @@ import { generatePath, Link } from 'react-router'; import styles from './full-screen-player-image.module.css'; import { useItemImageUrl } from '/@/renderer/components/item-image/item-image'; +import { + useIsRadioActive, + useRadioPlayer, +} from '/@/renderer/features/radio/hooks/use-radio-player'; import { AppRoute } from '/@/renderer/router/routes'; import { useNativeAspectRatio, usePlayerData, usePlayerSong } from '/@/renderer/store'; import { Badge } from '/@/shared/components/badge/badge'; @@ -45,8 +49,9 @@ const MotionImage = motion.img; const ImageWithPlaceholder = ({ className, + placeholderIcon = 'itemAlbum', ...props -}: HTMLMotionProps<'img'> & { placeholder?: string }) => { +}: HTMLMotionProps<'img'> & { placeholder?: string; placeholderIcon?: 'itemAlbum' | 'radio' }) => { const nativeAspectRatio = useNativeAspectRatio(); if (!props.src) { @@ -59,7 +64,7 @@ const ImageWithPlaceholder = ({ width: '100%', }} > - + ); } @@ -79,9 +84,14 @@ const ImageWithPlaceholder = ({ export const FullScreenPlayerImage = () => { const mainImageRef = useRef(null); + const isRadioActive = useIsRadioActive(); + const { isPlaying: isRadioPlaying, metadata: radioMetadata, stationName } = useRadioPlayer(); + const currentSong = usePlayerSong(); const { nextSong } = usePlayerData(); + const isPlayingRadio = isRadioActive && isRadioPlaying; + const currentImageUrl = useItemImageUrl({ id: currentSong?.imageId || undefined, itemType: LibraryItem.SONG, @@ -111,8 +121,11 @@ export const FullScreenPlayerImage = () => { imageStateRef.current = imageState; }, [imageState]); - // Update images when song or size changes + // Update images when song or size changes (skip when playing radio - no album art) useEffect(() => { + if (isPlayingRadio) { + return; + } if (currentSong?._uniqueId === previousSongRef.current) { return; } @@ -126,7 +139,14 @@ export const FullScreenPlayerImage = () => { }); previousSongRef.current = currentSong?._uniqueId; - }, [currentSong?._uniqueId, currentImageUrl, nextSong?._uniqueId, nextImageUrl, setImageState]); + }, [ + isPlayingRadio, + currentSong?._uniqueId, + currentImageUrl, + nextSong?._uniqueId, + nextImageUrl, + setImageState, + ]); return ( { >
- {imageState.current === 0 && ( + {!isPlayingRadio && imageState.current === 0 && ( { /> )} - {imageState.current === 1 && ( + {!isPlayingRadio && imageState.current === 1 && ( { variants={imageVariants} /> )} + + {isPlayingRadio && ( + + )}
- {currentSong?.name} - - - {currentSong?.album} + {isPlayingRadio + ? radioMetadata?.title || stationName || 'Radio' + : currentSong?.name} + {isPlayingRadio ? ( + + {stationName || 'Radio'} + + ) : ( + + {currentSong?.album} + + )} - {currentSong?.artists?.map((artist, index) => ( - - {index > 0 && ( - - • - - )} - - {artist.name} - - - ))} + {isPlayingRadio + ? radioMetadata?.artist || stationName || 'Radio' + : currentSong?.artists?.map((artist, index) => ( + + {index > 0 && ( + + • + + )} + + {artist.name} + + + ))} - - {currentSong?.container && ( - {currentSong?.container} - )} - {currentSong?.releaseYear && ( - {currentSong?.releaseYear} - )} - + {!isPlayingRadio && ( + + {currentSong?.container && ( + {currentSong?.container} + )} + {currentSong?.releaseYear && ( + {currentSong?.releaseYear} + )} + + )}
); diff --git a/src/renderer/features/player/components/full-screen-player.tsx b/src/renderer/features/player/components/full-screen-player.tsx index 7226927d3..e5ee33337 100644 --- a/src/renderer/features/player/components/full-screen-player.tsx +++ b/src/renderer/features/player/components/full-screen-player.tsx @@ -17,6 +17,10 @@ import { useItemImageUrl } from '/@/renderer/components/item-image/item-image'; import { SONG_TABLE_COLUMNS } from '/@/renderer/components/item-list/item-table-list/default-columns'; import { FullScreenPlayerImage } from '/@/renderer/features/player/components/full-screen-player-image'; import { FullScreenPlayerQueue } from '/@/renderer/features/player/components/full-screen-player-queue'; +import { + useIsRadioActive, + useRadioPlayer, +} from '/@/renderer/features/radio/hooks/use-radio-player'; import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu'; import { useFastAverageColor } from '/@/renderer/hooks'; import { @@ -657,6 +661,11 @@ export const FullScreenPlayer = () => { const { dynamicBackground, dynamicImageBlur, dynamicIsImage } = useFullScreenPlayerStore(); const { setStore } = useFullScreenPlayerStoreActions(); const { windowBarStyle } = useWindowSettings(); + const isRadioActive = useIsRadioActive(); + const { isPlaying: isRadioPlaying } = useRadioPlayer(); + + const isPlayingRadio = isRadioActive && isRadioPlaying; + const effectiveDynamicBackground = dynamicBackground && !isPlayingRadio; const location = useLocation(); const isOpenedRef = useRef(null); @@ -671,13 +680,13 @@ export const FullScreenPlayer = () => { return (
diff --git a/src/renderer/features/player/components/left-controls.module.css b/src/renderer/features/player/components/left-controls.module.css index 37ed61921..61b42c7ff 100644 --- a/src/renderer/features/player/components/left-controls.module.css +++ b/src/renderer/features/player/components/left-controls.module.css @@ -50,6 +50,11 @@ object-fit: var(--theme-image-fit); } +.radio-image { + background: var(--theme-colors-surface); + border-radius: var(--theme-radius-md); +} + .line-item { display: inline-block; width: 100%; diff --git a/src/renderer/features/player/components/left-controls.tsx b/src/renderer/features/player/components/left-controls.tsx index a45a839c3..10028cc34 100644 --- a/src/renderer/features/player/components/left-controls.tsx +++ b/src/renderer/features/player/components/left-controls.tsx @@ -21,7 +21,9 @@ import { useSetFullScreenPlayerStore, } from '/@/renderer/store'; import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; +import { Center } from '/@/shared/components/center/center'; import { Group } from '/@/shared/components/group/group'; +import { Icon } from '/@/shared/components/icon/icon'; import { Separator } from '/@/shared/components/separator/separator'; import { Text } from '/@/shared/components/text/text'; import { Tooltip } from '/@/shared/components/tooltip/tooltip'; @@ -48,7 +50,7 @@ export const LeftControls = () => { const { bindings } = useHotkeySettings(); const isRadioMode = isRadioActive; - const hideImage = (image && !collapsed) || isRadioMode; + const hideImage = image && !collapsed; const isSongDefined = Boolean(currentSong?.id) && !isRadioMode; const title = currentSong?.name; const artists = currentSong?.artists; @@ -116,20 +118,31 @@ export const LeftControls = () => { })} openDelay={0} > - + {isRadioMode ? ( +
+ +
+ ) : ( + + )} {!collapsed && ( & { placeholder?: string; useImageAspectRatio?: boolean }) => { +}: HTMLMotionProps<'img'> & { + placeholder?: string; + placeholderIcon?: 'itemAlbum' | 'radio'; + useImageAspectRatio?: boolean; +}) => { if (!props.src) { return (
- +
); } @@ -80,9 +93,13 @@ export const MobileFullscreenPlayerAlbumArt = () => { const { fullScreenPlayer: albumArtRes } = useImageRes(); const { useImageAspectRatio } = useFullScreenPlayerStore(); + const isRadioActive = useIsRadioActive(); + const { isPlaying: isRadioPlaying } = useRadioPlayer(); const currentSong = usePlayerSong(); const { nextSong } = usePlayerData(); + const isPlayingRadio = isRadioActive && isRadioPlaying; + const currentImageUrl = useItemImageUrl({ id: currentSong?.imageId || undefined, itemType: LibraryItem.SONG, @@ -151,38 +168,58 @@ export const MobileFullscreenPlayerAlbumArt = () => { })} > - {imageState.current === 0 && ( + {isPlayingRadio ? ( - )} + ) : ( + <> + {imageState.current === 0 && ( + + )} - {imageState.current === 1 && ( - + {imageState.current === 1 && ( + + )} + )}
diff --git a/src/renderer/features/player/components/mobile-fullscreen-player-metadata.tsx b/src/renderer/features/player/components/mobile-fullscreen-player-metadata.tsx index 5630df629..ee90fb72b 100644 --- a/src/renderer/features/player/components/mobile-fullscreen-player-metadata.tsx +++ b/src/renderer/features/player/components/mobile-fullscreen-player-metadata.tsx @@ -16,6 +16,9 @@ interface MobileFullscreenPlayerMetadataProps { currentSong?: QueueSong; onToggleFavorite: (e: MouseEvent) => void; onUpdateRating: (rating: number) => void; + radioArtist?: string; + radioStationName?: string; + radioTitle?: string; showRating?: boolean; } @@ -24,17 +27,24 @@ export const MobileFullscreenPlayerMetadata = memo( currentSong, onToggleFavorite, onUpdateRating, + radioArtist, + radioStationName, + radioTitle, showRating, }: MobileFullscreenPlayerMetadataProps) => { - const title = currentSong?.name; - const artists = currentSong?.artists; - const album = currentSong?.album; + const isRadio = radioTitle !== undefined || radioStationName !== undefined; + + const title = isRadio ? radioTitle || radioStationName || 'Radio' : currentSong?.name; + const artistsDisplay = isRadio + ? radioArtist || radioStationName || '—' + : currentSong?.artists?.map((a) => a.name).join(', '); + const album = isRadio ? radioStationName || '—' : currentSong?.album; const container = currentSong?.container; const year = currentSong?.releaseYear; const isFavorite = currentSong?.userFavorite; const rating = currentSong?.userRating; - const hasMetadata = container || year; + const hasMetadata = !isRadio && (container || year); return (
@@ -49,7 +59,7 @@ export const MobileFullscreenPlayerMetadata = memo(
- {artists?.map((a) => a.name).join(', ') || '—'} + {artistsDisplay || '—'} {album || '—'} @@ -65,21 +75,23 @@ export const MobileFullscreenPlayerMetadata = memo( )} )} - - - {showRating && ( - - )} - + {!isRadio && ( + + + {showRating && ( + + )} + + )} ); }, diff --git a/src/renderer/features/player/components/mobile-fullscreen-player.tsx b/src/renderer/features/player/components/mobile-fullscreen-player.tsx index 47b5c3214..4483a4696 100644 --- a/src/renderer/features/player/components/mobile-fullscreen-player.tsx +++ b/src/renderer/features/player/components/mobile-fullscreen-player.tsx @@ -24,6 +24,10 @@ import { MobileFullscreenPlayerControls } from '/@/renderer/features/player/comp import { MobileFullscreenPlayerHeader } from '/@/renderer/features/player/components/mobile-fullscreen-player-header'; import { MobileFullscreenPlayerMetadata } from '/@/renderer/features/player/components/mobile-fullscreen-player-metadata'; import { MobileFullscreenPlayerProgress } from '/@/renderer/features/player/components/mobile-fullscreen-player-progress'; +import { + useIsRadioActive, + useRadioPlayer, +} from '/@/renderer/features/radio/hooks/use-radio-player'; import { useSetFavorite } from '/@/renderer/features/shared/hooks/use-set-favorite'; import { useSetRating } from '/@/renderer/features/shared/hooks/use-set-rating'; import { useFastAverageColor } from '/@/renderer/hooks'; @@ -376,7 +380,12 @@ export const MobileFullscreenPlayer = () => { useFullScreenPlayerStore(); const currentSong = usePlayerSong(); const { currentSong: currentSongData } = usePlayerData(); + const isRadioActive = useIsRadioActive(); + const { isPlaying: isRadioPlaying, metadata: radioMetadata, stationName } = useRadioPlayer(); const server = useCurrentServer(); + + const isPlayingRadio = isRadioActive && isRadioPlaying; + const effectiveDynamicBackground = dynamicBackground && !isPlayingRadio; const setFavorite = useSetFavorite(); const { showRatings: showRatingsSetting } = useGeneralSettings(); const setRating = useSetRating(); @@ -443,11 +452,11 @@ export const MobileFullscreenPlayer = () => { return ( { currentSong={currentSong} onToggleFavorite={handleToggleFavorite} onUpdateRating={handleUpdateRating} + radioArtist={isPlayingRadio ? (radioMetadata?.artist ?? undefined) : undefined} + radioStationName={isPlayingRadio ? (stationName ?? undefined) : undefined} + radioTitle={isPlayingRadio ? (radioMetadata?.title ?? undefined) : undefined} showRating={showRating} /> diff --git a/src/renderer/features/sidebar/components/server-selector.tsx b/src/renderer/features/sidebar/components/server-selector.tsx index ab8743518..cfd87f72e 100644 --- a/src/renderer/features/sidebar/components/server-selector.tsx +++ b/src/renderer/features/sidebar/components/server-selector.tsx @@ -4,7 +4,6 @@ import { useTranslation } from 'react-i18next'; import styles from './server-selector.module.css'; -import { useRadioStore } from '/@/renderer/features/radio/hooks/use-radio-player'; import JellyfinLogo from '/@/renderer/features/servers/assets/jellyfin.png'; import NavidromeLogo from '/@/renderer/features/servers/assets/navidrome.png'; import OpenSubsonicLogo from '/@/renderer/features/servers/assets/opensubsonic.png'; @@ -26,8 +25,7 @@ export const ServerSelector = () => { const { t } = useTranslation(); const currentServer = useCurrentServer(); const sidebarImageEnabled = useAppStore((state) => state.sidebar.image); - const isRadioPlaying = useRadioStore((state) => state.isPlaying); - const showImage = sidebarImageEnabled && !isRadioPlaying; + const showImage = sidebarImageEnabled; const { data: musicFolders } = useQuery( currentServer diff --git a/src/renderer/features/sidebar/components/sidebar.tsx b/src/renderer/features/sidebar/components/sidebar.tsx index bd67ce1a9..5828594ac 100644 --- a/src/renderer/features/sidebar/components/sidebar.tsx +++ b/src/renderer/features/sidebar/components/sidebar.tsx @@ -7,7 +7,10 @@ import styles from './sidebar.module.css'; import { useItemImageUrl } from '/@/renderer/components/item-image/item-image'; import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller'; -import { useRadioStore } from '/@/renderer/features/radio/hooks/use-radio-player'; +import { + useIsRadioActive, + useRadioPlayer, +} from '/@/renderer/features/radio/hooks/use-radio-player'; import { ActionBar } from '/@/renderer/features/sidebar/components/action-bar'; import { ServerSelector } from '/@/renderer/features/sidebar/components/server-selector'; import { SidebarCollectionList } from '/@/renderer/features/sidebar/components/sidebar-collection-list'; @@ -32,7 +35,9 @@ import { } from '/@/renderer/store/settings.store'; import { Accordion } from '/@/shared/components/accordion/accordion'; import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; +import { Center } from '/@/shared/components/center/center'; import { Group } from '/@/shared/components/group/group'; +import { Icon } from '/@/shared/components/icon/icon'; import { ImageUnloader } from '/@/shared/components/image/image'; import { ScrollArea } from '/@/shared/components/scroll-area/scroll-area'; import { Text } from '/@/shared/components/text/text'; @@ -68,8 +73,7 @@ export const Sidebar = () => { const sidebarItems = useSidebarItems(); const { windowBarStyle } = useWindowSettings(); const sidebarImageEnabled = useAppStore((state) => state.sidebar.image); - const isRadioPlaying = useRadioStore((state) => state.isPlaying); - const showImage = sidebarImageEnabled && !isRadioPlaying; + const showImage = sidebarImageEnabled; const sidebarItemsWithRoute: SidebarItemType[] = useMemo(() => { if (!sidebarItems) return []; @@ -161,6 +165,8 @@ const SidebarImage = () => { const leftWidth = useAppStore((state) => state.sidebar.leftWidth); const { setSideBar } = useAppStoreActions(); const currentSong = usePlayerSong(); + const isRadioActive = useIsRadioActive(); + const { isPlaying: isRadioPlaying } = useRadioPlayer(); const imageUrl = useItemImageUrl({ id: currentSong?.imageId || undefined, @@ -169,6 +175,7 @@ const SidebarImage = () => { type: 'sidebar', }); + const isPlayingRadio = isRadioActive && isRadioPlaying; const isSongDefined = Boolean(currentSong?.id); const setFullScreenPlayerStore = useSetFullScreenPlayerStore(); @@ -181,7 +188,7 @@ const SidebarImage = () => { e.preventDefault(); e.stopPropagation(); - if (!currentSong) { + if (!currentSong || isPlayingRadio) { return; } @@ -215,7 +222,19 @@ const SidebarImage = () => { postProcess: 'sentenceCase', })} > - {imageUrl ? ( + {isPlayingRadio ? ( +
+ +
+ ) : imageUrl ? ( ) : (