mirror of
https://github.com/jeffvli/feishin.git
synced 2026-07-13 22:20:13 +02:00
add links, favorite, rating to album group column (#2139)
- additionally fix drop and drop interactions
This commit is contained in:
@@ -0,0 +1,10 @@
|
|||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--theme-spacing-xs);
|
||||||
|
align-items: center;
|
||||||
|
margin-top: var(--theme-spacing-xxs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import styles from './album-group-controls.module.css';
|
||||||
|
|
||||||
|
import { albumQueries } from '/@/renderer/features/albums/api/album-api';
|
||||||
|
import { useSetFavorite } from '/@/renderer/features/shared/hooks/use-set-favorite';
|
||||||
|
import { useSetRating } from '/@/renderer/features/shared/hooks/use-set-rating';
|
||||||
|
import { useIsMutatingCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation';
|
||||||
|
import { useIsMutatingDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation';
|
||||||
|
import { useIsMutatingRating } from '/@/renderer/features/shared/mutations/set-rating-mutation';
|
||||||
|
import { useShowRatings } from '/@/renderer/store';
|
||||||
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
|
import { Rating } from '/@/shared/components/rating/rating';
|
||||||
|
import { LibraryItem, ServerType } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
|
const isRatingSupported = (serverType: ServerType | undefined) =>
|
||||||
|
serverType === ServerType.NAVIDROME || serverType === ServerType.SUBSONIC;
|
||||||
|
|
||||||
|
const useAlbumGroupAlbum = (albumId: string | undefined, serverId: string | undefined) => {
|
||||||
|
const enabled = !!albumId && !!serverId && !albumId.startsWith('dummy/');
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
...albumQueries.detail({ query: { id: albumId! }, serverId: serverId! }),
|
||||||
|
enabled,
|
||||||
|
staleTime: 5 * 60 * 1000,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
interface AlbumGroupControlsProps {
|
||||||
|
albumId: string | undefined;
|
||||||
|
isGroupHovered: boolean;
|
||||||
|
serverId: string | undefined;
|
||||||
|
serverType: ServerType | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AlbumGroupControls = ({
|
||||||
|
albumId,
|
||||||
|
isGroupHovered,
|
||||||
|
serverId,
|
||||||
|
serverType,
|
||||||
|
}: AlbumGroupControlsProps) => {
|
||||||
|
const showRatingsSetting = useShowRatings();
|
||||||
|
const detailQuery = useAlbumGroupAlbum(albumId, serverId);
|
||||||
|
const setFavorite = useSetFavorite();
|
||||||
|
const setRating = useSetRating();
|
||||||
|
|
||||||
|
const isMutatingCreateFavorite = useIsMutatingCreateFavorite();
|
||||||
|
const isMutatingDeleteFavorite = useIsMutatingDeleteFavorite();
|
||||||
|
const isMutatingFavorite = isMutatingCreateFavorite || isMutatingDeleteFavorite;
|
||||||
|
const isMutatingRating = useIsMutatingRating();
|
||||||
|
|
||||||
|
const album = detailQuery.data;
|
||||||
|
const showRating = showRatingsSetting && isRatingSupported(serverType ?? album?._serverType);
|
||||||
|
|
||||||
|
const handleFavorite = useCallback(
|
||||||
|
(event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
if (!album) return;
|
||||||
|
|
||||||
|
setFavorite(album._serverId, [album.id], LibraryItem.ALBUM, !album.userFavorite);
|
||||||
|
},
|
||||||
|
[album, setFavorite],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRating = useCallback(
|
||||||
|
(rating: number) => {
|
||||||
|
if (!album) return;
|
||||||
|
|
||||||
|
if (album.userRating === rating) {
|
||||||
|
return setRating(album._serverId, [album.id], LibraryItem.ALBUM, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return setRating(album._serverId, [album.id], LibraryItem.ALBUM, rating);
|
||||||
|
},
|
||||||
|
[album, setRating],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!album) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.controls}>
|
||||||
|
<ActionIcon
|
||||||
|
className={album.userFavorite || isGroupHovered ? undefined : styles.hidden}
|
||||||
|
disabled={isMutatingFavorite}
|
||||||
|
icon="favorite"
|
||||||
|
iconProps={{
|
||||||
|
color: album.userFavorite ? 'primary' : 'muted',
|
||||||
|
fill: album.userFavorite ? 'primary' : undefined,
|
||||||
|
size: 'md',
|
||||||
|
}}
|
||||||
|
onClick={handleFavorite}
|
||||||
|
onDoubleClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
}}
|
||||||
|
size="xs"
|
||||||
|
variant="subtle"
|
||||||
|
/>
|
||||||
|
{showRating && (
|
||||||
|
<Rating
|
||||||
|
className={album.userRating || isGroupHovered ? undefined : styles.hidden}
|
||||||
|
onChange={handleRating}
|
||||||
|
readOnly={isMutatingRating}
|
||||||
|
size="xs"
|
||||||
|
value={album.userRating || 0}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -13,6 +13,32 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
padding-top: calc(var(--theme-spacing-xs) * 0.5);
|
padding-top: calc(var(--theme-spacing-xs) * 0.5);
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: var(--theme-radius-md);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 5;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
content: '';
|
||||||
|
background-color: rgb(0 0 0);
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover::before {
|
||||||
|
@mixin dark {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin light {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.info {
|
.info {
|
||||||
@@ -27,6 +53,15 @@
|
|||||||
font-size: var(--theme-font-size-sm);
|
font-size: var(--theme-font-size-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.album-name a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.artist-name {
|
.artist-name {
|
||||||
font-size: var(--theme-font-size-xs);
|
font-size: var(--theme-font-size-xs);
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
import { ReactElement, useState } from 'react';
|
import { ReactElement, useState } from 'react';
|
||||||
|
import { generatePath, Link } from 'react-router';
|
||||||
|
|
||||||
import imageColumnStyles from '../item-detail-list/columns/image-column.module.css';
|
import imageColumnStyles from '../item-detail-list/columns/image-column.module.css';
|
||||||
|
import { AlbumGroupControls } from './album-group-controls';
|
||||||
import styles from './album-group-header.module.css';
|
import styles from './album-group-header.module.css';
|
||||||
import { TableItemSize } from './item-table-list';
|
import { TableItemSize } from './item-table-list';
|
||||||
|
|
||||||
import { ItemImage } from '/@/renderer/components/item-image/item-image';
|
import { ItemImage } from '/@/renderer/components/item-image/item-image';
|
||||||
|
import { JoinedArtists } from '/@/renderer/features/albums/components/joined-artists';
|
||||||
import { PlayButton } from '/@/renderer/features/shared/components/play-button';
|
import { PlayButton } from '/@/renderer/features/shared/components/play-button';
|
||||||
import {
|
import {
|
||||||
LONG_PRESS_PLAY_BEHAVIOR,
|
LONG_PRESS_PLAY_BEHAVIOR,
|
||||||
PlayTooltip,
|
PlayTooltip,
|
||||||
} from '/@/renderer/features/shared/components/play-button-group';
|
} from '/@/renderer/features/shared/components/play-button-group';
|
||||||
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
import { useAlbumGroupImageSize, usePlayButtonBehavior } from '/@/renderer/store';
|
import { useAlbumGroupImageSize, usePlayButtonBehavior } from '/@/renderer/store';
|
||||||
import { LibraryItem, Song } from '/@/shared/types/domain-types';
|
import { LibraryItem, Song } from '/@/shared/types/domain-types';
|
||||||
import { Play } from '/@/shared/types/types';
|
import { Play } from '/@/shared/types/types';
|
||||||
@@ -35,6 +39,11 @@ export const AlbumGroupHeader = ({
|
|||||||
large: TableItemSize.LARGE,
|
large: TableItemSize.LARGE,
|
||||||
normal: TableItemSize.DEFAULT,
|
normal: TableItemSize.DEFAULT,
|
||||||
}[size];
|
}[size];
|
||||||
|
|
||||||
|
const albumPath = song?.albumId
|
||||||
|
? generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { albumId: song.albumId })
|
||||||
|
: null;
|
||||||
|
|
||||||
// The album group spans the combined row height, but when the image is
|
// The album group spans the combined row height, but when the image is
|
||||||
// enlarged the group's last row is grown so the total reaches the img size.
|
// enlarged the group's last row is grown so the total reaches the img size.
|
||||||
const infoHeight =
|
const infoHeight =
|
||||||
@@ -58,13 +67,12 @@ export const AlbumGroupHeader = ({
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div
|
||||||
<div
|
className={styles.container}
|
||||||
className={styles.imageContainer}
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
onMouseEnter={() => setIsHovered(true)}
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
>
|
||||||
style={imageContainerStyle}
|
<div className={styles.imageContainer} style={imageContainerStyle}>
|
||||||
>
|
|
||||||
<ItemImage
|
<ItemImage
|
||||||
className={imageColumnStyles.compactImage}
|
className={imageColumnStyles.compactImage}
|
||||||
enableDebounce
|
enableDebounce
|
||||||
@@ -93,8 +101,29 @@ export const AlbumGroupHeader = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.info} style={{ height: infoHeight }}>
|
<div className={styles.info} style={{ height: infoHeight }}>
|
||||||
<div className={styles.albumName}>{song?.album ?? ''}</div>
|
<div className={styles.albumName}>
|
||||||
<div className={styles.artistName}>{song?.albumArtistName ?? ''}</div>
|
{song?.albumId && albumPath ? (
|
||||||
|
<Link state={{ item: song }} to={albumPath}>
|
||||||
|
{song.album ?? ''}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
(song?.album ?? '')
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.artistName}>
|
||||||
|
<JoinedArtists
|
||||||
|
artistName={song?.albumArtistName ?? ''}
|
||||||
|
artists={song?.albumArtists ?? []}
|
||||||
|
linkProps={{ fw: 400 }}
|
||||||
|
rootTextProps={{ fw: 400, size: 'xs' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<AlbumGroupControls
|
||||||
|
albumId={song?.albumId}
|
||||||
|
isGroupHovered={isHovered}
|
||||||
|
serverId={song?._serverId}
|
||||||
|
serverType={song?._serverType}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -84,7 +84,12 @@ export const AlbumGroupColumn = (props: ItemTableListInnerColumn) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableColumnContainer {...props} enableAlternateRowColors={false}>
|
<TableColumnContainer
|
||||||
|
{...props}
|
||||||
|
dragRef={null}
|
||||||
|
enableAlternateRowColors={false}
|
||||||
|
isDraggedOver={null}
|
||||||
|
>
|
||||||
<AlbumGroupHeader
|
<AlbumGroupHeader
|
||||||
groupRowCount={groupRowCount}
|
groupRowCount={groupRowCount}
|
||||||
onPlay={handlePlay}
|
onPlay={handlePlay}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
|||||||
label: i18n.t('table.config.label.albumGroup'),
|
label: i18n.t('table.config.label.albumGroup'),
|
||||||
pinned: 'left',
|
pinned: 'left',
|
||||||
value: TableColumn.ALBUM_GROUP,
|
value: TableColumn.ALBUM_GROUP,
|
||||||
width: 200,
|
width: 240,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
align: 'center',
|
align: 'center',
|
||||||
|
|||||||
+25
-2
@@ -83,23 +83,46 @@ export const useRowInteractionDelegate = ({
|
|||||||
let pending: null | { edge: 'bottom' | 'top' | null; rowKey: string } = null;
|
let pending: null | { edge: 'bottom' | 'top' | null; rowKey: string } = null;
|
||||||
let rafId: null | number = null;
|
let rafId: null | number = null;
|
||||||
|
|
||||||
|
const isExcludedFromDragBorder = (el: HTMLElement) =>
|
||||||
|
el.hasAttribute('data-exclude-row-drag-border');
|
||||||
|
|
||||||
const clearRow = (rowKey: string) => {
|
const clearRow = (rowKey: string) => {
|
||||||
root.querySelectorAll(`[data-row-index="${rowKey}"]`).forEach((node) => {
|
root.querySelectorAll(`[data-row-index="${rowKey}"]`).forEach((node) => {
|
||||||
const el = node as HTMLElement;
|
const el = node as HTMLElement;
|
||||||
el.removeAttribute('data-row-dragged-over');
|
el.removeAttribute('data-row-dragged-over');
|
||||||
el.removeAttribute('data-row-dragged-over-first');
|
el.removeAttribute('data-row-dragged-over-first');
|
||||||
|
el.removeAttribute('data-exclude-row-drag-left');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const applyRow = (rowKey: string, edge: 'bottom' | 'top') => {
|
const applyRow = (rowKey: string, edge: 'bottom' | 'top') => {
|
||||||
const nodes = root.querySelectorAll(`[data-row-index="${rowKey}"]`);
|
const nodes = Array.from(root.querySelectorAll(`[data-row-index="${rowKey}"]`));
|
||||||
nodes.forEach((node, idx) => {
|
const eligibleNodes = nodes.filter(
|
||||||
|
(node) => !isExcludedFromDragBorder(node as HTMLElement),
|
||||||
|
);
|
||||||
|
const hasExcludedBeforeFirst = eligibleNodes.length < nodes.length;
|
||||||
|
|
||||||
|
nodes.forEach((node) => {
|
||||||
|
if (!isExcludedFromDragBorder(node as HTMLElement)) return;
|
||||||
|
const el = node as HTMLElement;
|
||||||
|
el.removeAttribute('data-row-dragged-over');
|
||||||
|
el.removeAttribute('data-row-dragged-over-first');
|
||||||
|
el.removeAttribute('data-exclude-row-drag-left');
|
||||||
|
});
|
||||||
|
|
||||||
|
eligibleNodes.forEach((node, idx) => {
|
||||||
const el = node as HTMLElement;
|
const el = node as HTMLElement;
|
||||||
el.setAttribute('data-row-dragged-over', edge);
|
el.setAttribute('data-row-dragged-over', edge);
|
||||||
if (idx === 0) {
|
if (idx === 0) {
|
||||||
el.setAttribute('data-row-dragged-over-first', 'true');
|
el.setAttribute('data-row-dragged-over-first', 'true');
|
||||||
|
if (hasExcludedBeforeFirst) {
|
||||||
|
el.setAttribute('data-exclude-row-drag-left', 'true');
|
||||||
|
} else {
|
||||||
|
el.removeAttribute('data-exclude-row-drag-left');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
el.removeAttribute('data-row-dragged-over-first');
|
el.removeAttribute('data-row-dragged-over-first');
|
||||||
|
el.removeAttribute('data-exclude-row-drag-left');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -166,6 +166,11 @@
|
|||||||
margin-left: 9999px;
|
margin-left: 9999px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.container.data-row[data-row-dragged-over='top'][data-row-dragged-over-first='true'][data-exclude-row-drag-left='true']::after {
|
||||||
|
left: 0;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.container.data-row[data-row-dragged-over='bottom']::after {
|
.container.data-row[data-row-dragged-over='bottom']::after {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
@@ -185,6 +190,11 @@
|
|||||||
margin-left: 9999px;
|
margin-left: 9999px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.container.data-row[data-row-dragged-over='bottom'][data-row-dragged-over-first='true'][data-exclude-row-drag-left='true']::after {
|
||||||
|
left: 0;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.container.data-row > * {
|
.container.data-row > * {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
|||||||
@@ -842,6 +842,7 @@ export const TableColumnContainer = (
|
|||||||
[styles.withHorizontalBorder]: showHorizontalBorder && clampHeight === null,
|
[styles.withHorizontalBorder]: showHorizontalBorder && clampHeight === null,
|
||||||
[styles.withVerticalBorder]: showVerticalBorder,
|
[styles.withVerticalBorder]: showVerticalBorder,
|
||||||
})}
|
})}
|
||||||
|
data-exclude-row-drag-border={props.type === TableColumn.ALBUM_GROUP ? true : undefined}
|
||||||
data-row-index={isDataRow ? `${props.tableId}-${props.rowIndex}` : undefined}
|
data-row-index={isDataRow ? `${props.tableId}-${props.rowIndex}` : undefined}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
|
|||||||
@@ -2503,7 +2503,7 @@ export const useSettingsStore = createWithEqualityFn<SettingsSlice>()(
|
|||||||
id: TableColumn.ALBUM_GROUP,
|
id: TableColumn.ALBUM_GROUP,
|
||||||
isEnabled: false,
|
isEnabled: false,
|
||||||
pinned: 'left',
|
pinned: 'left',
|
||||||
width: 200,
|
width: 240,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user