add item card navigation

This commit is contained in:
jeffvli
2025-11-19 22:28:03 -08:00
parent b4b31bc6b4
commit 96d15929ba
2 changed files with 202 additions and 85 deletions
@@ -21,9 +21,12 @@
.image-container { .image-container {
position: relative; position: relative;
display: block;
width: 100%; width: 100%;
aspect-ratio: 1; aspect-ratio: 1;
overflow: hidden; overflow: hidden;
color: inherit;
text-decoration: none;
&::before { &::before {
position: absolute; position: absolute;
+199 -85
View File
@@ -8,6 +8,7 @@ import styles from './item-card.module.css';
import { ItemCardControls } from '/@/renderer/components/item-card/item-card-controls'; import { ItemCardControls } from '/@/renderer/components/item-card/item-card-controls';
import { getDraggedItems } from '/@/renderer/components/item-list/helpers/get-dragged-items'; import { getDraggedItems } from '/@/renderer/components/item-list/helpers/get-dragged-items';
import { getTitlePath } from '/@/renderer/components/item-list/helpers/get-title-path';
import { ItemListStateActions } from '/@/renderer/components/item-list/helpers/item-list-state'; import { ItemListStateActions } from '/@/renderer/components/item-list/helpers/item-list-state';
import { ItemControls } from '/@/renderer/components/item-list/types'; import { ItemControls } from '/@/renderer/components/item-list/types';
import { useDragDrop } from '/@/renderer/hooks/use-drag-drop'; import { useDragDrop } from '/@/renderer/hooks/use-drag-drop';
@@ -170,6 +171,8 @@ const CompactItemCard = ({
}); });
if (data) { if (data) {
const navigationPath = getItemNavigationPath(data, itemType);
const handleMouseEnter = () => { const handleMouseEnter = () => {
if (withControls) { if (withControls) {
setShowControls(true); setShowControls(true);
@@ -182,7 +185,7 @@ const CompactItemCard = ({
} }
}; };
const handleContextMenu = (e: React.MouseEvent<HTMLDivElement>) => { const handleContextMenu = (e: React.MouseEvent<HTMLElement>) => {
if (!data || !controls) { if (!data || !controls) {
return; return;
} }
@@ -197,50 +200,81 @@ const CompactItemCard = ({
}); });
}; };
const handleImageClick = (e: React.MouseEvent<HTMLElement>) => {
// Prevent navigation on double-click, let the double-click handler work
if (e.detail === 2 && navigationPath) {
e.preventDefault();
}
handleClick(e as any);
};
const imageContainerClassName = clsx(styles.imageContainer, {
[styles.isRound]: isRound,
});
const imageContainerContent = (
<>
<Image
className={clsx(styles.image, { [styles.isRound]: isRound })}
src={imageUrl}
/>
<AnimatePresence>
{withControls && showControls && (
<ItemCardControls
controls={controls}
item={data}
itemType={itemType}
type="compact"
/>
)}
</AnimatePresence>
<div className={clsx(styles.detailContainer, styles.compact)}>
{rows
.filter(
(row): row is NonNullable<typeof row> =>
row !== null && row !== undefined,
)
.map((row, index) => (
<ItemCardRow
data={data!}
index={index}
key={row.id}
row={row}
type="compact"
/>
))}
</div>
</>
);
return ( return (
<div <div
className={clsx(styles.container, styles.compact, { className={clsx(styles.container, styles.compact, {
[styles.selected]: isSelected, [styles.selected]: isSelected,
})} })}
> >
<div {navigationPath ? (
className={clsx(styles.imageContainer, { [styles.isRound]: isRound })} <Link
onClick={handleClick} className={imageContainerClassName}
onContextMenu={handleContextMenu} onClick={handleImageClick}
onMouseEnter={handleMouseEnter} onContextMenu={handleContextMenu}
onMouseLeave={handleMouseLeave} onMouseEnter={handleMouseEnter}
> onMouseLeave={handleMouseLeave}
<Image to={navigationPath}
className={clsx(styles.image, { [styles.isRound]: isRound })} >
src={imageUrl} {imageContainerContent}
/> </Link>
<AnimatePresence> ) : (
{withControls && showControls && ( <div
<ItemCardControls className={imageContainerClassName}
controls={controls} onClick={handleImageClick}
item={data} onContextMenu={handleContextMenu}
itemType={itemType} onMouseEnter={handleMouseEnter}
type="compact" onMouseLeave={handleMouseLeave}
/> >
)} {imageContainerContent}
</AnimatePresence>
<div className={clsx(styles.detailContainer, styles.compact)}>
{rows
.filter(
(row): row is NonNullable<typeof row> =>
row !== null && row !== undefined,
)
.map((row, index) => (
<ItemCardRow
data={data!}
index={index}
key={row.id}
row={row}
type="compact"
/>
))}
</div> </div>
</div> )}
</div> </div>
); );
} }
@@ -325,6 +359,8 @@ const DefaultItemCard = ({
}); });
if (data) { if (data) {
const navigationPath = getItemNavigationPath(data, itemType);
const handleMouseEnter = () => { const handleMouseEnter = () => {
if (withControls) { if (withControls) {
setShowControls(true); setShowControls(true);
@@ -337,7 +373,7 @@ const DefaultItemCard = ({
} }
}; };
const handleContextMenu = (e: React.MouseEvent<HTMLDivElement>) => { const handleContextMenu = (e: React.MouseEvent<HTMLElement>) => {
if (!data || !controls) { if (!data || !controls) {
return; return;
} }
@@ -352,34 +388,65 @@ const DefaultItemCard = ({
}); });
}; };
const handleImageClick = (e: React.MouseEvent<HTMLElement>) => {
// Prevent navigation on double-click, let the double-click handler work
if (e.detail === 2 && navigationPath) {
e.preventDefault();
}
handleClick(e as any);
};
const imageContainerClassName = clsx(styles.imageContainer, {
[styles.isRound]: isRound,
});
const imageContainerContent = (
<>
<Image
className={clsx(styles.image, { [styles.isRound]: isRound })}
src={imageUrl}
/>
<AnimatePresence>
{withControls && showControls && (
<ItemCardControls
controls={controls}
item={data}
itemType={itemType}
type="default"
/>
)}
</AnimatePresence>
</>
);
return ( return (
<div <div
className={clsx(styles.container, { className={clsx(styles.container, {
[styles.selected]: isSelected, [styles.selected]: isSelected,
})} })}
> >
<div {navigationPath ? (
className={clsx(styles.imageContainer, { [styles.isRound]: isRound })} <Link
onClick={handleClick} className={imageContainerClassName}
onContextMenu={handleContextMenu} onClick={handleImageClick}
onMouseEnter={handleMouseEnter} onContextMenu={handleContextMenu}
onMouseLeave={handleMouseLeave} onMouseEnter={handleMouseEnter}
> onMouseLeave={handleMouseLeave}
<Image to={navigationPath}
className={clsx(styles.image, { [styles.isRound]: isRound })} >
src={imageUrl} {imageContainerContent}
/> </Link>
<AnimatePresence> ) : (
{withControls && showControls && ( <div
<ItemCardControls className={imageContainerClassName}
controls={controls} onClick={handleImageClick}
item={data} onContextMenu={handleContextMenu}
itemType={itemType} onMouseEnter={handleMouseEnter}
type="default" onMouseLeave={handleMouseLeave}
/> >
)} {imageContainerContent}
</AnimatePresence> </div>
</div> )}
<div className={styles.detailContainer}> <div className={styles.detailContainer}>
{rows {rows
.filter( .filter(
@@ -525,6 +592,8 @@ const PosterItemCard = ({
}); });
if (data) { if (data) {
const navigationPath = getItemNavigationPath(data, itemType);
const handleMouseEnter = () => { const handleMouseEnter = () => {
if (withControls) { if (withControls) {
setShowControls(true); setShowControls(true);
@@ -537,7 +606,7 @@ const PosterItemCard = ({
} }
}; };
const handleContextMenu = (e: React.MouseEvent<HTMLDivElement>) => { const handleContextMenu = (e: React.MouseEvent<HTMLElement>) => {
if (!data || !controls) { if (!data || !controls) {
return; return;
} }
@@ -552,6 +621,38 @@ const PosterItemCard = ({
}); });
}; };
const handleImageClick = (e: React.MouseEvent<HTMLElement>) => {
// Prevent navigation on double-click, let the double-click handler work
if (e.detail === 2 && navigationPath) {
e.preventDefault();
}
handleClick(e as any);
};
const imageContainerClassName = clsx(styles.imageContainer, {
[styles.isRound]: isRound,
});
const imageContainerContent = (
<>
<Image
className={clsx(styles.image, { [styles.isRound]: isRound })}
src={imageUrl}
/>
<AnimatePresence>
{withControls && showControls && data && (
<ItemCardControls
controls={controls}
internalState={internalState}
item={data}
itemType={itemType}
type="poster"
/>
)}
</AnimatePresence>
</>
);
return ( return (
<div <div
className={clsx(styles.container, styles.poster, { className={clsx(styles.container, styles.poster, {
@@ -560,29 +661,28 @@ const PosterItemCard = ({
})} })}
ref={ref} ref={ref}
> >
<div {navigationPath ? (
className={clsx(styles.imageContainer, { [styles.isRound]: isRound })} <Link
onClick={handleClick} className={imageContainerClassName}
onContextMenu={handleContextMenu} onClick={handleImageClick}
onMouseEnter={handleMouseEnter} onContextMenu={handleContextMenu}
onMouseLeave={handleMouseLeave} onMouseEnter={handleMouseEnter}
> onMouseLeave={handleMouseLeave}
<Image to={navigationPath}
className={clsx(styles.image, { [styles.isRound]: isRound })} >
src={imageUrl} {imageContainerContent}
/> </Link>
<AnimatePresence> ) : (
{withControls && showControls && data && ( <div
<ItemCardControls className={imageContainerClassName}
controls={controls} onClick={handleImageClick}
internalState={internalState} onContextMenu={handleContextMenu}
item={data} onMouseEnter={handleMouseEnter}
itemType={itemType} onMouseLeave={handleMouseLeave}
type="poster" >
/> {imageContainerContent}
)} </div>
</AnimatePresence> )}
</div>
{data && ( {data && (
<div className={styles.detailContainer}> <div className={styles.detailContainer}>
{rows {rows
@@ -868,6 +968,20 @@ const getImageUrl = (data: Album | AlbumArtist | Artist | Playlist | Song | unde
return undefined; return undefined;
}; };
const getItemNavigationPath = (
data: Album | AlbumArtist | Artist | Playlist | Song | undefined,
itemType: LibraryItem,
): null | string => {
if (!data || !('id' in data) || !data.id) {
return null;
}
// Check if data has _itemType (like in title row logic)
const effectiveItemType = '_itemType' in data && data._itemType ? data._itemType : itemType;
return getTitlePath(effectiveItemType, data.id);
};
const ItemCardRow = ({ const ItemCardRow = ({
data, data,
index, index,