handle playback from ItemCard

This commit is contained in:
jeffvli
2026-02-07 00:41:36 -08:00
parent f04ea3bca0
commit f46ca8cd35
5 changed files with 66 additions and 13 deletions
+2 -1
View File
@@ -79,6 +79,7 @@
"dismiss": "dismiss", "dismiss": "dismiss",
"doNotShowAgain": "do not show this again", "doNotShowAgain": "do not show this again",
"duration": "duration", "duration": "duration",
"external": "external",
"view": "view", "view": "view",
"edit": "edit", "edit": "edit",
"enable": "enable", "enable": "enable",
@@ -901,7 +902,7 @@
"musicbrainzPrioritizeCountries": "prioritize MusicBrainz countries", "musicbrainzPrioritizeCountries": "prioritize MusicBrainz countries",
"musicbrainzPrioritizeCountries_description": "country codes to prioritize when ordering MusicBrainz releases, comma separated and non case-sensitive (e.g. us, gb, de)", "musicbrainzPrioritizeCountries_description": "country codes to prioritize when ordering MusicBrainz releases, comma separated and non case-sensitive (e.g. us, gb, de)",
"youtube": "enable youtube integration", "youtube": "enable youtube integration",
"youtube_description": "external songs will attempt to use YouTube to resolve stream URLs for playback", "youtube_description": "external songs will attempt to use YouTube to resolve stream URLs for playback (desktop only)",
"neteaseTranslation_description": "When enabled, fetches and displays translated lyrics from NetEase if available", "neteaseTranslation_description": "When enabled, fetches and displays translated lyrics from NetEase if available",
"neteaseTranslation": "Enable NetEase translations", "neteaseTranslation": "Enable NetEase translations",
"notify": "enable song notifications", "notify": "enable song notifications",
@@ -61,7 +61,7 @@
.image-container.external { .image-container.external {
img { img {
opacity: 0.3; opacity: 0.5;
filter: grayscale(0.5) saturate(0.7); filter: grayscale(0.5) saturate(0.7);
transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out;
} }
@@ -19,7 +19,7 @@ import { ItemControls } from '/@/renderer/components/item-list/types';
import { JoinedArtists } from '/@/renderer/features/albums/components/joined-artists'; import { JoinedArtists } from '/@/renderer/features/albums/components/joined-artists';
import { useDragDrop } from '/@/renderer/hooks/use-drag-drop'; import { useDragDrop } from '/@/renderer/hooks/use-drag-drop';
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { useShowRatings } from '/@/renderer/store'; import { useIntegrationsSettings, useShowRatings } from '/@/renderer/store';
import { import {
formatDateAbsolute, formatDateAbsolute,
formatDateAbsoluteUTC, formatDateAbsoluteUTC,
@@ -179,6 +179,7 @@ const CompactItemCard = ({
showRating, showRating,
withControls, withControls,
}: ItemCardDerivativeProps) => { }: ItemCardDerivativeProps) => {
const { youtube: youtubeIntegrationEnabled } = useIntegrationsSettings();
const [showControls, setShowControls] = useState(false); const [showControls, setShowControls] = useState(false);
const itemRowId = const itemRowId =
data && internalState && typeof data === 'object' && 'id' in data data && internalState && typeof data === 'object' && 'id' in data
@@ -342,9 +343,12 @@ const CompactItemCard = ({
const hasRating = showRating && userRating !== null && userRating > 0; const hasRating = showRating && userRating !== null && userRating > 0;
const isExternal = data._serverType === ServerType.EXTERNAL; const isExternal = data._serverType === ServerType.EXTERNAL;
const showItemCardControls =
withControls && showControls && data && (!isExternal || youtubeIntegrationEnabled);
const imageContainerClassName = clsx(styles.imageContainer, { const imageContainerClassName = clsx(styles.imageContainer, {
[styles.isRound]: isRound, [styles.isRound]: isRound,
[styles.noHoverOverlay]: isExternal, [styles.noHoverOverlay]: isExternal && !showItemCardControls,
}); });
const imageContainerContent = ( const imageContainerContent = (
@@ -377,7 +381,7 @@ const CompactItemCard = ({
{isFavorite && <div className={styles.favoriteBadge} />} {isFavorite && <div className={styles.favoriteBadge} />}
{hasRating && <div className={styles.ratingBadge}>{userRating}</div>} {hasRating && <div className={styles.ratingBadge}>{userRating}</div>}
<AnimatePresence> <AnimatePresence>
{withControls && showControls && data && !isExternal && ( {showItemCardControls && (
<ItemCardControls <ItemCardControls
controls={controls} controls={controls}
enableExpansion={enableExpansion} enableExpansion={enableExpansion}
@@ -486,6 +490,7 @@ const DefaultItemCard = ({
showRating, showRating,
withControls, withControls,
}: ItemCardDerivativeProps) => { }: ItemCardDerivativeProps) => {
const { youtube: youtubeIntegrationEnabled } = useIntegrationsSettings();
const [showControls, setShowControls] = useState(false); const [showControls, setShowControls] = useState(false);
const itemRowId = const itemRowId =
data && internalState && typeof data === 'object' && 'id' in data data && internalState && typeof data === 'object' && 'id' in data
@@ -584,10 +589,13 @@ const DefaultItemCard = ({
const hasRating = showRating && userRating !== null && userRating > 0; const hasRating = showRating && userRating !== null && userRating > 0;
const isExternal = data._serverType === ServerType.EXTERNAL; const isExternal = data._serverType === ServerType.EXTERNAL;
const showItemCardControls =
withControls && showControls && data && (!isExternal || youtubeIntegrationEnabled);
const imageContainerClassName = clsx(styles.imageContainer, { const imageContainerClassName = clsx(styles.imageContainer, {
[styles.external]: isExternal, [styles.external]: isExternal,
[styles.isRound]: isRound, [styles.isRound]: isRound,
[styles.noHoverOverlay]: isExternal, [styles.noHoverOverlay]: isExternal && !showItemCardControls,
}); });
const imageContainerContent = ( const imageContainerContent = (
@@ -618,7 +626,7 @@ const DefaultItemCard = ({
{isFavorite && <div className={styles.favoriteBadge} />} {isFavorite && <div className={styles.favoriteBadge} />}
{hasRating && <div className={styles.ratingBadge}>{userRating}</div>} {hasRating && <div className={styles.ratingBadge}>{userRating}</div>}
<AnimatePresence> <AnimatePresence>
{withControls && showControls && !isExternal && ( {showItemCardControls && (
<ItemCardControls <ItemCardControls
controls={controls} controls={controls}
enableExpansion={enableExpansion} enableExpansion={enableExpansion}
@@ -725,6 +733,7 @@ const PosterItemCard = ({
showRating, showRating,
withControls, withControls,
}: ItemCardDerivativeProps) => { }: ItemCardDerivativeProps) => {
const { youtube: youtubeIntegrationEnabled } = useIntegrationsSettings();
const [showControls, setShowControls] = useState(false); const [showControls, setShowControls] = useState(false);
const itemRowId = const itemRowId =
data && internalState && typeof data === 'object' && 'id' in data data && internalState && typeof data === 'object' && 'id' in data
@@ -888,10 +897,13 @@ const PosterItemCard = ({
const hasRating = showRating && userRating !== null && userRating > 0; const hasRating = showRating && userRating !== null && userRating > 0;
const isExternal = data._serverType === ServerType.EXTERNAL; const isExternal = data._serverType === ServerType.EXTERNAL;
const showItemCardControls =
withControls && showControls && data && (!isExternal || youtubeIntegrationEnabled);
const imageContainerClassName = clsx(styles.imageContainer, { const imageContainerClassName = clsx(styles.imageContainer, {
[styles.external]: isExternal, [styles.external]: isExternal,
[styles.isRound]: isRound, [styles.isRound]: isRound,
[styles.noHoverOverlay]: isExternal, [styles.noHoverOverlay]: isExternal && !showItemCardControls,
}); });
const imageContainerContent = ( const imageContainerContent = (
@@ -922,7 +934,7 @@ const PosterItemCard = ({
{isFavorite && <div className={styles.favoriteBadge} />} {isFavorite && <div className={styles.favoriteBadge} />}
{hasRating && <div className={styles.ratingBadge}>{userRating}</div>} {hasRating && <div className={styles.ratingBadge}>{userRating}</div>}
<AnimatePresence> <AnimatePresence>
{withControls && showControls && data && !isExternal && ( {showItemCardControls && (
<ItemCardControls <ItemCardControls
controls={controls} controls={controls}
enableExpansion={enableExpansion} enableExpansion={enableExpansion}
@@ -949,7 +961,7 @@ const PosterItemCard = ({
{enableNavigation && navigationPath && (imageAsLink ?? !internalState) ? ( {enableNavigation && navigationPath && (imageAsLink ?? !internalState) ? (
<Link <Link
className={imageContainerClassName} className={imageContainerClassName}
data-unavailable-text={i18n.t('common.unavailable', { data-unavailable-text={i18n.t('common.external', {
postProcess: 'titleCase', postProcess: 'titleCase',
})} })}
draggable={false} draggable={false}
@@ -966,7 +978,7 @@ const PosterItemCard = ({
) : ( ) : (
<div <div
className={imageContainerClassName} className={imageContainerClassName}
data-unavailable-text={i18n.t('common.unavailable', { data-unavailable-text={i18n.t('common.external', {
postProcess: 'titleCase', postProcess: 'titleCase',
})} })}
onClick={handleImageClick} onClick={handleImageClick}
@@ -1,14 +1,16 @@
import { useQueryClient } from '@tanstack/react-query';
import { useEffect, useMemo, useRef } from 'react'; import { useEffect, useMemo, useRef } from 'react';
import { useNavigate } from 'react-router'; import { useNavigate } from 'react-router';
import { getTitlePath } from '/@/renderer/components/item-list/helpers/get-title-path'; import { getTitlePath } from '/@/renderer/components/item-list/helpers/get-title-path';
import { ItemListStateItemWithRequiredProperties } from '/@/renderer/components/item-list/helpers/item-list-state'; import { ItemListStateItemWithRequiredProperties } from '/@/renderer/components/item-list/helpers/item-list-state';
import { DefaultItemControlProps, ItemControls } from '/@/renderer/components/item-list/types'; import { DefaultItemControlProps, ItemControls } from '/@/renderer/components/item-list/types';
import { albumQueries } from '/@/renderer/features/albums/api/album-api';
import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller'; import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller';
import { usePlayer } from '/@/renderer/features/player/context/player-context'; import { usePlayer } from '/@/renderer/features/player/context/player-context';
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 { LibraryItem, QueueSong, Song } from '/@/shared/types/domain-types'; import { LibraryItem, QueueSong, ServerType, Song } from '/@/shared/types/domain-types';
import { Play, TableColumn } from '/@/shared/types/types'; import { Play, TableColumn } from '/@/shared/types/types';
interface UseDefaultItemListControlsArgs { interface UseDefaultItemListControlsArgs {
@@ -34,6 +36,7 @@ const itemTypeMapping = {
export const useDefaultItemListControls = (args?: UseDefaultItemListControlsArgs) => { export const useDefaultItemListControls = (args?: UseDefaultItemListControlsArgs) => {
const player = usePlayer(); const player = usePlayer();
const queryClient = useQueryClient();
const navigate = useNavigate(); const navigate = useNavigate();
const navigateRef = useRef(navigate); const navigateRef = useRef(navigate);
const setFavorite = useSetFavorite(); const setFavorite = useSetFavorite();
@@ -384,6 +387,40 @@ export const useDefaultItemListControls = (args?: UseDefaultItemListControlsArgs
return; return;
} }
const isExternal =
(item as Song & { _serverType?: ServerType })._serverType ===
ServerType.EXTERNAL;
if (isExternal) {
if (
itemType === LibraryItem.SONG ||
itemType === LibraryItem.PLAYLIST_SONG ||
(item as { _itemType?: LibraryItem })._itemType === LibraryItem.SONG
) {
player.addToQueueByData([item as Song], playType, item.id);
return;
}
if (itemType === LibraryItem.ALBUM) {
(async () => {
try {
const album = await queryClient.fetchQuery(
albumQueries.detail({
query: { id: item.id },
serverId: 'musicbrainz',
}),
);
const songs = album?.songs ?? [];
if (songs.length > 0) {
player.addToQueueByData(songs, playType);
}
} catch {
console.error('Error fetching album songs for item', item);
}
})();
return;
}
}
player.addToQueueByFetch(item._serverId, [item.id], itemType, playType); player.addToQueueByFetch(item._serverId, [item.id], itemType, playType);
}, },
@@ -417,10 +454,11 @@ export const useDefaultItemListControls = (args?: UseDefaultItemListControlsArgs
}; };
}, [ }, [
enableMultiSelect, enableMultiSelect,
overrides,
onColumnReordered, onColumnReordered,
onColumnResized, onColumnResized,
overrides,
player, player,
queryClient,
setFavorite, setFavorite,
setRating, setRating,
]); ]);
@@ -1,3 +1,4 @@
import isElectron from 'is-electron';
import { memo } from 'react'; import { memo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -116,6 +117,7 @@ export const IntegrationsTab = memo(() => {
<Switch <Switch
aria-label={t('setting.youtube', { postProcess: 'sentenceCase' })} aria-label={t('setting.youtube', { postProcess: 'sentenceCase' })}
defaultChecked={settings.youtube} defaultChecked={settings.youtube}
disabled={!isElectron()}
onChange={(e) => updateIntegrations({ youtube: e.currentTarget.checked })} onChange={(e) => updateIntegrations({ youtube: e.currentTarget.checked })}
/> />
), ),