handle radio metadata in discord rpc / fullscreen player (#1649)

This commit is contained in:
jeffvli
2026-02-05 19:14:30 -08:00
parent 4c256348fc
commit 027e4046a2
10 changed files with 332 additions and 124 deletions
@@ -4,6 +4,10 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
import { api } from '/@/renderer/api'; import { api } from '/@/renderer/api';
import { useItemImageUrl } from '/@/renderer/components/item-image/item-image'; import { useItemImageUrl } from '/@/renderer/components/item-image/item-image';
import {
useIsRadioActive,
useRadioPlayer,
} from '/@/renderer/features/radio/hooks/use-radio-player';
import { import {
DiscordDisplayType, DiscordDisplayType,
DiscordLinkType, DiscordLinkType,
@@ -37,6 +41,9 @@ export const useDiscordRpc = () => {
const privateMode = useAppStore((state) => state.privateMode); const privateMode = useAppStore((state) => state.privateMode);
const [lastUniqueId, setlastUniqueId] = useState(''); const [lastUniqueId, setlastUniqueId] = useState('');
const isRadioActive = useIsRadioActive();
const { isPlaying: isRadioPlaying, metadata: radioMetadata, stationName } = useRadioPlayer();
const currentSong = usePlayerSong(); const currentSong = usePlayerSong();
const imageUrl = useItemImageUrl({ const imageUrl = useItemImageUrl({
id: currentSong?.imageId || undefined, id: currentSong?.imageId || undefined,
@@ -67,14 +74,17 @@ export const useDiscordRpc = () => {
: song !== previousSong; : song !== previousSong;
const trackChanged = song ? lastUniqueId !== song._uniqueId : false; const trackChanged = song ? lastUniqueId !== song._uniqueId : false;
const isPlayingRadio = isRadioActive && isRadioPlaying;
const hasTrackOrRadio = Boolean(current[0]) || isPlayingRadio;
if ( if (
!current[0] || // No track !hasTrackOrRadio || // No track and not playing radio
(current[2] === 'paused' && !discordSettings.showPaused) // Track paused with show paused setting disabled (current[2] === 'paused' && !discordSettings.showPaused) // Paused with show paused setting disabled
) { ) {
let reason: string; let reason: string;
if (!current[0]) { if (!hasTrackOrRadio) {
reason = 'no_track'; reason = current[0] ? 'no_track' : 'no_track_or_radio';
} else if (current[1] === 0) { } else if (current[1] === 0 && !isPlayingRadio) {
reason = 'start_of_track'; reason = 'start_of_track';
} else { } else {
reason = 'paused_with_show_paused_disabled'; reason = 'paused_with_show_paused_disabled';
@@ -90,6 +100,46 @@ export const useDiscordRpc = () => {
return discordRpc?.clearActivity(); 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) { if (!song) {
return; return;
} }
@@ -306,6 +356,11 @@ export const useDiscordRpc = () => {
discordSettings.linkType, discordSettings.linkType,
lastUniqueId, lastUniqueId,
currentSong?._uniqueId, currentSong?._uniqueId,
isRadioActive,
isRadioPlaying,
radioMetadata?.artist,
radioMetadata?.title,
stationName,
], ],
); );
@@ -6,6 +6,10 @@ import { generatePath, Link } from 'react-router';
import styles from './full-screen-player-image.module.css'; import styles from './full-screen-player-image.module.css';
import { useItemImageUrl } from '/@/renderer/components/item-image/item-image'; 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 { AppRoute } from '/@/renderer/router/routes';
import { useNativeAspectRatio, usePlayerData, usePlayerSong } from '/@/renderer/store'; import { useNativeAspectRatio, usePlayerData, usePlayerSong } from '/@/renderer/store';
import { Badge } from '/@/shared/components/badge/badge'; import { Badge } from '/@/shared/components/badge/badge';
@@ -45,8 +49,9 @@ const MotionImage = motion.img;
const ImageWithPlaceholder = ({ const ImageWithPlaceholder = ({
className, className,
placeholderIcon = 'itemAlbum',
...props ...props
}: HTMLMotionProps<'img'> & { placeholder?: string }) => { }: HTMLMotionProps<'img'> & { placeholder?: string; placeholderIcon?: 'itemAlbum' | 'radio' }) => {
const nativeAspectRatio = useNativeAspectRatio(); const nativeAspectRatio = useNativeAspectRatio();
if (!props.src) { if (!props.src) {
@@ -59,7 +64,7 @@ const ImageWithPlaceholder = ({
width: '100%', width: '100%',
}} }}
> >
<Icon color="muted" icon="itemAlbum" size="25%" /> <Icon color="muted" icon={placeholderIcon} size="25%" />
</Center> </Center>
); );
} }
@@ -79,9 +84,14 @@ const ImageWithPlaceholder = ({
export const FullScreenPlayerImage = () => { export const FullScreenPlayerImage = () => {
const mainImageRef = useRef<HTMLImageElement | null>(null); const mainImageRef = useRef<HTMLImageElement | null>(null);
const isRadioActive = useIsRadioActive();
const { isPlaying: isRadioPlaying, metadata: radioMetadata, stationName } = useRadioPlayer();
const currentSong = usePlayerSong(); const currentSong = usePlayerSong();
const { nextSong } = usePlayerData(); const { nextSong } = usePlayerData();
const isPlayingRadio = isRadioActive && isRadioPlaying;
const currentImageUrl = useItemImageUrl({ const currentImageUrl = useItemImageUrl({
id: currentSong?.imageId || undefined, id: currentSong?.imageId || undefined,
itemType: LibraryItem.SONG, itemType: LibraryItem.SONG,
@@ -111,8 +121,11 @@ export const FullScreenPlayerImage = () => {
imageStateRef.current = imageState; imageStateRef.current = imageState;
}, [imageState]); }, [imageState]);
// Update images when song or size changes // Update images when song or size changes (skip when playing radio - no album art)
useEffect(() => { useEffect(() => {
if (isPlayingRadio) {
return;
}
if (currentSong?._uniqueId === previousSongRef.current) { if (currentSong?._uniqueId === previousSongRef.current) {
return; return;
} }
@@ -126,7 +139,14 @@ export const FullScreenPlayerImage = () => {
}); });
previousSongRef.current = currentSong?._uniqueId; previousSongRef.current = currentSong?._uniqueId;
}, [currentSong?._uniqueId, currentImageUrl, nextSong?._uniqueId, nextImageUrl, setImageState]); }, [
isPlayingRadio,
currentSong?._uniqueId,
currentImageUrl,
nextSong?._uniqueId,
nextImageUrl,
setImageState,
]);
return ( return (
<Flex <Flex
@@ -138,7 +158,7 @@ export const FullScreenPlayerImage = () => {
> >
<div className={styles.imageContainer} ref={mainImageRef}> <div className={styles.imageContainer} ref={mainImageRef}>
<AnimatePresence initial={false} mode="sync"> <AnimatePresence initial={false} mode="sync">
{imageState.current === 0 && ( {!isPlayingRadio && imageState.current === 0 && (
<ImageWithPlaceholder <ImageWithPlaceholder
animate="open" animate="open"
className="full-screen-player-image" className="full-screen-player-image"
@@ -153,7 +173,7 @@ export const FullScreenPlayerImage = () => {
/> />
)} )}
{imageState.current === 1 && ( {!isPlayingRadio && imageState.current === 1 && (
<ImageWithPlaceholder <ImageWithPlaceholder
animate="open" animate="open"
className="full-screen-player-image" className="full-screen-player-image"
@@ -167,57 +187,85 @@ export const FullScreenPlayerImage = () => {
variants={imageVariants} variants={imageVariants}
/> />
)} )}
{isPlayingRadio && (
<ImageWithPlaceholder
animate="open"
className="full-screen-player-image"
custom={{ isOpen: true }}
draggable={false}
exit="closed"
initial="closed"
key="radio"
placeholder="var(--theme-colors-foreground-muted)"
placeholderIcon="radio"
src=""
variants={imageVariants}
/>
)}
</AnimatePresence> </AnimatePresence>
</div> </div>
<Stack className={styles.metadataContainer} gap="md" maw="100%"> <Stack className={styles.metadataContainer} gap="md" maw="100%">
<Text fw={900} lh="1.2" overflow="hidden" size="4xl" w="100%"> <Text fw={900} lh="1.2" overflow="hidden" size="4xl" w="100%">
{currentSong?.name} {isPlayingRadio
</Text> ? radioMetadata?.title || stationName || 'Radio'
<Text : currentSong?.name}
component={Link}
isLink
overflow="hidden"
size="xl"
to={generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, {
albumId: currentSong?.albumId || '',
})}
w="100%"
>
{currentSong?.album}
</Text> </Text>
{isPlayingRadio ? (
<Text overflow="hidden" size="xl" w="100%">
{stationName || 'Radio'}
</Text>
) : (
<Text
component={Link}
isLink
overflow="hidden"
size="xl"
to={generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, {
albumId: currentSong?.albumId || '',
})}
w="100%"
>
{currentSong?.album}
</Text>
)}
<Text key="fs-artists"> <Text key="fs-artists">
{currentSong?.artists?.map((artist, index) => ( {isPlayingRadio
<Fragment key={`fs-artist-${artist.id}`}> ? radioMetadata?.artist || stationName || 'Radio'
{index > 0 && ( : currentSong?.artists?.map((artist, index) => (
<Text <Fragment key={`fs-artist-${artist.id}`}>
style={{ {index > 0 && (
display: 'inline-block', <Text
padding: '0 0.5rem', style={{
}} display: 'inline-block',
> padding: '0 0.5rem',
}}
</Text> >
)}
<Text </Text>
component={Link} )}
isLink <Text
to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, { component={Link}
albumArtistId: artist.id, isLink
})} to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
> albumArtistId: artist.id,
{artist.name} })}
</Text> >
</Fragment> {artist.name}
))} </Text>
</Fragment>
))}
</Text> </Text>
<Group justify="center" mt="sm"> {!isPlayingRadio && (
{currentSong?.container && ( <Group justify="center" mt="sm">
<Badge variant="transparent">{currentSong?.container}</Badge> {currentSong?.container && (
)} <Badge variant="transparent">{currentSong?.container}</Badge>
{currentSong?.releaseYear && ( )}
<Badge variant="transparent">{currentSong?.releaseYear}</Badge> {currentSong?.releaseYear && (
)} <Badge variant="transparent">{currentSong?.releaseYear}</Badge>
</Group> )}
</Group>
)}
</Stack> </Stack>
</Flex> </Flex>
); );
@@ -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 { 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 { FullScreenPlayerImage } from '/@/renderer/features/player/components/full-screen-player-image';
import { FullScreenPlayerQueue } from '/@/renderer/features/player/components/full-screen-player-queue'; 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 { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
import { useFastAverageColor } from '/@/renderer/hooks'; import { useFastAverageColor } from '/@/renderer/hooks';
import { import {
@@ -657,6 +661,11 @@ export const FullScreenPlayer = () => {
const { dynamicBackground, dynamicImageBlur, dynamicIsImage } = useFullScreenPlayerStore(); const { dynamicBackground, dynamicImageBlur, dynamicIsImage } = useFullScreenPlayerStore();
const { setStore } = useFullScreenPlayerStoreActions(); const { setStore } = useFullScreenPlayerStoreActions();
const { windowBarStyle } = useWindowSettings(); const { windowBarStyle } = useWindowSettings();
const isRadioActive = useIsRadioActive();
const { isPlaying: isRadioPlaying } = useRadioPlayer();
const isPlayingRadio = isRadioActive && isRadioPlaying;
const effectiveDynamicBackground = dynamicBackground && !isPlayingRadio;
const location = useLocation(); const location = useLocation();
const isOpenedRef = useRef<boolean | null>(null); const isOpenedRef = useRef<boolean | null>(null);
@@ -671,13 +680,13 @@ export const FullScreenPlayer = () => {
return ( return (
<PlayerContainer <PlayerContainer
dynamicBackground={dynamicBackground} dynamicBackground={effectiveDynamicBackground}
dynamicIsImage={dynamicIsImage} dynamicIsImage={dynamicIsImage}
windowBarStyle={windowBarStyle} windowBarStyle={windowBarStyle}
> >
<Controls /> <Controls />
<BackgroundImageOverlay <BackgroundImageOverlay
dynamicBackground={dynamicBackground} dynamicBackground={effectiveDynamicBackground}
dynamicImageBlur={dynamicImageBlur} dynamicImageBlur={dynamicImageBlur}
/> />
<div className={styles.responsiveContainer}> <div className={styles.responsiveContainer}>
@@ -50,6 +50,11 @@
object-fit: var(--theme-image-fit); object-fit: var(--theme-image-fit);
} }
.radio-image {
background: var(--theme-colors-surface);
border-radius: var(--theme-radius-md);
}
.line-item { .line-item {
display: inline-block; display: inline-block;
width: 100%; width: 100%;
@@ -21,7 +21,9 @@ import {
useSetFullScreenPlayerStore, useSetFullScreenPlayerStore,
} from '/@/renderer/store'; } from '/@/renderer/store';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { Center } from '/@/shared/components/center/center';
import { Group } from '/@/shared/components/group/group'; import { Group } from '/@/shared/components/group/group';
import { Icon } from '/@/shared/components/icon/icon';
import { Separator } from '/@/shared/components/separator/separator'; import { Separator } from '/@/shared/components/separator/separator';
import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
import { Tooltip } from '/@/shared/components/tooltip/tooltip'; import { Tooltip } from '/@/shared/components/tooltip/tooltip';
@@ -48,7 +50,7 @@ export const LeftControls = () => {
const { bindings } = useHotkeySettings(); const { bindings } = useHotkeySettings();
const isRadioMode = isRadioActive; const isRadioMode = isRadioActive;
const hideImage = (image && !collapsed) || isRadioMode; const hideImage = image && !collapsed;
const isSongDefined = Boolean(currentSong?.id) && !isRadioMode; const isSongDefined = Boolean(currentSong?.id) && !isRadioMode;
const title = currentSong?.name; const title = currentSong?.name;
const artists = currentSong?.artists; const artists = currentSong?.artists;
@@ -116,20 +118,31 @@ export const LeftControls = () => {
})} })}
openDelay={0} openDelay={0}
> >
<ItemImage {isRadioMode ? (
className={clsx( <Center
styles.playerbarImage, className={clsx(
PlaybackSelectors.playerCoverArt, styles.playerbarImage,
)} styles.radioImage,
enableDebounce={false} )}
enableViewport={false} >
explicitStatus={currentSong?.explicitStatus} <Icon color="muted" icon="radio" size="40%" />
fetchPriority="high" </Center>
id={currentSong?.imageId} ) : (
itemType={LibraryItem.SONG} <ItemImage
serverId={currentSong?._serverId} className={clsx(
type="table" styles.playerbarImage,
/> PlaybackSelectors.playerCoverArt,
)}
enableDebounce={false}
enableViewport={false}
explicitStatus={currentSong?.explicitStatus}
fetchPriority="high"
id={currentSong?.imageId}
itemType={LibraryItem.SONG}
serverId={currentSong?._serverId}
type="table"
/>
)}
</Tooltip> </Tooltip>
{!collapsed && ( {!collapsed && (
<ActionIcon <ActionIcon
@@ -5,6 +5,10 @@ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react
import styles from './mobile-fullscreen-player.module.css'; import styles from './mobile-fullscreen-player.module.css';
import { useItemImageUrl } from '/@/renderer/components/item-image/item-image'; import { useItemImageUrl } from '/@/renderer/components/item-image/item-image';
import {
useIsRadioActive,
useRadioPlayer,
} from '/@/renderer/features/radio/hooks/use-radio-player';
import { import {
useFullScreenPlayerStore, useFullScreenPlayerStore,
useImageRes, useImageRes,
@@ -44,9 +48,14 @@ const MotionImage = motion.img;
const ImageWithPlaceholder = ({ const ImageWithPlaceholder = ({
className, className,
placeholderIcon,
useImageAspectRatio, useImageAspectRatio,
...props ...props
}: HTMLMotionProps<'img'> & { placeholder?: string; useImageAspectRatio?: boolean }) => { }: HTMLMotionProps<'img'> & {
placeholder?: string;
placeholderIcon?: 'itemAlbum' | 'radio';
useImageAspectRatio?: boolean;
}) => {
if (!props.src) { if (!props.src) {
return ( return (
<Center <Center
@@ -57,7 +66,11 @@ const ImageWithPlaceholder = ({
width: '100%', width: '100%',
}} }}
> >
<Icon color="muted" icon="itemAlbum" size="25%" /> <Icon
color="muted"
icon={placeholderIcon === 'radio' ? 'radio' : 'itemAlbum'}
size="25%"
/>
</Center> </Center>
); );
} }
@@ -80,9 +93,13 @@ export const MobileFullscreenPlayerAlbumArt = () => {
const { fullScreenPlayer: albumArtRes } = useImageRes(); const { fullScreenPlayer: albumArtRes } = useImageRes();
const { useImageAspectRatio } = useFullScreenPlayerStore(); const { useImageAspectRatio } = useFullScreenPlayerStore();
const isRadioActive = useIsRadioActive();
const { isPlaying: isRadioPlaying } = useRadioPlayer();
const currentSong = usePlayerSong(); const currentSong = usePlayerSong();
const { nextSong } = usePlayerData(); const { nextSong } = usePlayerData();
const isPlayingRadio = isRadioActive && isRadioPlaying;
const currentImageUrl = useItemImageUrl({ const currentImageUrl = useItemImageUrl({
id: currentSong?.imageId || undefined, id: currentSong?.imageId || undefined,
itemType: LibraryItem.SONG, itemType: LibraryItem.SONG,
@@ -151,38 +168,58 @@ export const MobileFullscreenPlayerAlbumArt = () => {
})} })}
> >
<AnimatePresence initial={false} mode="sync"> <AnimatePresence initial={false} mode="sync">
{imageState.current === 0 && ( {isPlayingRadio ? (
<ImageWithPlaceholder <ImageWithPlaceholder
animate="open" animate="open"
className={PlaybackSelectors.playerCoverArt} className={PlaybackSelectors.playerCoverArt}
custom={{ isOpen: imageState.current === 0 }} custom={{ isOpen: true }}
draggable={false} draggable={false}
exit="closed" exit="closed"
initial="closed" initial="closed"
key={`top-${currentSong?._uniqueId || 'none'}`} key="radio"
loading="eager" loading="eager"
placeholder="var(--theme-colors-foreground-muted)" placeholder="var(--theme-colors-foreground-muted)"
src={imageState.topImage || ''} placeholderIcon="radio"
src=""
useImageAspectRatio={useImageAspectRatio} useImageAspectRatio={useImageAspectRatio}
variants={imageVariants} variants={imageVariants}
/> />
)} ) : (
<>
{imageState.current === 0 && (
<ImageWithPlaceholder
animate="open"
className={PlaybackSelectors.playerCoverArt}
custom={{ isOpen: imageState.current === 0 }}
draggable={false}
exit="closed"
initial="closed"
key={`top-${currentSong?._uniqueId || 'none'}`}
loading="eager"
placeholder="var(--theme-colors-foreground-muted)"
src={imageState.topImage || ''}
useImageAspectRatio={useImageAspectRatio}
variants={imageVariants}
/>
)}
{imageState.current === 1 && ( {imageState.current === 1 && (
<ImageWithPlaceholder <ImageWithPlaceholder
animate="open" animate="open"
className={PlaybackSelectors.playerCoverArt} className={PlaybackSelectors.playerCoverArt}
custom={{ isOpen: imageState.current === 1 }} custom={{ isOpen: imageState.current === 1 }}
draggable={false} draggable={false}
exit="closed" exit="closed"
initial="closed" initial="closed"
key={`bottom-${currentSong?._uniqueId || 'none'}`} key={`bottom-${currentSong?._uniqueId || 'none'}`}
loading="eager" loading="eager"
placeholder="var(--theme-colors-foreground-muted)" placeholder="var(--theme-colors-foreground-muted)"
src={imageState.bottomImage || ''} src={imageState.bottomImage || ''}
useImageAspectRatio={useImageAspectRatio} useImageAspectRatio={useImageAspectRatio}
variants={imageVariants} variants={imageVariants}
/> />
)}
</>
)} )}
</AnimatePresence> </AnimatePresence>
</div> </div>
@@ -16,6 +16,9 @@ interface MobileFullscreenPlayerMetadataProps {
currentSong?: QueueSong; currentSong?: QueueSong;
onToggleFavorite: (e: MouseEvent<HTMLButtonElement>) => void; onToggleFavorite: (e: MouseEvent<HTMLButtonElement>) => void;
onUpdateRating: (rating: number) => void; onUpdateRating: (rating: number) => void;
radioArtist?: string;
radioStationName?: string;
radioTitle?: string;
showRating?: boolean; showRating?: boolean;
} }
@@ -24,17 +27,24 @@ export const MobileFullscreenPlayerMetadata = memo(
currentSong, currentSong,
onToggleFavorite, onToggleFavorite,
onUpdateRating, onUpdateRating,
radioArtist,
radioStationName,
radioTitle,
showRating, showRating,
}: MobileFullscreenPlayerMetadataProps) => { }: MobileFullscreenPlayerMetadataProps) => {
const title = currentSong?.name; const isRadio = radioTitle !== undefined || radioStationName !== undefined;
const artists = currentSong?.artists;
const album = currentSong?.album; 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 container = currentSong?.container;
const year = currentSong?.releaseYear; const year = currentSong?.releaseYear;
const isFavorite = currentSong?.userFavorite; const isFavorite = currentSong?.userFavorite;
const rating = currentSong?.userRating; const rating = currentSong?.userRating;
const hasMetadata = container || year; const hasMetadata = !isRadio && (container || year);
return ( return (
<div className={styles.metadataContainer}> <div className={styles.metadataContainer}>
@@ -49,7 +59,7 @@ export const MobileFullscreenPlayerMetadata = memo(
</TextTitle> </TextTitle>
</div> </div>
<Text className={clsx(PlaybackSelectors.songArtist)} size="md" truncate> <Text className={clsx(PlaybackSelectors.songArtist)} size="md" truncate>
{artists?.map((a) => a.name).join(', ') || '—'} {artistsDisplay || '—'}
</Text> </Text>
<Text className={clsx(PlaybackSelectors.songAlbum)} size="md" truncate> <Text className={clsx(PlaybackSelectors.songAlbum)} size="md" truncate>
{album || '—'} {album || '—'}
@@ -65,21 +75,23 @@ export const MobileFullscreenPlayerMetadata = memo(
)} )}
</Group> </Group>
)} )}
<Group align="center" className={styles.actionsRow} gap="xs"> {!isRadio && (
<ActionIcon <Group align="center" className={styles.actionsRow} gap="xs">
icon="favorite" <ActionIcon
iconProps={{ icon="favorite"
fill: isFavorite ? 'primary' : undefined, iconProps={{
size: 'md', fill: isFavorite ? 'primary' : undefined,
}} size: 'md',
onClick={onToggleFavorite} }}
size="sm" onClick={onToggleFavorite}
variant="subtle" size="sm"
/> variant="subtle"
{showRating && ( />
<Rating onChange={onUpdateRating} size="sm" value={rating || 0} /> {showRating && (
)} <Rating onChange={onUpdateRating} size="sm" value={rating || 0} />
</Group> )}
</Group>
)}
</div> </div>
); );
}, },
@@ -24,6 +24,10 @@ import { MobileFullscreenPlayerControls } from '/@/renderer/features/player/comp
import { MobileFullscreenPlayerHeader } from '/@/renderer/features/player/components/mobile-fullscreen-player-header'; import { MobileFullscreenPlayerHeader } from '/@/renderer/features/player/components/mobile-fullscreen-player-header';
import { MobileFullscreenPlayerMetadata } from '/@/renderer/features/player/components/mobile-fullscreen-player-metadata'; import { MobileFullscreenPlayerMetadata } from '/@/renderer/features/player/components/mobile-fullscreen-player-metadata';
import { MobileFullscreenPlayerProgress } from '/@/renderer/features/player/components/mobile-fullscreen-player-progress'; 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 { useSetFavorite } from '/@/renderer/features/shared/hooks/use-set-favorite';
import { useSetRating } from '/@/renderer/features/shared/hooks/use-set-rating'; import { useSetRating } from '/@/renderer/features/shared/hooks/use-set-rating';
import { useFastAverageColor } from '/@/renderer/hooks'; import { useFastAverageColor } from '/@/renderer/hooks';
@@ -376,7 +380,12 @@ export const MobileFullscreenPlayer = () => {
useFullScreenPlayerStore(); useFullScreenPlayerStore();
const currentSong = usePlayerSong(); const currentSong = usePlayerSong();
const { currentSong: currentSongData } = usePlayerData(); const { currentSong: currentSongData } = usePlayerData();
const isRadioActive = useIsRadioActive();
const { isPlaying: isRadioPlaying, metadata: radioMetadata, stationName } = useRadioPlayer();
const server = useCurrentServer(); const server = useCurrentServer();
const isPlayingRadio = isRadioActive && isRadioPlaying;
const effectiveDynamicBackground = dynamicBackground && !isPlayingRadio;
const setFavorite = useSetFavorite(); const setFavorite = useSetFavorite();
const { showRatings: showRatingsSetting } = useGeneralSettings(); const { showRatings: showRatingsSetting } = useGeneralSettings();
const setRating = useSetRating(); const setRating = useSetRating();
@@ -443,11 +452,11 @@ export const MobileFullscreenPlayer = () => {
return ( return (
<MobilePlayerContainer <MobilePlayerContainer
dynamicBackground={dynamicBackground} dynamicBackground={effectiveDynamicBackground}
dynamicIsImage={dynamicIsImage} dynamicIsImage={dynamicIsImage}
> >
<BackgroundImageOverlay <BackgroundImageOverlay
dynamicBackground={dynamicBackground} dynamicBackground={effectiveDynamicBackground}
dynamicImageBlur={dynamicImageBlur} dynamicImageBlur={dynamicImageBlur}
/> />
<motion.div <motion.div
@@ -470,6 +479,9 @@ export const MobileFullscreenPlayer = () => {
currentSong={currentSong} currentSong={currentSong}
onToggleFavorite={handleToggleFavorite} onToggleFavorite={handleToggleFavorite}
onUpdateRating={handleUpdateRating} onUpdateRating={handleUpdateRating}
radioArtist={isPlayingRadio ? (radioMetadata?.artist ?? undefined) : undefined}
radioStationName={isPlayingRadio ? (stationName ?? undefined) : undefined}
radioTitle={isPlayingRadio ? (radioMetadata?.title ?? undefined) : undefined}
showRating={showRating} showRating={showRating}
/> />
<MobileFullscreenPlayerProgress currentSong={currentSong} /> <MobileFullscreenPlayerProgress currentSong={currentSong} />
@@ -4,7 +4,6 @@ import { useTranslation } from 'react-i18next';
import styles from './server-selector.module.css'; 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 JellyfinLogo from '/@/renderer/features/servers/assets/jellyfin.png';
import NavidromeLogo from '/@/renderer/features/servers/assets/navidrome.png'; import NavidromeLogo from '/@/renderer/features/servers/assets/navidrome.png';
import OpenSubsonicLogo from '/@/renderer/features/servers/assets/opensubsonic.png'; import OpenSubsonicLogo from '/@/renderer/features/servers/assets/opensubsonic.png';
@@ -26,8 +25,7 @@ export const ServerSelector = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const currentServer = useCurrentServer(); const currentServer = useCurrentServer();
const sidebarImageEnabled = useAppStore((state) => state.sidebar.image); const sidebarImageEnabled = useAppStore((state) => state.sidebar.image);
const isRadioPlaying = useRadioStore((state) => state.isPlaying); const showImage = sidebarImageEnabled;
const showImage = sidebarImageEnabled && !isRadioPlaying;
const { data: musicFolders } = useQuery( const { data: musicFolders } = useQuery(
currentServer currentServer
@@ -7,7 +7,10 @@ import styles from './sidebar.module.css';
import { useItemImageUrl } from '/@/renderer/components/item-image/item-image'; import { useItemImageUrl } from '/@/renderer/components/item-image/item-image';
import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller'; 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 { ActionBar } from '/@/renderer/features/sidebar/components/action-bar';
import { ServerSelector } from '/@/renderer/features/sidebar/components/server-selector'; import { ServerSelector } from '/@/renderer/features/sidebar/components/server-selector';
import { SidebarCollectionList } from '/@/renderer/features/sidebar/components/sidebar-collection-list'; import { SidebarCollectionList } from '/@/renderer/features/sidebar/components/sidebar-collection-list';
@@ -32,7 +35,9 @@ import {
} from '/@/renderer/store/settings.store'; } from '/@/renderer/store/settings.store';
import { Accordion } from '/@/shared/components/accordion/accordion'; import { Accordion } from '/@/shared/components/accordion/accordion';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { Center } from '/@/shared/components/center/center';
import { Group } from '/@/shared/components/group/group'; import { Group } from '/@/shared/components/group/group';
import { Icon } from '/@/shared/components/icon/icon';
import { ImageUnloader } from '/@/shared/components/image/image'; import { ImageUnloader } from '/@/shared/components/image/image';
import { ScrollArea } from '/@/shared/components/scroll-area/scroll-area'; import { ScrollArea } from '/@/shared/components/scroll-area/scroll-area';
import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
@@ -68,8 +73,7 @@ export const Sidebar = () => {
const sidebarItems = useSidebarItems(); const sidebarItems = useSidebarItems();
const { windowBarStyle } = useWindowSettings(); const { windowBarStyle } = useWindowSettings();
const sidebarImageEnabled = useAppStore((state) => state.sidebar.image); const sidebarImageEnabled = useAppStore((state) => state.sidebar.image);
const isRadioPlaying = useRadioStore((state) => state.isPlaying); const showImage = sidebarImageEnabled;
const showImage = sidebarImageEnabled && !isRadioPlaying;
const sidebarItemsWithRoute: SidebarItemType[] = useMemo(() => { const sidebarItemsWithRoute: SidebarItemType[] = useMemo(() => {
if (!sidebarItems) return []; if (!sidebarItems) return [];
@@ -161,6 +165,8 @@ const SidebarImage = () => {
const leftWidth = useAppStore((state) => state.sidebar.leftWidth); const leftWidth = useAppStore((state) => state.sidebar.leftWidth);
const { setSideBar } = useAppStoreActions(); const { setSideBar } = useAppStoreActions();
const currentSong = usePlayerSong(); const currentSong = usePlayerSong();
const isRadioActive = useIsRadioActive();
const { isPlaying: isRadioPlaying } = useRadioPlayer();
const imageUrl = useItemImageUrl({ const imageUrl = useItemImageUrl({
id: currentSong?.imageId || undefined, id: currentSong?.imageId || undefined,
@@ -169,6 +175,7 @@ const SidebarImage = () => {
type: 'sidebar', type: 'sidebar',
}); });
const isPlayingRadio = isRadioActive && isRadioPlaying;
const isSongDefined = Boolean(currentSong?.id); const isSongDefined = Boolean(currentSong?.id);
const setFullScreenPlayerStore = useSetFullScreenPlayerStore(); const setFullScreenPlayerStore = useSetFullScreenPlayerStore();
@@ -181,7 +188,7 @@ const SidebarImage = () => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
if (!currentSong) { if (!currentSong || isPlayingRadio) {
return; return;
} }
@@ -215,7 +222,19 @@ const SidebarImage = () => {
postProcess: 'sentenceCase', postProcess: 'sentenceCase',
})} })}
> >
{imageUrl ? ( {isPlayingRadio ? (
<Center
className={styles.sidebarImage}
style={{
background: 'var(--theme-colors-surface)',
borderRadius: 'var(--theme-card-default-radius)',
height: '100%',
width: '100%',
}}
>
<Icon color="muted" icon="radio" size="40%" />
</Center>
) : imageUrl ? (
<img className={styles.sidebarImage} loading="eager" src={imageUrl} /> <img className={styles.sidebarImage} loading="eager" src={imageUrl} />
) : ( ) : (
<ImageUnloader icon="emptySongImage" /> <ImageUnloader icon="emptySongImage" />