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%;
|
||||
aspect-ratio: 1;
|
||||
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 {
|
||||
@@ -27,6 +53,15 @@
|
||||
font-size: var(--theme-font-size-sm);
|
||||
}
|
||||
|
||||
.album-name a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.artist-name {
|
||||
font-size: var(--theme-font-size-xs);
|
||||
opacity: 0.7;
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import { ReactElement, useState } from 'react';
|
||||
import { generatePath, Link } from 'react-router';
|
||||
|
||||
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 { TableItemSize } from './item-table-list';
|
||||
|
||||
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 {
|
||||
LONG_PRESS_PLAY_BEHAVIOR,
|
||||
PlayTooltip,
|
||||
} from '/@/renderer/features/shared/components/play-button-group';
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { useAlbumGroupImageSize, usePlayButtonBehavior } from '/@/renderer/store';
|
||||
import { LibraryItem, Song } from '/@/shared/types/domain-types';
|
||||
import { Play } from '/@/shared/types/types';
|
||||
@@ -35,6 +39,11 @@ export const AlbumGroupHeader = ({
|
||||
large: TableItemSize.LARGE,
|
||||
normal: TableItemSize.DEFAULT,
|
||||
}[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
|
||||
// enlarged the group's last row is grown so the total reaches the img size.
|
||||
const infoHeight =
|
||||
@@ -58,13 +67,12 @@ export const AlbumGroupHeader = ({
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div
|
||||
className={styles.imageContainer}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
style={imageContainerStyle}
|
||||
>
|
||||
<div
|
||||
className={styles.container}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<div className={styles.imageContainer} style={imageContainerStyle}>
|
||||
<ItemImage
|
||||
className={imageColumnStyles.compactImage}
|
||||
enableDebounce
|
||||
@@ -93,8 +101,29 @@ export const AlbumGroupHeader = ({
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.info} style={{ height: infoHeight }}>
|
||||
<div className={styles.albumName}>{song?.album ?? ''}</div>
|
||||
<div className={styles.artistName}>{song?.albumArtistName ?? ''}</div>
|
||||
<div className={styles.albumName}>
|
||||
{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>
|
||||
);
|
||||
|
||||
@@ -84,7 +84,12 @@ export const AlbumGroupColumn = (props: ItemTableListInnerColumn) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<TableColumnContainer {...props} enableAlternateRowColors={false}>
|
||||
<TableColumnContainer
|
||||
{...props}
|
||||
dragRef={null}
|
||||
enableAlternateRowColors={false}
|
||||
isDraggedOver={null}
|
||||
>
|
||||
<AlbumGroupHeader
|
||||
groupRowCount={groupRowCount}
|
||||
onPlay={handlePlay}
|
||||
|
||||
@@ -20,7 +20,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
label: i18n.t('table.config.label.albumGroup'),
|
||||
pinned: 'left',
|
||||
value: TableColumn.ALBUM_GROUP,
|
||||
width: 200,
|
||||
width: 240,
|
||||
},
|
||||
{
|
||||
align: 'center',
|
||||
|
||||
+25
-2
@@ -83,23 +83,46 @@ export const useRowInteractionDelegate = ({
|
||||
let pending: null | { edge: 'bottom' | 'top' | null; rowKey: string } = null;
|
||||
let rafId: null | number = null;
|
||||
|
||||
const isExcludedFromDragBorder = (el: HTMLElement) =>
|
||||
el.hasAttribute('data-exclude-row-drag-border');
|
||||
|
||||
const clearRow = (rowKey: string) => {
|
||||
root.querySelectorAll(`[data-row-index="${rowKey}"]`).forEach((node) => {
|
||||
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');
|
||||
});
|
||||
};
|
||||
|
||||
const applyRow = (rowKey: string, edge: 'bottom' | 'top') => {
|
||||
const nodes = root.querySelectorAll(`[data-row-index="${rowKey}"]`);
|
||||
nodes.forEach((node, idx) => {
|
||||
const nodes = Array.from(root.querySelectorAll(`[data-row-index="${rowKey}"]`));
|
||||
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;
|
||||
el.setAttribute('data-row-dragged-over', edge);
|
||||
if (idx === 0) {
|
||||
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 {
|
||||
el.removeAttribute('data-row-dragged-over-first');
|
||||
el.removeAttribute('data-exclude-row-drag-left');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -166,6 +166,11 @@
|
||||
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 {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
@@ -185,6 +190,11 @@
|
||||
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 > * {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
@@ -842,6 +842,7 @@ export const TableColumnContainer = (
|
||||
[styles.withHorizontalBorder]: showHorizontalBorder && clampHeight === null,
|
||||
[styles.withVerticalBorder]: showVerticalBorder,
|
||||
})}
|
||||
data-exclude-row-drag-border={props.type === TableColumn.ALBUM_GROUP ? true : undefined}
|
||||
data-row-index={isDataRow ? `${props.tableId}-${props.rowIndex}` : undefined}
|
||||
onClick={handleClick}
|
||||
onContextMenu={handleContextMenu}
|
||||
|
||||
@@ -2503,7 +2503,7 @@ export const useSettingsStore = createWithEqualityFn<SettingsSlice>()(
|
||||
id: TableColumn.ALBUM_GROUP,
|
||||
isEnabled: false,
|
||||
pinned: 'left',
|
||||
width: 200,
|
||||
width: 240,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user