mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 12:30:12 +02:00
handle radio metadata in discord rpc / fullscreen player (#1649)
This commit is contained in:
@@ -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,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -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%',
|
||||
}}
|
||||
>
|
||||
<Icon color="muted" icon="itemAlbum" size="25%" />
|
||||
<Icon color="muted" icon={placeholderIcon} size="25%" />
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
@@ -79,9 +84,14 @@ const ImageWithPlaceholder = ({
|
||||
export const FullScreenPlayerImage = () => {
|
||||
const mainImageRef = useRef<HTMLImageElement | null>(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 (
|
||||
<Flex
|
||||
@@ -138,7 +158,7 @@ export const FullScreenPlayerImage = () => {
|
||||
>
|
||||
<div className={styles.imageContainer} ref={mainImageRef}>
|
||||
<AnimatePresence initial={false} mode="sync">
|
||||
{imageState.current === 0 && (
|
||||
{!isPlayingRadio && imageState.current === 0 && (
|
||||
<ImageWithPlaceholder
|
||||
animate="open"
|
||||
className="full-screen-player-image"
|
||||
@@ -153,7 +173,7 @@ export const FullScreenPlayerImage = () => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{imageState.current === 1 && (
|
||||
{!isPlayingRadio && imageState.current === 1 && (
|
||||
<ImageWithPlaceholder
|
||||
animate="open"
|
||||
className="full-screen-player-image"
|
||||
@@ -167,57 +187,85 @@ export const FullScreenPlayerImage = () => {
|
||||
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>
|
||||
</div>
|
||||
<Stack className={styles.metadataContainer} gap="md" maw="100%">
|
||||
<Text fw={900} lh="1.2" overflow="hidden" size="4xl" w="100%">
|
||||
{currentSong?.name}
|
||||
</Text>
|
||||
<Text
|
||||
component={Link}
|
||||
isLink
|
||||
overflow="hidden"
|
||||
size="xl"
|
||||
to={generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, {
|
||||
albumId: currentSong?.albumId || '',
|
||||
})}
|
||||
w="100%"
|
||||
>
|
||||
{currentSong?.album}
|
||||
{isPlayingRadio
|
||||
? radioMetadata?.title || stationName || 'Radio'
|
||||
: currentSong?.name}
|
||||
</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">
|
||||
{currentSong?.artists?.map((artist, index) => (
|
||||
<Fragment key={`fs-artist-${artist.id}`}>
|
||||
{index > 0 && (
|
||||
<Text
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
padding: '0 0.5rem',
|
||||
}}
|
||||
>
|
||||
•
|
||||
</Text>
|
||||
)}
|
||||
<Text
|
||||
component={Link}
|
||||
isLink
|
||||
to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
|
||||
albumArtistId: artist.id,
|
||||
})}
|
||||
>
|
||||
{artist.name}
|
||||
</Text>
|
||||
</Fragment>
|
||||
))}
|
||||
{isPlayingRadio
|
||||
? radioMetadata?.artist || stationName || 'Radio'
|
||||
: currentSong?.artists?.map((artist, index) => (
|
||||
<Fragment key={`fs-artist-${artist.id}`}>
|
||||
{index > 0 && (
|
||||
<Text
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
padding: '0 0.5rem',
|
||||
}}
|
||||
>
|
||||
•
|
||||
</Text>
|
||||
)}
|
||||
<Text
|
||||
component={Link}
|
||||
isLink
|
||||
to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
|
||||
albumArtistId: artist.id,
|
||||
})}
|
||||
>
|
||||
{artist.name}
|
||||
</Text>
|
||||
</Fragment>
|
||||
))}
|
||||
</Text>
|
||||
<Group justify="center" mt="sm">
|
||||
{currentSong?.container && (
|
||||
<Badge variant="transparent">{currentSong?.container}</Badge>
|
||||
)}
|
||||
{currentSong?.releaseYear && (
|
||||
<Badge variant="transparent">{currentSong?.releaseYear}</Badge>
|
||||
)}
|
||||
</Group>
|
||||
{!isPlayingRadio && (
|
||||
<Group justify="center" mt="sm">
|
||||
{currentSong?.container && (
|
||||
<Badge variant="transparent">{currentSong?.container}</Badge>
|
||||
)}
|
||||
{currentSong?.releaseYear && (
|
||||
<Badge variant="transparent">{currentSong?.releaseYear}</Badge>
|
||||
)}
|
||||
</Group>
|
||||
)}
|
||||
</Stack>
|
||||
</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 { 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<boolean | null>(null);
|
||||
@@ -671,13 +680,13 @@ export const FullScreenPlayer = () => {
|
||||
|
||||
return (
|
||||
<PlayerContainer
|
||||
dynamicBackground={dynamicBackground}
|
||||
dynamicBackground={effectiveDynamicBackground}
|
||||
dynamicIsImage={dynamicIsImage}
|
||||
windowBarStyle={windowBarStyle}
|
||||
>
|
||||
<Controls />
|
||||
<BackgroundImageOverlay
|
||||
dynamicBackground={dynamicBackground}
|
||||
dynamicBackground={effectiveDynamicBackground}
|
||||
dynamicImageBlur={dynamicImageBlur}
|
||||
/>
|
||||
<div className={styles.responsiveContainer}>
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
<ItemImage
|
||||
className={clsx(
|
||||
styles.playerbarImage,
|
||||
PlaybackSelectors.playerCoverArt,
|
||||
)}
|
||||
enableDebounce={false}
|
||||
enableViewport={false}
|
||||
explicitStatus={currentSong?.explicitStatus}
|
||||
fetchPriority="high"
|
||||
id={currentSong?.imageId}
|
||||
itemType={LibraryItem.SONG}
|
||||
serverId={currentSong?._serverId}
|
||||
type="table"
|
||||
/>
|
||||
{isRadioMode ? (
|
||||
<Center
|
||||
className={clsx(
|
||||
styles.playerbarImage,
|
||||
styles.radioImage,
|
||||
)}
|
||||
>
|
||||
<Icon color="muted" icon="radio" size="40%" />
|
||||
</Center>
|
||||
) : (
|
||||
<ItemImage
|
||||
className={clsx(
|
||||
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>
|
||||
{!collapsed && (
|
||||
<ActionIcon
|
||||
|
||||
@@ -5,6 +5,10 @@ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react
|
||||
import styles from './mobile-fullscreen-player.module.css';
|
||||
|
||||
import { useItemImageUrl } from '/@/renderer/components/item-image/item-image';
|
||||
import {
|
||||
useIsRadioActive,
|
||||
useRadioPlayer,
|
||||
} from '/@/renderer/features/radio/hooks/use-radio-player';
|
||||
import {
|
||||
useFullScreenPlayerStore,
|
||||
useImageRes,
|
||||
@@ -44,9 +48,14 @@ const MotionImage = motion.img;
|
||||
|
||||
const ImageWithPlaceholder = ({
|
||||
className,
|
||||
placeholderIcon,
|
||||
useImageAspectRatio,
|
||||
...props
|
||||
}: HTMLMotionProps<'img'> & { placeholder?: string; useImageAspectRatio?: boolean }) => {
|
||||
}: HTMLMotionProps<'img'> & {
|
||||
placeholder?: string;
|
||||
placeholderIcon?: 'itemAlbum' | 'radio';
|
||||
useImageAspectRatio?: boolean;
|
||||
}) => {
|
||||
if (!props.src) {
|
||||
return (
|
||||
<Center
|
||||
@@ -57,7 +66,11 @@ const ImageWithPlaceholder = ({
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Icon color="muted" icon="itemAlbum" size="25%" />
|
||||
<Icon
|
||||
color="muted"
|
||||
icon={placeholderIcon === 'radio' ? 'radio' : 'itemAlbum'}
|
||||
size="25%"
|
||||
/>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
@@ -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 = () => {
|
||||
})}
|
||||
>
|
||||
<AnimatePresence initial={false} mode="sync">
|
||||
{imageState.current === 0 && (
|
||||
{isPlayingRadio ? (
|
||||
<ImageWithPlaceholder
|
||||
animate="open"
|
||||
className={PlaybackSelectors.playerCoverArt}
|
||||
custom={{ isOpen: imageState.current === 0 }}
|
||||
custom={{ isOpen: true }}
|
||||
draggable={false}
|
||||
exit="closed"
|
||||
initial="closed"
|
||||
key={`top-${currentSong?._uniqueId || 'none'}`}
|
||||
key="radio"
|
||||
loading="eager"
|
||||
placeholder="var(--theme-colors-foreground-muted)"
|
||||
src={imageState.topImage || ''}
|
||||
placeholderIcon="radio"
|
||||
src=""
|
||||
useImageAspectRatio={useImageAspectRatio}
|
||||
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 && (
|
||||
<ImageWithPlaceholder
|
||||
animate="open"
|
||||
className={PlaybackSelectors.playerCoverArt}
|
||||
custom={{ isOpen: imageState.current === 1 }}
|
||||
draggable={false}
|
||||
exit="closed"
|
||||
initial="closed"
|
||||
key={`bottom-${currentSong?._uniqueId || 'none'}`}
|
||||
loading="eager"
|
||||
placeholder="var(--theme-colors-foreground-muted)"
|
||||
src={imageState.bottomImage || ''}
|
||||
useImageAspectRatio={useImageAspectRatio}
|
||||
variants={imageVariants}
|
||||
/>
|
||||
{imageState.current === 1 && (
|
||||
<ImageWithPlaceholder
|
||||
animate="open"
|
||||
className={PlaybackSelectors.playerCoverArt}
|
||||
custom={{ isOpen: imageState.current === 1 }}
|
||||
draggable={false}
|
||||
exit="closed"
|
||||
initial="closed"
|
||||
key={`bottom-${currentSong?._uniqueId || 'none'}`}
|
||||
loading="eager"
|
||||
placeholder="var(--theme-colors-foreground-muted)"
|
||||
src={imageState.bottomImage || ''}
|
||||
useImageAspectRatio={useImageAspectRatio}
|
||||
variants={imageVariants}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
@@ -16,6 +16,9 @@ interface MobileFullscreenPlayerMetadataProps {
|
||||
currentSong?: QueueSong;
|
||||
onToggleFavorite: (e: MouseEvent<HTMLButtonElement>) => 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 (
|
||||
<div className={styles.metadataContainer}>
|
||||
@@ -49,7 +59,7 @@ export const MobileFullscreenPlayerMetadata = memo(
|
||||
</TextTitle>
|
||||
</div>
|
||||
<Text className={clsx(PlaybackSelectors.songArtist)} size="md" truncate>
|
||||
{artists?.map((a) => a.name).join(', ') || '—'}
|
||||
{artistsDisplay || '—'}
|
||||
</Text>
|
||||
<Text className={clsx(PlaybackSelectors.songAlbum)} size="md" truncate>
|
||||
{album || '—'}
|
||||
@@ -65,21 +75,23 @@ export const MobileFullscreenPlayerMetadata = memo(
|
||||
)}
|
||||
</Group>
|
||||
)}
|
||||
<Group align="center" className={styles.actionsRow} gap="xs">
|
||||
<ActionIcon
|
||||
icon="favorite"
|
||||
iconProps={{
|
||||
fill: isFavorite ? 'primary' : undefined,
|
||||
size: 'md',
|
||||
}}
|
||||
onClick={onToggleFavorite}
|
||||
size="sm"
|
||||
variant="subtle"
|
||||
/>
|
||||
{showRating && (
|
||||
<Rating onChange={onUpdateRating} size="sm" value={rating || 0} />
|
||||
)}
|
||||
</Group>
|
||||
{!isRadio && (
|
||||
<Group align="center" className={styles.actionsRow} gap="xs">
|
||||
<ActionIcon
|
||||
icon="favorite"
|
||||
iconProps={{
|
||||
fill: isFavorite ? 'primary' : undefined,
|
||||
size: 'md',
|
||||
}}
|
||||
onClick={onToggleFavorite}
|
||||
size="sm"
|
||||
variant="subtle"
|
||||
/>
|
||||
{showRating && (
|
||||
<Rating onChange={onUpdateRating} size="sm" value={rating || 0} />
|
||||
)}
|
||||
</Group>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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 (
|
||||
<MobilePlayerContainer
|
||||
dynamicBackground={dynamicBackground}
|
||||
dynamicBackground={effectiveDynamicBackground}
|
||||
dynamicIsImage={dynamicIsImage}
|
||||
>
|
||||
<BackgroundImageOverlay
|
||||
dynamicBackground={dynamicBackground}
|
||||
dynamicBackground={effectiveDynamicBackground}
|
||||
dynamicImageBlur={dynamicImageBlur}
|
||||
/>
|
||||
<motion.div
|
||||
@@ -470,6 +479,9 @@ export const MobileFullscreenPlayer = () => {
|
||||
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}
|
||||
/>
|
||||
<MobileFullscreenPlayerProgress currentSong={currentSong} />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ? (
|
||||
<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} />
|
||||
) : (
|
||||
<ImageUnloader icon="emptySongImage" />
|
||||
|
||||
Reference in New Issue
Block a user