mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-16 21:50:35 +02:00
handle playback from ItemCard
This commit is contained in:
@@ -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 })}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user