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",
"doNotShowAgain": "do not show this again",
"duration": "duration",
"external": "external",
"view": "view",
"edit": "edit",
"enable": "enable",
@@ -901,7 +902,7 @@
"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)",
"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": "Enable NetEase translations",
"notify": "enable song notifications",
@@ -61,7 +61,7 @@
.image-container.external {
img {
opacity: 0.3;
opacity: 0.5;
filter: grayscale(0.5) saturate(0.7);
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 { useDragDrop } from '/@/renderer/hooks/use-drag-drop';
import { AppRoute } from '/@/renderer/router/routes';
import { useShowRatings } from '/@/renderer/store';
import { useIntegrationsSettings, useShowRatings } from '/@/renderer/store';
import {
formatDateAbsolute,
formatDateAbsoluteUTC,
@@ -179,6 +179,7 @@ const CompactItemCard = ({
showRating,
withControls,
}: ItemCardDerivativeProps) => {
const { youtube: youtubeIntegrationEnabled } = useIntegrationsSettings();
const [showControls, setShowControls] = useState(false);
const itemRowId =
data && internalState && typeof data === 'object' && 'id' in data
@@ -342,9 +343,12 @@ const CompactItemCard = ({
const hasRating = showRating && userRating !== null && userRating > 0;
const isExternal = data._serverType === ServerType.EXTERNAL;
const showItemCardControls =
withControls && showControls && data && (!isExternal || youtubeIntegrationEnabled);
const imageContainerClassName = clsx(styles.imageContainer, {
[styles.isRound]: isRound,
[styles.noHoverOverlay]: isExternal,
[styles.noHoverOverlay]: isExternal && !showItemCardControls,
});
const imageContainerContent = (
@@ -377,7 +381,7 @@ const CompactItemCard = ({
{isFavorite && <div className={styles.favoriteBadge} />}
{hasRating && <div className={styles.ratingBadge}>{userRating}</div>}
<AnimatePresence>
{withControls && showControls && data && !isExternal && (
{showItemCardControls && (
<ItemCardControls
controls={controls}
enableExpansion={enableExpansion}
@@ -486,6 +490,7 @@ const DefaultItemCard = ({
showRating,
withControls,
}: ItemCardDerivativeProps) => {
const { youtube: youtubeIntegrationEnabled } = useIntegrationsSettings();
const [showControls, setShowControls] = useState(false);
const itemRowId =
data && internalState && typeof data === 'object' && 'id' in data
@@ -584,10 +589,13 @@ const DefaultItemCard = ({
const hasRating = showRating && userRating !== null && userRating > 0;
const isExternal = data._serverType === ServerType.EXTERNAL;
const showItemCardControls =
withControls && showControls && data && (!isExternal || youtubeIntegrationEnabled);
const imageContainerClassName = clsx(styles.imageContainer, {
[styles.external]: isExternal,
[styles.isRound]: isRound,
[styles.noHoverOverlay]: isExternal,
[styles.noHoverOverlay]: isExternal && !showItemCardControls,
});
const imageContainerContent = (
@@ -618,7 +626,7 @@ const DefaultItemCard = ({
{isFavorite && <div className={styles.favoriteBadge} />}
{hasRating && <div className={styles.ratingBadge}>{userRating}</div>}
<AnimatePresence>
{withControls && showControls && !isExternal && (
{showItemCardControls && (
<ItemCardControls
controls={controls}
enableExpansion={enableExpansion}
@@ -725,6 +733,7 @@ const PosterItemCard = ({
showRating,
withControls,
}: ItemCardDerivativeProps) => {
const { youtube: youtubeIntegrationEnabled } = useIntegrationsSettings();
const [showControls, setShowControls] = useState(false);
const itemRowId =
data && internalState && typeof data === 'object' && 'id' in data
@@ -888,10 +897,13 @@ const PosterItemCard = ({
const hasRating = showRating && userRating !== null && userRating > 0;
const isExternal = data._serverType === ServerType.EXTERNAL;
const showItemCardControls =
withControls && showControls && data && (!isExternal || youtubeIntegrationEnabled);
const imageContainerClassName = clsx(styles.imageContainer, {
[styles.external]: isExternal,
[styles.isRound]: isRound,
[styles.noHoverOverlay]: isExternal,
[styles.noHoverOverlay]: isExternal && !showItemCardControls,
});
const imageContainerContent = (
@@ -922,7 +934,7 @@ const PosterItemCard = ({
{isFavorite && <div className={styles.favoriteBadge} />}
{hasRating && <div className={styles.ratingBadge}>{userRating}</div>}
<AnimatePresence>
{withControls && showControls && data && !isExternal && (
{showItemCardControls && (
<ItemCardControls
controls={controls}
enableExpansion={enableExpansion}
@@ -949,7 +961,7 @@ const PosterItemCard = ({
{enableNavigation && navigationPath && (imageAsLink ?? !internalState) ? (
<Link
className={imageContainerClassName}
data-unavailable-text={i18n.t('common.unavailable', {
data-unavailable-text={i18n.t('common.external', {
postProcess: 'titleCase',
})}
draggable={false}
@@ -966,7 +978,7 @@ const PosterItemCard = ({
) : (
<div
className={imageContainerClassName}
data-unavailable-text={i18n.t('common.unavailable', {
data-unavailable-text={i18n.t('common.external', {
postProcess: 'titleCase',
})}
onClick={handleImageClick}
@@ -1,14 +1,16 @@
import { useQueryClient } from '@tanstack/react-query';
import { useEffect, useMemo, useRef } from 'react';
import { useNavigate } from 'react-router';
import { getTitlePath } from '/@/renderer/components/item-list/helpers/get-title-path';
import { ItemListStateItemWithRequiredProperties } from '/@/renderer/components/item-list/helpers/item-list-state';
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 { usePlayer } from '/@/renderer/features/player/context/player-context';
import { useSetFavorite } from '/@/renderer/features/shared/hooks/use-set-favorite';
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';
interface UseDefaultItemListControlsArgs {
@@ -34,6 +36,7 @@ const itemTypeMapping = {
export const useDefaultItemListControls = (args?: UseDefaultItemListControlsArgs) => {
const player = usePlayer();
const queryClient = useQueryClient();
const navigate = useNavigate();
const navigateRef = useRef(navigate);
const setFavorite = useSetFavorite();
@@ -384,6 +387,40 @@ export const useDefaultItemListControls = (args?: UseDefaultItemListControlsArgs
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);
},
@@ -417,10 +454,11 @@ export const useDefaultItemListControls = (args?: UseDefaultItemListControlsArgs
};
}, [
enableMultiSelect,
overrides,
onColumnReordered,
onColumnResized,
overrides,
player,
queryClient,
setFavorite,
setRating,
]);
@@ -1,3 +1,4 @@
import isElectron from 'is-electron';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -116,6 +117,7 @@ export const IntegrationsTab = memo(() => {
<Switch
aria-label={t('setting.youtube', { postProcess: 'sentenceCase' })}
defaultChecked={settings.youtube}
disabled={!isElectron()}
onChange={(e) => updateIntegrations({ youtube: e.currentTarget.checked })}
/>
),