mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 20:40:15 +02:00
various cleanup
This commit is contained in:
@@ -1,71 +0,0 @@
|
||||
.play-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background-color: rgb(255 255 255);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
transition: scale 0.2s linear;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
scale: 1.1;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 1;
|
||||
scale: 1;
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: rgb(0 0 0);
|
||||
stroke: rgb(0 0 0);
|
||||
}
|
||||
}
|
||||
|
||||
.secondary-button {
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
transition: scale 0.2s linear;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
scale: 1.1;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 1;
|
||||
scale: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.grid-card-controls-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.controls-row {
|
||||
width: 100%;
|
||||
height: calc(100% / 3);
|
||||
}
|
||||
|
||||
.bottom-controls {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
padding: 1rem 0.5rem;
|
||||
}
|
||||
|
||||
.favorite-wrapper {
|
||||
svg {
|
||||
fill: var(--theme-colors-primary-filled);
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
import type { PlayQueueAddOptions } from '/@/shared/types/types';
|
||||
import type { MouseEvent } from 'react';
|
||||
|
||||
import styles from './card-controls.module.css';
|
||||
|
||||
import {
|
||||
ALBUM_CONTEXT_MENU_ITEMS,
|
||||
ARTIST_CONTEXT_MENU_ITEMS,
|
||||
} from '/@/renderer/features/context-menu/context-menu-items';
|
||||
import { useHandleGeneralContextMenu } from '/@/renderer/features/context-menu/hooks/use-handle-context-menu';
|
||||
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||
import { Button } from '/@/shared/components/button/button';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||
import { Play } from '/@/shared/types/types';
|
||||
|
||||
export const CardControls = ({
|
||||
handlePlayQueueAdd,
|
||||
itemData,
|
||||
itemType,
|
||||
}: {
|
||||
handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void;
|
||||
itemData: any;
|
||||
itemType: LibraryItem;
|
||||
}) => {
|
||||
const playButtonBehavior = usePlayButtonBehavior();
|
||||
|
||||
const handlePlay = (e: MouseEvent<HTMLButtonElement>, playType?: Play) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handlePlayQueueAdd?.({
|
||||
byItemType: {
|
||||
id: [itemData.id],
|
||||
type: itemType,
|
||||
},
|
||||
playType: playType || playButtonBehavior,
|
||||
});
|
||||
};
|
||||
|
||||
const handleContextMenu = useHandleGeneralContextMenu(
|
||||
itemType,
|
||||
itemType === LibraryItem.ALBUM ? ALBUM_CONTEXT_MENU_ITEMS : ARTIST_CONTEXT_MENU_ITEMS,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.gridCardControlsContainer}>
|
||||
<div className={styles.bottomControls}>
|
||||
<button className={styles.playButton} onClick={handlePlay}>
|
||||
<Icon icon="mediaPlay" />
|
||||
</button>
|
||||
<Group gap="xs">
|
||||
<Button
|
||||
className={styles.secondaryButton}
|
||||
disabled
|
||||
p={5}
|
||||
style={{ svg: { fill: 'white !important' } }}
|
||||
variant="subtle"
|
||||
>
|
||||
<div className={itemData?.isFavorite ? styles.favoriteWrapper : ''}>
|
||||
{itemData?.isFavorite ? (
|
||||
<Icon icon="favorite" />
|
||||
) : (
|
||||
<Icon icon="favorite" />
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
<ActionIcon
|
||||
className={styles.secondaryButton}
|
||||
onClick={(e: any) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleContextMenu(e, [itemData]);
|
||||
}}
|
||||
p={5}
|
||||
style={{ svg: { fill: 'white !important' } }}
|
||||
variant="subtle"
|
||||
>
|
||||
<Icon icon="ellipsisHorizontal" />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
.row {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 22px;
|
||||
padding: 0 0.2rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--theme-colors-foreground);
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.row.secondary {
|
||||
color: var(--theme-colors-foreground-muted);
|
||||
}
|
||||
@@ -1,350 +0,0 @@
|
||||
import clsx from 'clsx';
|
||||
import formatDuration from 'format-duration';
|
||||
import React from 'react';
|
||||
import { TFunction, useTranslation } from 'react-i18next';
|
||||
import { generatePath } from 'react-router';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
import styles from './card-rows.module.css';
|
||||
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { formatDateAbsolute, formatDateRelative, formatRating } from '/@/renderer/utils/format';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import {
|
||||
Album,
|
||||
AlbumArtist,
|
||||
Artist,
|
||||
ExplicitStatus,
|
||||
Playlist,
|
||||
Song,
|
||||
} from '/@/shared/types/domain-types';
|
||||
import { CardRow } from '/@/shared/types/types';
|
||||
|
||||
interface CardRowsProps {
|
||||
data: any;
|
||||
rows: CardRow<Album>[] | CardRow<AlbumArtist>[] | CardRow<Artist>[];
|
||||
}
|
||||
|
||||
export const CardRows = ({ data, rows }: CardRowsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
{rows.map((row, index: number) => {
|
||||
if (row.arrayProperty && row.route) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(styles.row, {
|
||||
[styles.secondary]: index > 0,
|
||||
})}
|
||||
key={`row-${row.property}-${index}`}
|
||||
>
|
||||
{data[row.property].map((item: any, itemIndex: number) => (
|
||||
<React.Fragment key={`${data.id}-${item.id}`}>
|
||||
{itemIndex > 0 && (
|
||||
<Text
|
||||
isMuted
|
||||
isNoSelect
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
padding: '0 2px 0 1px',
|
||||
}}
|
||||
>
|
||||
,
|
||||
</Text>
|
||||
)}{' '}
|
||||
<Text
|
||||
component={Link}
|
||||
isLink
|
||||
isMuted={index > 0}
|
||||
isNoSelect
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
overflow="hidden"
|
||||
size={index > 0 ? 'sm' : 'md'}
|
||||
to={generatePath(
|
||||
row.route!.route,
|
||||
row.route!.slugs?.reduce((acc, slug) => {
|
||||
return {
|
||||
...acc,
|
||||
[slug.slugProperty]:
|
||||
data[row.property][itemIndex][
|
||||
slug.idProperty
|
||||
],
|
||||
};
|
||||
}, {}),
|
||||
)}
|
||||
>
|
||||
{row.arrayProperty &&
|
||||
(row.format
|
||||
? row.format(item, t)
|
||||
: item[row.arrayProperty])}
|
||||
</Text>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (row.arrayProperty) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(styles.row, {
|
||||
[styles.secondary]: index > 0,
|
||||
})}
|
||||
key={`row-${row.property}`}
|
||||
>
|
||||
{data[row.property].map((item: any) => (
|
||||
<Text
|
||||
isMuted={index > 0}
|
||||
isNoSelect
|
||||
key={`${data.id}-${item.id}`}
|
||||
overflow="hidden"
|
||||
size={index > 0 ? 'sm' : 'md'}
|
||||
>
|
||||
{row.arrayProperty &&
|
||||
(row.format
|
||||
? row.format(item, t)
|
||||
: item[row.arrayProperty])}
|
||||
</Text>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(styles.row, {
|
||||
[styles.secondary]: index > 0,
|
||||
})}
|
||||
key={`row-${row.property}`}
|
||||
>
|
||||
{row.route ? (
|
||||
<Text
|
||||
component={Link}
|
||||
isLink
|
||||
isNoSelect
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
overflow="hidden"
|
||||
to={generatePath(
|
||||
row.route.route,
|
||||
row.route.slugs?.reduce((acc, slug) => {
|
||||
return {
|
||||
...acc,
|
||||
[slug.slugProperty]: data[slug.idProperty],
|
||||
};
|
||||
}, {}),
|
||||
)}
|
||||
>
|
||||
{data && (row.format ? row.format(data, t) : data[row.property])}
|
||||
</Text>
|
||||
) : (
|
||||
<Text
|
||||
isMuted={index > 0}
|
||||
isNoSelect
|
||||
overflow="hidden"
|
||||
size={index > 0 ? 'sm' : 'md'}
|
||||
>
|
||||
{data && (row.format ? row.format(data, t) : data[row.property])}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ALBUM_CARD_ROWS: { [key: string]: CardRow<Album> } = {
|
||||
albumArtists: {
|
||||
arrayProperty: 'name',
|
||||
property: 'albumArtists',
|
||||
route: {
|
||||
route: AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL,
|
||||
slugs: [{ idProperty: 'id', slugProperty: 'albumArtistId' }],
|
||||
},
|
||||
},
|
||||
artists: {
|
||||
arrayProperty: 'name',
|
||||
property: 'artists',
|
||||
route: {
|
||||
route: AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL,
|
||||
slugs: [{ idProperty: 'id', slugProperty: 'albumArtistId' }],
|
||||
},
|
||||
},
|
||||
createdAt: {
|
||||
format: (song) => formatDateAbsolute(song.createdAt),
|
||||
property: 'createdAt',
|
||||
},
|
||||
duration: {
|
||||
format: (album) => (album.duration === null ? null : formatDuration(album.duration)),
|
||||
property: 'duration',
|
||||
},
|
||||
explicitStatus: {
|
||||
format: (album, t: TFunction) =>
|
||||
album.explicitStatus === ExplicitStatus.EXPLICIT
|
||||
? t('common.explicit', { postProcess: 'sentenceCase' })
|
||||
: album.explicitStatus === ExplicitStatus.CLEAN
|
||||
? t('common.clean', { postProcess: 'sentenceCase' })
|
||||
: null,
|
||||
property: 'explicitStatus',
|
||||
},
|
||||
lastPlayedAt: {
|
||||
format: (album) => formatDateRelative(album.lastPlayedAt),
|
||||
property: 'lastPlayedAt',
|
||||
},
|
||||
name: {
|
||||
property: 'name',
|
||||
route: {
|
||||
route: AppRoute.LIBRARY_ALBUMS_DETAIL,
|
||||
slugs: [{ idProperty: 'id', slugProperty: 'albumId' }],
|
||||
},
|
||||
},
|
||||
playCount: {
|
||||
property: 'playCount',
|
||||
},
|
||||
rating: {
|
||||
format: (album) => formatRating(album),
|
||||
property: 'userRating',
|
||||
},
|
||||
releaseDate: {
|
||||
property: 'releaseDate',
|
||||
},
|
||||
releaseYear: {
|
||||
property: 'releaseYear',
|
||||
},
|
||||
songCount: {
|
||||
property: 'songCount',
|
||||
},
|
||||
};
|
||||
|
||||
export const SONG_CARD_ROWS: { [key: string]: CardRow<Song> } = {
|
||||
album: {
|
||||
property: 'album',
|
||||
route: {
|
||||
route: AppRoute.LIBRARY_ALBUMS_DETAIL,
|
||||
slugs: [{ idProperty: 'albumId', slugProperty: 'albumId' }],
|
||||
},
|
||||
},
|
||||
albumArtists: {
|
||||
arrayProperty: 'name',
|
||||
property: 'albumArtists',
|
||||
route: {
|
||||
route: AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL,
|
||||
slugs: [{ idProperty: 'id', slugProperty: 'albumArtistId' }],
|
||||
},
|
||||
},
|
||||
artists: {
|
||||
arrayProperty: 'name',
|
||||
property: 'artists',
|
||||
route: {
|
||||
route: AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL,
|
||||
slugs: [{ idProperty: 'id', slugProperty: 'albumArtistId' }],
|
||||
},
|
||||
},
|
||||
createdAt: {
|
||||
format: (song) => formatDateAbsolute(song.createdAt),
|
||||
property: 'createdAt',
|
||||
},
|
||||
duration: {
|
||||
format: (song) => (song.duration === null ? null : formatDuration(song.duration)),
|
||||
property: 'duration',
|
||||
},
|
||||
explicitStatus: {
|
||||
format: (song, t: TFunction) =>
|
||||
song.explicitStatus === ExplicitStatus.EXPLICIT
|
||||
? t('common.explicit', { postProcess: 'sentenceCase' })
|
||||
: song.explicitStatus === ExplicitStatus.CLEAN
|
||||
? t('common.clean', { postProcess: 'sentenceCase' })
|
||||
: null,
|
||||
property: 'explicitStatus',
|
||||
},
|
||||
lastPlayedAt: {
|
||||
format: (song) => formatDateRelative(song.lastPlayedAt),
|
||||
property: 'lastPlayedAt',
|
||||
},
|
||||
name: {
|
||||
property: 'name',
|
||||
route: {
|
||||
route: AppRoute.LIBRARY_ALBUMS_DETAIL,
|
||||
slugs: [{ idProperty: 'albumId', slugProperty: 'albumId' }],
|
||||
},
|
||||
},
|
||||
playCount: {
|
||||
property: 'playCount',
|
||||
},
|
||||
rating: {
|
||||
format: (song) => formatRating(song),
|
||||
property: 'userRating',
|
||||
},
|
||||
releaseDate: {
|
||||
property: 'releaseDate',
|
||||
},
|
||||
releaseYear: {
|
||||
property: 'releaseYear',
|
||||
},
|
||||
};
|
||||
|
||||
export const ALBUMARTIST_CARD_ROWS: { [key: string]: CardRow<AlbumArtist> } = {
|
||||
albumCount: {
|
||||
property: 'albumCount',
|
||||
},
|
||||
duration: {
|
||||
format: (artist) => (artist.duration === null ? null : formatDuration(artist.duration)),
|
||||
property: 'duration',
|
||||
},
|
||||
genres: {
|
||||
property: 'genres',
|
||||
},
|
||||
lastPlayedAt: {
|
||||
format: (artist) => formatDateRelative(artist.lastPlayedAt),
|
||||
property: 'lastPlayedAt',
|
||||
},
|
||||
name: {
|
||||
property: 'name',
|
||||
route: {
|
||||
route: AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL,
|
||||
slugs: [{ idProperty: 'id', slugProperty: 'albumArtistId' }],
|
||||
},
|
||||
},
|
||||
playCount: {
|
||||
property: 'playCount',
|
||||
},
|
||||
rating: {
|
||||
format: (artist) => formatRating(artist),
|
||||
property: 'userRating',
|
||||
},
|
||||
songCount: {
|
||||
property: 'songCount',
|
||||
},
|
||||
};
|
||||
|
||||
export const PLAYLIST_CARD_ROWS: { [key: string]: CardRow<Playlist> } = {
|
||||
duration: {
|
||||
format: (playlist) =>
|
||||
playlist.duration === null ? null : formatDuration(playlist.duration),
|
||||
property: 'duration',
|
||||
},
|
||||
name: {
|
||||
property: 'name',
|
||||
route: {
|
||||
route: AppRoute.PLAYLISTS_DETAIL_SONGS,
|
||||
slugs: [{ idProperty: 'id', slugProperty: 'playlistId' }],
|
||||
},
|
||||
},
|
||||
nameFull: {
|
||||
property: 'name',
|
||||
route: {
|
||||
route: AppRoute.PLAYLISTS_DETAIL_SONGS,
|
||||
slugs: [{ idProperty: 'id', slugProperty: 'playlistId' }],
|
||||
},
|
||||
},
|
||||
owner: {
|
||||
property: 'owner',
|
||||
},
|
||||
public: {
|
||||
property: 'public',
|
||||
},
|
||||
songCount: {
|
||||
property: 'songCount',
|
||||
},
|
||||
};
|
||||
@@ -1,67 +0,0 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
pointer-events: auto;
|
||||
|
||||
&:global(.card-controls) {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.container.hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
aspect-ratio: 1/1;
|
||||
overflow: hidden;
|
||||
background: var(--theme-card-default-bg);
|
||||
border-radius: var(--theme-card-poster-radius);
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
user-select: none;
|
||||
content: '';
|
||||
background: linear-gradient(0deg, rgb(0 0 0 / 100%) 35%, rgb(0 0 0 / 0%) 100%);
|
||||
opacity: 0;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&::before {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover:global(.card-controls) {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.image {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 100% !important;
|
||||
max-height: 100%;
|
||||
border: 0;
|
||||
|
||||
img {
|
||||
height: 100%;
|
||||
object-fit: var(--theme-image-fit);
|
||||
}
|
||||
}
|
||||
|
||||
.detail-container {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { generatePath, Link } from 'react-router';
|
||||
|
||||
import styles from './poster-card.module.css';
|
||||
|
||||
import { CardRows } from '/@/renderer/components/card/card-rows';
|
||||
import { Image } from '/@/shared/components/image/image';
|
||||
import { Skeleton } from '/@/shared/components/skeleton/skeleton';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
import { Album, AlbumArtist, Artist, LibraryItem } from '/@/shared/types/domain-types';
|
||||
import { CardRoute, CardRow, Play, PlayQueueAddOptions } from '/@/shared/types/types';
|
||||
|
||||
interface BaseGridCardProps {
|
||||
controls: {
|
||||
cardRows: CardRow<Album>[] | CardRow<AlbumArtist>[] | CardRow<Artist>[];
|
||||
handleFavorite: (options: {
|
||||
id: string[];
|
||||
isFavorite: boolean;
|
||||
itemType: LibraryItem;
|
||||
serverId: string;
|
||||
}) => void;
|
||||
handlePlayQueueAdd: ((options: PlayQueueAddOptions) => void) | undefined;
|
||||
itemType: LibraryItem;
|
||||
playButtonBehavior: Play;
|
||||
route: CardRoute;
|
||||
};
|
||||
data: any;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export const PosterCard = ({
|
||||
controls,
|
||||
data,
|
||||
isLoading,
|
||||
uniqueId,
|
||||
}: BaseGridCardProps & { uniqueId: string }) => {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
if (!isLoading) {
|
||||
const path = generatePath(
|
||||
controls.route.route as string,
|
||||
controls.route.slugs?.reduce((acc, slug) => {
|
||||
return {
|
||||
...acc,
|
||||
[slug.slugProperty]: data[slug.idProperty],
|
||||
};
|
||||
}, {}),
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.container}
|
||||
key={`${uniqueId}-${data.id}`}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<Link className={styles.imageContainer} to={path}>
|
||||
<Image className={styles.image} src={data?.imageUrl} />
|
||||
{/* <GridCardControls
|
||||
handleFavorite={controls.handleFavorite}
|
||||
isHovered={isHovered}
|
||||
itemData={data}
|
||||
itemType={controls.itemType}
|
||||
/> */}
|
||||
</Link>
|
||||
<div className={styles.detailContainer}>
|
||||
<CardRows data={data} rows={controls.cardRows} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container} key={`placeholder-${uniqueId}-${data.id}`}>
|
||||
<div className={styles.imageContainer}>
|
||||
<Skeleton className={styles.image} />
|
||||
</div>
|
||||
<div className={styles.detailContainer}>
|
||||
<Stack gap="xs">
|
||||
{(controls?.cardRows || []).map((row, index) => (
|
||||
<Skeleton height={14} key={`${index}-${row.arrayProperty}`} />
|
||||
))}
|
||||
</Stack>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
import type { ICellRendererParams } from '@ag-grid-community/core';
|
||||
|
||||
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';
|
||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||
|
||||
export const ActionsCell = ({ api, context }: ICellRendererParams) => {
|
||||
return (
|
||||
<CellContainer position="center">
|
||||
<ActionIcon
|
||||
icon="ellipsisHorizontal"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
context.onCellContextMenu(undefined, api, e);
|
||||
}}
|
||||
size="sm"
|
||||
variant="subtle"
|
||||
/>
|
||||
</CellContainer>
|
||||
);
|
||||
};
|
||||
@@ -1,52 +0,0 @@
|
||||
import type { AlbumArtist, Artist } from '/@/shared/types/domain-types';
|
||||
import type { ICellRendererParams } from '@ag-grid-community/core';
|
||||
|
||||
import React from 'react';
|
||||
import { generatePath } from 'react-router';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { Separator } from '/@/shared/components/separator/separator';
|
||||
import { Skeleton } from '/@/shared/components/skeleton/skeleton';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
|
||||
export const AlbumArtistCell = ({ data, value }: ICellRendererParams) => {
|
||||
if (value === undefined) {
|
||||
return (
|
||||
<CellContainer position="left">
|
||||
<Skeleton height="1rem" width="80%" />
|
||||
</CellContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CellContainer position="left">
|
||||
<Text isMuted overflow="hidden" size="md">
|
||||
{value?.map((item: AlbumArtist | Artist, index: number) => (
|
||||
<React.Fragment key={`row-${item.id}-${data.uniqueId}`}>
|
||||
{index > 0 && <Separator />}
|
||||
{item.id ? (
|
||||
<Text
|
||||
component={Link}
|
||||
isLink
|
||||
isMuted
|
||||
overflow="hidden"
|
||||
size="md"
|
||||
to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
|
||||
albumArtistId: item.id,
|
||||
})}
|
||||
>
|
||||
{item.name || '—'}
|
||||
</Text>
|
||||
) : (
|
||||
<Text isMuted overflow="hidden" size="md">
|
||||
{item.name || '—'}
|
||||
</Text>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Text>
|
||||
</CellContainer>
|
||||
);
|
||||
};
|
||||
@@ -1,52 +0,0 @@
|
||||
import type { AlbumArtist, Artist } from '/@/shared/types/domain-types';
|
||||
import type { ICellRendererParams } from '@ag-grid-community/core';
|
||||
|
||||
import React from 'react';
|
||||
import { generatePath } from 'react-router';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { Separator } from '/@/shared/components/separator/separator';
|
||||
import { Skeleton } from '/@/shared/components/skeleton/skeleton';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
|
||||
export const ArtistCell = ({ data, value }: ICellRendererParams) => {
|
||||
if (value === undefined) {
|
||||
return (
|
||||
<CellContainer position="left">
|
||||
<Skeleton height="1rem" width="80%" />
|
||||
</CellContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CellContainer position="left">
|
||||
<Text isMuted overflow="hidden" size="md">
|
||||
{value?.map((item: AlbumArtist | Artist, index: number) => (
|
||||
<React.Fragment key={`row-${item.id}-${data.uniqueId}`}>
|
||||
{index > 0 && <Separator />}
|
||||
{item.id ? (
|
||||
<Text
|
||||
component={Link}
|
||||
isLink
|
||||
isMuted
|
||||
overflow="hidden"
|
||||
size="md"
|
||||
to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
|
||||
albumArtistId: item.id,
|
||||
})}
|
||||
>
|
||||
{item.name || '—'}
|
||||
</Text>
|
||||
) : (
|
||||
<Text isMuted overflow="hidden" size="md">
|
||||
{item.name || '—'}
|
||||
</Text>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Text>
|
||||
</CellContainer>
|
||||
);
|
||||
};
|
||||
@@ -1,35 +0,0 @@
|
||||
.play-button {
|
||||
position: absolute;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background: var(--theme-colors-white);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
opacity: 0.7;
|
||||
transition: scale 0.1s ease-in-out;
|
||||
|
||||
svg {
|
||||
color: var(--theme-colors-black);
|
||||
fill: var(--theme-colors-black);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--theme-colors-white);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.list-controls-container {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
import clsx from 'clsx';
|
||||
|
||||
import styles from './combined-title-cell-controls.module.css';
|
||||
|
||||
import { usePlayQueueAdd } from '/@/renderer/features/player/hooks/use-playqueue-add';
|
||||
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||
import { Play } from '/@/shared/types/types';
|
||||
|
||||
export const ListCoverControls = ({
|
||||
className,
|
||||
context,
|
||||
itemData,
|
||||
itemType,
|
||||
uniqueId,
|
||||
}: {
|
||||
className?: string;
|
||||
context: Record<string, any>;
|
||||
itemData: any;
|
||||
itemType: LibraryItem;
|
||||
uniqueId?: string;
|
||||
}) => {
|
||||
const playButtonBehavior = usePlayButtonBehavior();
|
||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||
const isQueue = Boolean(context?.isQueue);
|
||||
|
||||
const handlePlay = async (e: React.MouseEvent<HTMLButtonElement>, playType?: Play) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
handlePlayQueueAdd?.({
|
||||
byItemType: {
|
||||
id: [itemData.id],
|
||||
type: itemType,
|
||||
},
|
||||
playType: playType || playButtonBehavior,
|
||||
});
|
||||
};
|
||||
|
||||
const handlePlayFromQueue = () => {
|
||||
context.handleDoubleClick({
|
||||
data: {
|
||||
uniqueId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={clsx(styles.listControlsContainer, className)}>
|
||||
<ActionIcon
|
||||
classNames={{ root: styles.playButton }}
|
||||
icon="mediaPlay"
|
||||
onClick={isQueue ? handlePlayFromQueue : handlePlay}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,50 +0,0 @@
|
||||
.cell-container {
|
||||
display: grid;
|
||||
grid-template-areas: 'image info';
|
||||
grid-template-rows: 1fr;
|
||||
grid-auto-columns: 1fr;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
letter-spacing: 0.5px;
|
||||
|
||||
&:hover {
|
||||
.play-button {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.play-button {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.image-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
grid-area: image;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.metadata-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
grid-area: info;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.metadata-wrapper > div {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.skeleton-metadata {
|
||||
height: var(--theme-font-size-md);
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
import type { ICellRendererParams } from '@ag-grid-community/core';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { generatePath } from 'react-router';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
import styles from './combined-title-cell.module.css';
|
||||
|
||||
import { ListCoverControls } from '/@/renderer/components/virtual-table/cells/combined-title-cell-controls';
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { SEPARATOR_STRING } from '/@/shared/api/utils';
|
||||
import { Image } from '/@/shared/components/image/image';
|
||||
import { Skeleton } from '/@/shared/components/skeleton/skeleton';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import { AlbumArtist, Artist } from '/@/shared/types/domain-types';
|
||||
|
||||
export const CombinedTitleCell = ({
|
||||
context,
|
||||
data,
|
||||
node,
|
||||
rowIndex,
|
||||
value,
|
||||
}: ICellRendererParams) => {
|
||||
const artists = useMemo(() => {
|
||||
if (!value) return null;
|
||||
return value.artists?.length ? value.artists : value.albumArtists;
|
||||
}, [value]);
|
||||
|
||||
if (value === undefined) {
|
||||
return (
|
||||
<div
|
||||
className={styles.cellContainer}
|
||||
style={{ gridTemplateColumns: `${node.rowHeight || 40}px minmax(0, 1fr)` }}
|
||||
>
|
||||
<div
|
||||
className={styles.imageWrapper}
|
||||
style={{
|
||||
height: `${(node.rowHeight || 40) - 10}px`,
|
||||
width: `${(node.rowHeight || 40) - 10}px`,
|
||||
}}
|
||||
>
|
||||
<Skeleton className={styles.image} />
|
||||
</div>
|
||||
<Skeleton className={styles.skeletonMetadata} height="1rem" width="80%" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.cellContainer}
|
||||
style={{ gridTemplateColumns: `${node.rowHeight || 40}px minmax(0, 1fr)` }}
|
||||
>
|
||||
<div
|
||||
className={styles.imageWrapper}
|
||||
style={{
|
||||
height: `${(node.rowHeight || 40) - 10}px`,
|
||||
width: `${(node.rowHeight || 40) - 10}px`,
|
||||
}}
|
||||
>
|
||||
<Image alt="cover" className={styles.image} src={value.imageUrl} />
|
||||
|
||||
<ListCoverControls
|
||||
className={styles.playButton}
|
||||
context={context}
|
||||
itemData={value}
|
||||
itemType={context.itemType}
|
||||
uniqueId={data?.uniqueId}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.metadataWrapper}>
|
||||
<Text className="current-song-child" overflow="hidden" size="md">
|
||||
{value.name}
|
||||
</Text>
|
||||
<Text isMuted overflow="hidden" size="md">
|
||||
{artists?.length ? (
|
||||
artists.map((artist: AlbumArtist | Artist, index: number) => (
|
||||
<React.Fragment key={`queue-${rowIndex}-artist-${artist.id}`}>
|
||||
{index > 0 ? SEPARATOR_STRING : null}
|
||||
{artist.id ? (
|
||||
<Text
|
||||
component={Link}
|
||||
isLink
|
||||
isMuted
|
||||
overflow="hidden"
|
||||
size="md"
|
||||
style={{ width: 'fit-content' }}
|
||||
to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
|
||||
albumArtistId: artist.id,
|
||||
})}
|
||||
>
|
||||
{artist.name}
|
||||
</Text>
|
||||
) : (
|
||||
<Text
|
||||
isMuted
|
||||
overflow="hidden"
|
||||
size="md"
|
||||
style={{ width: 'fit-content' }}
|
||||
>
|
||||
{artist.name}
|
||||
</Text>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))
|
||||
) : (
|
||||
<Text isMuted>—</Text>
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,61 +0,0 @@
|
||||
import type { ICellRendererParams } from '@ag-grid-community/core';
|
||||
|
||||
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';
|
||||
import { useCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation';
|
||||
import { useDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation';
|
||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||
|
||||
export const FavoriteCell = ({ data, node, value }: ICellRendererParams) => {
|
||||
const createMutation = useCreateFavorite({});
|
||||
const deleteMutation = useDeleteFavorite({});
|
||||
|
||||
const handleToggleFavorite = () => {
|
||||
const newFavoriteValue = !value;
|
||||
|
||||
if (newFavoriteValue) {
|
||||
createMutation.mutate(
|
||||
{
|
||||
apiClientProps: { serverId: data.serverId },
|
||||
query: {
|
||||
id: [data.id],
|
||||
type: data.itemType,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
node.setData({ ...data, userFavorite: newFavoriteValue });
|
||||
},
|
||||
},
|
||||
);
|
||||
} else {
|
||||
deleteMutation.mutate(
|
||||
{
|
||||
apiClientProps: { serverId: data.serverId },
|
||||
query: {
|
||||
id: [data.id],
|
||||
type: data.itemType,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
node.setData({ ...data, userFavorite: newFavoriteValue });
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<CellContainer position="center">
|
||||
<ActionIcon
|
||||
icon="favorite"
|
||||
iconProps={{
|
||||
fill: !value ? undefined : 'primary',
|
||||
}}
|
||||
onClick={handleToggleFavorite}
|
||||
size="sm"
|
||||
variant="subtle"
|
||||
/>
|
||||
</CellContainer>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
.container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import { ICellRendererParams } from '@ag-grid-community/core';
|
||||
import { useState } from 'react';
|
||||
|
||||
import styles from './full-width-disc-cell.module.css';
|
||||
|
||||
import { getNodesByDiscNumber, setNodeSelection } from '/@/renderer/components/virtual-table/utils';
|
||||
import { Button } from '/@/shared/components/button/button';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
|
||||
export const FullWidthDiscCell = ({ api, data, node }: ICellRendererParams) => {
|
||||
const [isSelected, setIsSelected] = useState(false);
|
||||
|
||||
const handleToggleDiscNodes = () => {
|
||||
if (!data) return;
|
||||
const split: string[] = node.data.id.split('-');
|
||||
const discNumber = Number(split[1]);
|
||||
// the subtitle could have '-' in it; make sure to have all remaining items
|
||||
const subtitle = split.length === 3 ? split.slice(2).join('-') : null;
|
||||
const nodes = getNodesByDiscNumber({ api, discNumber, subtitle });
|
||||
|
||||
setNodeSelection({ isSelected: !isSelected, nodes });
|
||||
setIsSelected((prev) => !prev);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Group justify="space-between" w="100%">
|
||||
<Button
|
||||
leftSection={isSelected ? <Icon icon="squareCheck" /> : <Icon icon="square" />}
|
||||
onClick={handleToggleDiscNodes}
|
||||
size="compact-md"
|
||||
variant="subtle"
|
||||
>
|
||||
{data.name}
|
||||
</Button>
|
||||
</Group>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
.cell-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.cell-container.right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.cell-container.center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cell-container.left {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
import type { ICellRendererParams } from '@ag-grid-community/core';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
import styles from './generic-cell.module.css';
|
||||
|
||||
import { Skeleton } from '/@/shared/components/skeleton/skeleton';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
|
||||
type Options = {
|
||||
array?: boolean;
|
||||
isArray?: boolean;
|
||||
isLink?: boolean;
|
||||
position?: 'center' | 'left' | 'right';
|
||||
primary?: boolean;
|
||||
};
|
||||
|
||||
export const GenericCell = ({ value, valueFormatted }: ICellRendererParams, options?: Options) => {
|
||||
const { isLink, position, primary } = options || {};
|
||||
const displayedValue = valueFormatted || value;
|
||||
|
||||
if (value === undefined) {
|
||||
return (
|
||||
<CellContainer position={position || 'left'}>
|
||||
<Skeleton height="1rem" width="80%" />
|
||||
</CellContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CellContainer position={position || 'left'}>
|
||||
{isLink ? (
|
||||
<Text
|
||||
component={Link}
|
||||
isLink={isLink}
|
||||
isMuted={!primary}
|
||||
overflow="hidden"
|
||||
size="md"
|
||||
to={displayedValue.link}
|
||||
>
|
||||
{isLink ? displayedValue.value : displayedValue}
|
||||
</Text>
|
||||
) : (
|
||||
<Text isMuted={!primary} isNoSelect={false} overflow="hidden" size="md">
|
||||
{displayedValue}
|
||||
</Text>
|
||||
)}
|
||||
</CellContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export const CellContainer = ({
|
||||
children,
|
||||
position,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
position: 'center' | 'left' | 'right';
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx({
|
||||
[styles.cellContainer]: true,
|
||||
[styles.center]: position === 'center',
|
||||
[styles.left]: position === 'left' || !position,
|
||||
[styles.right]: position === 'right',
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,35 +0,0 @@
|
||||
import type { AlbumArtist, Artist } from '/@/shared/types/domain-types';
|
||||
import type { ICellRendererParams } from '@ag-grid-community/core';
|
||||
|
||||
import React from 'react';
|
||||
import { generatePath, Link } from 'react-router';
|
||||
|
||||
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';
|
||||
import { useGenreRoute } from '/@/renderer/hooks/use-genre-route';
|
||||
import { Separator } from '/@/shared/components/separator/separator';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
|
||||
export const GenreCell = ({ data, value }: ICellRendererParams) => {
|
||||
const genrePath = useGenreRoute();
|
||||
return (
|
||||
<CellContainer position="left">
|
||||
<Text isMuted overflow="hidden" size="md">
|
||||
{value?.map((item: AlbumArtist | Artist, index: number) => (
|
||||
<React.Fragment key={`row-${item.id}-${data.uniqueId}`}>
|
||||
{index > 0 && <Separator />}
|
||||
<Text
|
||||
component={Link}
|
||||
isLink
|
||||
isMuted
|
||||
overflow="hidden"
|
||||
size="md"
|
||||
to={generatePath(genrePath, { genreId: item.id })}
|
||||
>
|
||||
{item.name || '—'}
|
||||
</Text>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Text>
|
||||
</CellContainer>
|
||||
);
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
import type { ICellRendererParams } from '@ag-grid-community/core';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';
|
||||
import { replaceURLWithHTMLLinks } from '/@/renderer/utils/linkify';
|
||||
import { Skeleton } from '/@/shared/components/skeleton/skeleton';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
|
||||
export const NoteCell = ({ value }: ICellRendererParams) => {
|
||||
const formattedValue = useMemo(() => {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return replaceURLWithHTMLLinks(value);
|
||||
}, [value]);
|
||||
|
||||
if (value === undefined) {
|
||||
return (
|
||||
<CellContainer position="left">
|
||||
<Skeleton height="1rem" width="80%" />
|
||||
</CellContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CellContainer position="left">
|
||||
<Text isMuted overflow="hidden">
|
||||
{formattedValue}
|
||||
</Text>
|
||||
</CellContainer>
|
||||
);
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
import type { ICellRendererParams } from '@ag-grid-community/core';
|
||||
|
||||
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';
|
||||
import { useSetRating } from '/@/renderer/features/shared/mutations/set-rating-mutation';
|
||||
import { Rating } from '/@/shared/components/rating/rating';
|
||||
|
||||
export const RatingCell = ({ node, value }: ICellRendererParams) => {
|
||||
const updateRatingMutation = useSetRating({});
|
||||
|
||||
const handleUpdateRating = (rating: number) => {
|
||||
updateRatingMutation.mutate(
|
||||
{
|
||||
apiClientProps: { serverId: value?.serverId || '' },
|
||||
query: {
|
||||
item: [value],
|
||||
rating,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
node.setData({ ...node.data, userRating: rating });
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<CellContainer position="center">
|
||||
<Rating onChange={handleUpdateRating} size="xs" value={value?.userRating} />
|
||||
</CellContainer>
|
||||
);
|
||||
};
|
||||
@@ -1,163 +0,0 @@
|
||||
import type { ICellRendererParams } from '@ag-grid-community/core';
|
||||
|
||||
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
|
||||
// const AnimatedSvg = () => {
|
||||
// return (
|
||||
// <div style={{ height: '1rem', transform: 'rotate(180deg)', width: '1rem' }}>
|
||||
// <svg
|
||||
// viewBox="100 130 57 80"
|
||||
// xmlns="http://www.w3.org/2000/svg"
|
||||
// >
|
||||
// <g>
|
||||
// <rect
|
||||
// fill="var(--theme-colors-primary-filled)"
|
||||
// height="80"
|
||||
// id="bar-1"
|
||||
// width="12"
|
||||
// x="100"
|
||||
// y="130"
|
||||
// >
|
||||
// <animate
|
||||
// attributeName="height"
|
||||
// begin="0.1s"
|
||||
// calcMode="spline"
|
||||
// dur="0.95s"
|
||||
// keySplines="0.42 0 0.58 1; 0.42 0 0.58 1"
|
||||
// keyTimes="0; 0.47368; 1"
|
||||
// repeatCount="indefinite"
|
||||
// values="80;15;80"
|
||||
// />
|
||||
// </rect>
|
||||
// <rect
|
||||
// fill="var(--theme-colors-primary-filled)"
|
||||
// height="80"
|
||||
// id="bar-2"
|
||||
// width="12"
|
||||
// x="115"
|
||||
// y="130"
|
||||
// >
|
||||
// <animate
|
||||
// attributeName="height"
|
||||
// begin="0.1s"
|
||||
// calcMode="spline"
|
||||
// dur="0.95s"
|
||||
// keySplines="0.45 0 0.55 1; 0.45 0 0.55 1"
|
||||
// keyTimes="0; 0.44444; 1"
|
||||
// repeatCount="indefinite"
|
||||
// values="25;80;25"
|
||||
// />
|
||||
// </rect>
|
||||
// <rect
|
||||
// fill="var(--theme-colors-primary-filled)"
|
||||
// height="80"
|
||||
// id="bar-3"
|
||||
// width="12"
|
||||
// x="130"
|
||||
// y="130"
|
||||
// >
|
||||
// <animate
|
||||
// attributeName="height"
|
||||
// begin="0.1s"
|
||||
// calcMode="spline"
|
||||
// dur="0.85s"
|
||||
// keySplines="0.65 0 0.35 1; 0.65 0 0.35 1"
|
||||
// keyTimes="0; 0.42105; 1"
|
||||
// repeatCount="indefinite"
|
||||
// values="80;10;80"
|
||||
// />
|
||||
// </rect>
|
||||
// <rect
|
||||
// fill="var(--theme-colors-primary-filled)"
|
||||
// height="80"
|
||||
// id="bar-4"
|
||||
// width="12"
|
||||
// x="145"
|
||||
// y="130"
|
||||
// >
|
||||
// <animate
|
||||
// attributeName="height"
|
||||
// begin="0.1s"
|
||||
// calcMode="spline"
|
||||
// dur="1.05s"
|
||||
// keySplines="0.42 0 0.58 1; 0.42 0 0.58 1"
|
||||
// keyTimes="0; 0.31579; 1"
|
||||
// repeatCount="indefinite"
|
||||
// values="30;80;30"
|
||||
// />
|
||||
// </rect>
|
||||
// </g>
|
||||
// </svg>
|
||||
// </div>
|
||||
// );
|
||||
// };
|
||||
|
||||
// const StaticSvg = () => {
|
||||
// return (
|
||||
// <div style={{ height: '1rem', transform: 'rotate(180deg)', width: '1rem' }}>
|
||||
// <svg
|
||||
// viewBox="100 130 57 80"
|
||||
// xmlns="http://www.w3.org/2000/svg"
|
||||
// >
|
||||
// <rect
|
||||
// fill="var(--primary-color)"
|
||||
// height="20"
|
||||
// width="12"
|
||||
// x="100"
|
||||
// y="130"
|
||||
// />
|
||||
// <rect
|
||||
// fill="var(--primary-color)"
|
||||
// height="60"
|
||||
// width="12"
|
||||
// x="115"
|
||||
// y="130"
|
||||
// />
|
||||
// <rect
|
||||
// fill="var(--primary-color)"
|
||||
// height="80"
|
||||
// width="12"
|
||||
// x="130"
|
||||
// y="130"
|
||||
// />
|
||||
// <rect
|
||||
// fill="var(--primary-color)"
|
||||
// height="45"
|
||||
// width="12"
|
||||
// x="145"
|
||||
// y="130"
|
||||
// />
|
||||
// </svg>
|
||||
// </div>
|
||||
// );
|
||||
// };
|
||||
|
||||
export const RowIndexCell = ({ eGridCell, value }: ICellRendererParams) => {
|
||||
const classList = eGridCell.classList;
|
||||
// const isFocused = classList.contains('focused');
|
||||
const isPlaying = classList.contains('playing');
|
||||
const isCurrentSong =
|
||||
classList.contains('current-song-cell') || classList.contains('current-playlist-song-cell');
|
||||
|
||||
return (
|
||||
<CellContainer position="right">
|
||||
{isPlaying && isCurrentSong ? (
|
||||
<Icon fill="primary" icon="mediaPlay" />
|
||||
) : isCurrentSong ? (
|
||||
<Icon fill="primary" icon="mediaPause" />
|
||||
) : (
|
||||
<Text
|
||||
className="current-song-child current-song-index"
|
||||
isMuted
|
||||
overflow="hidden"
|
||||
size="md"
|
||||
style={{ textAlign: 'right' }}
|
||||
>
|
||||
{value}
|
||||
</Text>
|
||||
)}
|
||||
</CellContainer>
|
||||
);
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
import type { ICellRendererParams } from '@ag-grid-community/core';
|
||||
|
||||
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';
|
||||
import { Skeleton } from '/@/shared/components/skeleton/skeleton';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
|
||||
export const TitleCell = ({ value }: ICellRendererParams) => {
|
||||
if (value === undefined) {
|
||||
return (
|
||||
<CellContainer position="left">
|
||||
<Skeleton height="1rem" width="80%" />
|
||||
</CellContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CellContainer position="left">
|
||||
<Text className="current-song-child" overflow="hidden" size="md">
|
||||
{value}
|
||||
</Text>
|
||||
</CellContainer>
|
||||
);
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { IHeaderParams } from '@ag-grid-community/core';
|
||||
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
|
||||
export interface ICustomHeaderParams extends IHeaderParams {
|
||||
menuIcon: string;
|
||||
}
|
||||
|
||||
export const DurationHeader = () => {
|
||||
return <Icon icon="duration" size="sm" />;
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
.header-wrapper {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.header-wrapper.right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.header-wrapper.center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.header-wrapper.left {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.header-text {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-weight: 500;
|
||||
line-height: inherit;
|
||||
color: var(--theme-colors-foreground);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.header-text.right {
|
||||
text-align: flex-end;
|
||||
}
|
||||
|
||||
.header-text.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header-text.left {
|
||||
text-align: flex-start;
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import type { IHeaderParams } from '@ag-grid-community/core';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import clsx from 'clsx';
|
||||
|
||||
import styles from './generic-table-header.module.css';
|
||||
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
|
||||
type Options = {
|
||||
children?: ReactNode;
|
||||
position?: 'center' | 'left' | 'right';
|
||||
preset?: Presets;
|
||||
};
|
||||
|
||||
type Presets = 'actions' | 'duration' | 'rowIndex' | 'userFavorite' | 'userRating';
|
||||
|
||||
const headerPresets = {
|
||||
actions: <Icon icon="ellipsisHorizontal" size="sm" />,
|
||||
duration: <Icon icon="duration" size="sm" />,
|
||||
rowIndex: <Icon icon="hash" size="sm" />,
|
||||
userFavorite: <Icon icon="favorite" size="sm" />,
|
||||
userRating: <Icon icon="star" size="sm" />,
|
||||
};
|
||||
|
||||
export const GenericTableHeader = (
|
||||
{ displayName }: IHeaderParams,
|
||||
{ children, position, preset }: Options,
|
||||
) => {
|
||||
if (preset) {
|
||||
return (
|
||||
<div className={clsx(styles.headerWrapper, styles[position ?? 'left'])}>
|
||||
{headerPresets[preset]}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clsx(styles.headerWrapper, styles[position ?? 'left'])}>
|
||||
<div className={clsx(styles.headerText, styles[position ?? 'left'])}>
|
||||
{children || displayName}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||
|
||||
import { useClickOutside } from '@mantine/hooks';
|
||||
import { MutableRefObject } from 'react';
|
||||
|
||||
export const useClickOutsideDeselect = (tableRef: MutableRefObject<AgGridReactType | null>) => {
|
||||
const handleDeselect = () => {
|
||||
if (tableRef.current) {
|
||||
tableRef.current.api.deselectAll();
|
||||
}
|
||||
};
|
||||
|
||||
const ref = useClickOutside(handleDeselect);
|
||||
|
||||
return ref;
|
||||
};
|
||||
@@ -1,100 +0,0 @@
|
||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||
|
||||
import { RowClassRules, RowNode } from '@ag-grid-community/core';
|
||||
import { MutableRefObject, useEffect, useMemo, useRef } from 'react';
|
||||
|
||||
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
|
||||
import { useAppFocus } from '/@/renderer/hooks';
|
||||
import { usePlayerSong } from '/@/renderer/store';
|
||||
import { Song } from '/@/shared/types/domain-types';
|
||||
|
||||
interface UseCurrentSongRowStylesProps {
|
||||
tableRef: MutableRefObject<AgGridReactType | null>;
|
||||
}
|
||||
|
||||
export const useCurrentSongRowStyles = ({ tableRef }: UseCurrentSongRowStylesProps) => {
|
||||
const currentSong = usePlayerSong();
|
||||
const isFocused = useAppFocus();
|
||||
const isFocusedRef = useRef<boolean>(isFocused);
|
||||
|
||||
useEffect(() => {
|
||||
// Redraw rows if the app focus changes
|
||||
if (isFocusedRef.current !== isFocused) {
|
||||
isFocusedRef.current = isFocused;
|
||||
if (tableRef?.current) {
|
||||
const { api, columnApi } = tableRef?.current || {};
|
||||
if (api == null || columnApi == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentNode = currentSong?.id ? api.getRowNode(currentSong.id) : undefined;
|
||||
|
||||
const rowNodes = [currentNode].filter((e) => e !== undefined) as RowNode<any>[];
|
||||
|
||||
if (rowNodes) {
|
||||
api.redrawRows({ rowNodes });
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [currentSong?.id, isFocused, tableRef]);
|
||||
|
||||
const rowClassRules = useMemo<RowClassRules<Song> | undefined>(() => {
|
||||
return {
|
||||
'current-song': (params) => {
|
||||
return (
|
||||
currentSong?.id !== undefined &&
|
||||
params?.data?.id === currentSong?.id &&
|
||||
params?.data?.albumId === currentSong?.albumId
|
||||
);
|
||||
},
|
||||
};
|
||||
}, [currentSong?.albumId, currentSong?.id]);
|
||||
|
||||
usePlayerEvents(
|
||||
{
|
||||
onCurrentSongChange: (properties, prev) => {
|
||||
const song = properties.song;
|
||||
const previousSong = prev.song;
|
||||
|
||||
if (tableRef?.current) {
|
||||
const { api, columnApi } = tableRef?.current || {};
|
||||
if (api == null || columnApi == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentNode = song?.id ? api.getRowNode(song.id) : undefined;
|
||||
|
||||
const previousNode = previousSong?.id
|
||||
? api.getRowNode(previousSong?.id)
|
||||
: undefined;
|
||||
|
||||
const rowNodes = [currentNode, previousNode].filter(
|
||||
(e) => e !== undefined,
|
||||
) as RowNode<any>[];
|
||||
|
||||
api.redrawRows({ rowNodes });
|
||||
}
|
||||
},
|
||||
onPlayerStatus: () => {
|
||||
const song = currentSong;
|
||||
|
||||
if (tableRef?.current) {
|
||||
const { api, columnApi } = tableRef?.current || {};
|
||||
if (api == null || columnApi == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentNode = song?.id ? api.getRowNode(song.id) : undefined;
|
||||
const rowNodes = [currentNode].filter((e) => e !== undefined) as RowNode<any>[];
|
||||
|
||||
api.redrawRows({ rowNodes });
|
||||
}
|
||||
},
|
||||
},
|
||||
[tableRef],
|
||||
);
|
||||
|
||||
return {
|
||||
rowClassRules,
|
||||
};
|
||||
};
|
||||
@@ -1,428 +0,0 @@
|
||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||
|
||||
import {
|
||||
BodyScrollEvent,
|
||||
ColDef,
|
||||
GetRowIdParams,
|
||||
GridReadyEvent,
|
||||
IDatasource,
|
||||
PaginationChangedEvent,
|
||||
RowDoubleClickedEvent,
|
||||
} from '@ag-grid-community/core';
|
||||
import { QueryKey, useQueryClient } from '@tanstack/react-query';
|
||||
import debounce from 'lodash/debounce';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import { MutableRefObject, useCallback, useMemo } from 'react';
|
||||
import { generatePath, useNavigate } from 'react-router';
|
||||
import { useSearchParams } from 'react-router';
|
||||
|
||||
import { api } from '/@/renderer/api';
|
||||
import { queryKeys, QueryPagination } from '/@/renderer/api/query-keys';
|
||||
import { getColumnDefs, VirtualTableProps } from '/@/renderer/components/virtual-table';
|
||||
import { SetContextMenuItems } from '/@/renderer/features/context-menu/events';
|
||||
import { useHandleTableContextMenu } from '/@/renderer/features/context-menu/hooks/use-handle-context-menu';
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { PersistedTableColumn, useListStoreActions } from '/@/renderer/store';
|
||||
import { ListKey, useListStoreByKey } from '/@/renderer/store/list.store';
|
||||
import {
|
||||
BasePaginatedResponse,
|
||||
BaseQuery,
|
||||
LibraryItem,
|
||||
ServerListItem,
|
||||
} from '/@/shared/types/domain-types';
|
||||
import { ListDisplayType, ListPagination } from '/@/shared/types/types';
|
||||
|
||||
export type AgGridFetchFn<TResponse, TFilter> = (
|
||||
args: { filter: TFilter; limit: number; startIndex: number },
|
||||
signal?: AbortSignal,
|
||||
) => Promise<TResponse>;
|
||||
|
||||
interface UseAgGridProps<TFilter> {
|
||||
columnType?: 'albumDetail' | 'generic';
|
||||
contextMenu: SetContextMenuItems;
|
||||
customFilters?: Partial<TFilter>;
|
||||
isClientSide?: boolean;
|
||||
isClientSideSort?: boolean;
|
||||
isSearchParams?: boolean;
|
||||
itemCount?: number;
|
||||
itemType: LibraryItem;
|
||||
pageKey: string;
|
||||
server: null | ServerListItem;
|
||||
tableRef: MutableRefObject<AgGridReactType | null>;
|
||||
}
|
||||
|
||||
const BLOCK_SIZE = 500;
|
||||
|
||||
export const useVirtualTable = <TFilter extends BaseQuery<any>>({
|
||||
columnType,
|
||||
contextMenu,
|
||||
customFilters,
|
||||
isClientSide,
|
||||
isClientSideSort,
|
||||
isSearchParams,
|
||||
itemCount,
|
||||
itemType,
|
||||
pageKey,
|
||||
server,
|
||||
tableRef,
|
||||
}: UseAgGridProps<TFilter>) => {
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
const { setTable, setTablePagination } = useListStoreActions();
|
||||
const properties = useListStoreByKey<TFilter>({ filter: customFilters, key: pageKey });
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const scrollOffset = searchParams.get('scrollOffset');
|
||||
const pagination = useMemo(() => {
|
||||
return {
|
||||
currentPage: Number(searchParams.get('currentPage')),
|
||||
itemsPerPage: Number(searchParams.get('itemsPerPage')),
|
||||
totalItems: Number(searchParams.get('totalItems')),
|
||||
totalPages: Number(searchParams.get('totalPages')),
|
||||
};
|
||||
}, [searchParams]);
|
||||
|
||||
const initialTableIndex =
|
||||
Number(isSearchParams ? scrollOffset : properties.table.scrollOffset) || 0;
|
||||
|
||||
const isPaginationEnabled = properties.display === ListDisplayType.TABLE_PAGINATED;
|
||||
|
||||
const columnDefs: ColDef[] = useMemo(() => {
|
||||
return getColumnDefs(properties.table.columns, true, columnType);
|
||||
}, [columnType, properties.table.columns]);
|
||||
|
||||
const defaultColumnDefs: ColDef = useMemo(() => {
|
||||
return {
|
||||
lockPinned: true,
|
||||
lockVisible: true,
|
||||
resizable: true,
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onGridSizeChange = () => {
|
||||
if (properties.table.autoFit) {
|
||||
tableRef?.current?.api.sizeColumnsToFit();
|
||||
}
|
||||
};
|
||||
|
||||
const queryKeyFn:
|
||||
| ((serverId: string, query: Record<any, any>, pagination: QueryPagination) => QueryKey)
|
||||
| null = useMemo(() => {
|
||||
switch (itemType) {
|
||||
case LibraryItem.ALBUM:
|
||||
return queryKeys.albums.list;
|
||||
case LibraryItem.ALBUM_ARTIST:
|
||||
return queryKeys.albumArtists.list;
|
||||
case LibraryItem.ARTIST:
|
||||
return queryKeys.artists.list;
|
||||
case LibraryItem.GENRE:
|
||||
return queryKeys.genres.list;
|
||||
case LibraryItem.PLAYLIST:
|
||||
return queryKeys.playlists.list;
|
||||
case LibraryItem.SONG:
|
||||
return queryKeys.songs.list;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}, [itemType]);
|
||||
|
||||
const queryFn: ((args: any) => Promise<BasePaginatedResponse<any> | null | undefined>) | null =
|
||||
useMemo(() => {
|
||||
switch (itemType) {
|
||||
case LibraryItem.ALBUM:
|
||||
return api.controller.getAlbumList;
|
||||
case LibraryItem.ALBUM_ARTIST:
|
||||
return api.controller.getAlbumArtistList;
|
||||
case LibraryItem.ARTIST:
|
||||
return api.controller.getArtistList;
|
||||
case LibraryItem.GENRE:
|
||||
return api.controller.getGenreList;
|
||||
case LibraryItem.PLAYLIST:
|
||||
return api.controller.getPlaylistList;
|
||||
case LibraryItem.SONG:
|
||||
return api.controller.getSongList;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}, [itemType]);
|
||||
|
||||
const onGridReady = useCallback(
|
||||
(params: GridReadyEvent) => {
|
||||
const dataSource: IDatasource = {
|
||||
getRows: async (params) => {
|
||||
const limit = params.endRow - params.startRow;
|
||||
const startIndex = params.startRow;
|
||||
|
||||
const queryKey = queryKeyFn!(
|
||||
server?.id || '',
|
||||
{
|
||||
...(properties.filter as any),
|
||||
},
|
||||
{
|
||||
limit,
|
||||
startIndex,
|
||||
},
|
||||
);
|
||||
|
||||
const results = (await queryClient.fetchQuery({
|
||||
queryFn: async ({ signal }) => {
|
||||
const res = await queryFn!({
|
||||
apiClientProps: {
|
||||
serverId: server?.id || '',
|
||||
signal,
|
||||
},
|
||||
query: {
|
||||
...properties.filter,
|
||||
limit,
|
||||
startIndex,
|
||||
},
|
||||
});
|
||||
|
||||
return res;
|
||||
},
|
||||
queryKey,
|
||||
})) as BasePaginatedResponse<any>;
|
||||
|
||||
if (isClientSideSort && results?.items) {
|
||||
const sortedResults = orderBy(
|
||||
results.items,
|
||||
[(item) => String(item[properties.filter.sortBy]).toLowerCase()],
|
||||
properties.filter.sortOrder === 'DESC' ? ['desc'] : ['asc'],
|
||||
);
|
||||
|
||||
params.successCallback(sortedResults || [], results?.totalRecordCount || 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (results.totalRecordCount === null) {
|
||||
const hasMoreRows = results?.items?.length === BLOCK_SIZE;
|
||||
const lastRowIndex = hasMoreRows
|
||||
? undefined
|
||||
: params.startRow + results.items.length;
|
||||
|
||||
params.successCallback(
|
||||
results?.items || [],
|
||||
hasMoreRows ? undefined : lastRowIndex,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
params.successCallback(results?.items || [], results?.totalRecordCount || 0);
|
||||
},
|
||||
rowCount: undefined,
|
||||
};
|
||||
|
||||
params.api.setDatasource(dataSource);
|
||||
params.api.ensureIndexVisible(initialTableIndex, 'top');
|
||||
},
|
||||
[
|
||||
initialTableIndex,
|
||||
queryKeyFn,
|
||||
server,
|
||||
properties.filter,
|
||||
queryClient,
|
||||
isClientSideSort,
|
||||
queryFn,
|
||||
],
|
||||
);
|
||||
|
||||
const setParamsTablePagination = useCallback(
|
||||
(args: { data: Partial<ListPagination>; key: ListKey }) => {
|
||||
const { data } = args;
|
||||
|
||||
setSearchParams(
|
||||
(params) => {
|
||||
if (data.currentPage) params.set('currentPage', String(data.currentPage));
|
||||
if (data.itemsPerPage) params.set('itemsPerPage', String(data.itemsPerPage));
|
||||
if (data.totalItems) params.set('totalItems', String(data.totalItems));
|
||||
if (data.totalPages) params.set('totalPages', String(data.totalPages));
|
||||
return params;
|
||||
},
|
||||
{ replace: true },
|
||||
);
|
||||
},
|
||||
[setSearchParams],
|
||||
);
|
||||
|
||||
const onPaginationChanged = useCallback(
|
||||
(event: PaginationChangedEvent) => {
|
||||
if (!isPaginationEnabled || !event.api) return;
|
||||
|
||||
try {
|
||||
// Scroll to top of page on pagination change
|
||||
const currentPageStartIndex =
|
||||
properties.table.pagination.currentPage *
|
||||
properties.table.pagination.itemsPerPage;
|
||||
event.api?.ensureIndexVisible(currentPageStartIndex, 'top');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
if (isSearchParams) {
|
||||
setSearchParams(
|
||||
(params) => {
|
||||
params.set('currentPage', String(event.api.paginationGetCurrentPage()));
|
||||
params.set('itemsPerPage', String(event.api.paginationGetPageSize()));
|
||||
params.set('totalItems', String(event.api.paginationGetRowCount()));
|
||||
params.set('totalPages', String(event.api.paginationGetTotalPages() + 1));
|
||||
return params;
|
||||
},
|
||||
{ replace: true },
|
||||
);
|
||||
} else {
|
||||
setTablePagination({
|
||||
data: {
|
||||
itemsPerPage: event.api.paginationGetPageSize(),
|
||||
totalItems: event.api.paginationGetRowCount(),
|
||||
totalPages: event.api.paginationGetTotalPages() + 1,
|
||||
},
|
||||
key: pageKey,
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
isPaginationEnabled,
|
||||
isSearchParams,
|
||||
properties.table.pagination.currentPage,
|
||||
properties.table.pagination.itemsPerPage,
|
||||
setSearchParams,
|
||||
setTablePagination,
|
||||
pageKey,
|
||||
],
|
||||
);
|
||||
|
||||
const onColumnMoved = useCallback(() => {
|
||||
const { columnApi } = tableRef?.current || {};
|
||||
const columnsOrder = columnApi?.getAllGridColumns();
|
||||
|
||||
if (!columnsOrder) return;
|
||||
|
||||
const columnsInSettings = properties.table.columns;
|
||||
const updatedColumns: PersistedTableColumn[] = [];
|
||||
for (const column of columnsOrder) {
|
||||
const columnInSettings = columnsInSettings.find(
|
||||
(c) => c.column === column.getColDef().colId,
|
||||
);
|
||||
|
||||
if (columnInSettings) {
|
||||
updatedColumns.push({
|
||||
...columnInSettings,
|
||||
...(!properties.table.autoFit && {
|
||||
width: column.getActualWidth(),
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setTable({ data: { columns: updatedColumns }, key: pageKey });
|
||||
}, [pageKey, properties.table.autoFit, properties.table.columns, setTable, tableRef]);
|
||||
|
||||
const onColumnResized = debounce(onColumnMoved, 200);
|
||||
|
||||
const onBodyScrollEnd = (e: BodyScrollEvent) => {
|
||||
const scrollOffset = Number((e.top / properties.table.rowHeight).toFixed(0));
|
||||
|
||||
if (isSearchParams) {
|
||||
setSearchParams(
|
||||
(params) => {
|
||||
params.set('scrollOffset', String(scrollOffset));
|
||||
return params;
|
||||
},
|
||||
{ replace: true },
|
||||
);
|
||||
} else {
|
||||
setTable({ data: { scrollOffset }, key: pageKey });
|
||||
}
|
||||
};
|
||||
|
||||
const onCellContextMenu = useHandleTableContextMenu(itemType, contextMenu);
|
||||
|
||||
const context = {
|
||||
itemType,
|
||||
onCellContextMenu,
|
||||
};
|
||||
|
||||
const defaultTableProps: Partial<VirtualTableProps> = useMemo(() => {
|
||||
return {
|
||||
alwaysShowHorizontalScroll: true,
|
||||
autoFitColumns: properties.table.autoFit,
|
||||
blockLoadDebounceMillis: 200,
|
||||
cacheBlockSize: BLOCK_SIZE,
|
||||
getRowId: (data: GetRowIdParams<any>) => data.data.id,
|
||||
infiniteInitialRowCount: itemCount || 100,
|
||||
pagination: isPaginationEnabled,
|
||||
paginationAutoPageSize: isPaginationEnabled,
|
||||
paginationPageSize: properties.table.pagination.itemsPerPage || 100,
|
||||
paginationProps: isPaginationEnabled
|
||||
? {
|
||||
pageKey,
|
||||
pagination: isSearchParams ? pagination : properties.table.pagination,
|
||||
setPagination: isSearchParams ? setParamsTablePagination : setTablePagination,
|
||||
}
|
||||
: undefined,
|
||||
rowBuffer: 20,
|
||||
rowHeight: properties.table.rowHeight || 40,
|
||||
rowModelType: isClientSide ? 'clientSide' : 'infinite',
|
||||
suppressRowDrag: true,
|
||||
};
|
||||
}, [
|
||||
isClientSide,
|
||||
isPaginationEnabled,
|
||||
isSearchParams,
|
||||
itemCount,
|
||||
pageKey,
|
||||
pagination,
|
||||
properties.table.autoFit,
|
||||
properties.table.pagination,
|
||||
properties.table.rowHeight,
|
||||
setParamsTablePagination,
|
||||
setTablePagination,
|
||||
]);
|
||||
|
||||
const onRowDoubleClicked = useCallback(
|
||||
(e: RowDoubleClickedEvent) => {
|
||||
switch (itemType) {
|
||||
case LibraryItem.ALBUM:
|
||||
navigate(generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { albumId: e.data.id }));
|
||||
break;
|
||||
case LibraryItem.ALBUM_ARTIST:
|
||||
navigate(
|
||||
generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
|
||||
albumArtistId: e.data.id,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case LibraryItem.ARTIST:
|
||||
navigate(
|
||||
generatePath(AppRoute.LIBRARY_ARTISTS_DETAIL, {
|
||||
artistId: e.data.id,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case LibraryItem.PLAYLIST:
|
||||
navigate(
|
||||
generatePath(AppRoute.PLAYLISTS_DETAIL_SONGS, { playlistId: e.data.id }),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
[itemType, navigate],
|
||||
);
|
||||
|
||||
return {
|
||||
columnDefs,
|
||||
context,
|
||||
defaultColumnDefs,
|
||||
onBodyScrollEnd,
|
||||
onCellContextMenu,
|
||||
onColumnMoved,
|
||||
onColumnResized,
|
||||
onGridReady,
|
||||
onGridSizeChange,
|
||||
onPaginationChanged,
|
||||
onRowDoubleClicked,
|
||||
...defaultTableProps,
|
||||
};
|
||||
};
|
||||
@@ -1,645 +0,0 @@
|
||||
import type {
|
||||
ColDef,
|
||||
ColumnMovedEvent,
|
||||
GridReadyEvent,
|
||||
GridSizeChangedEvent,
|
||||
ICellRendererParams,
|
||||
IHeaderParams,
|
||||
ModelUpdatedEvent,
|
||||
NewColumnsLoadedEvent,
|
||||
ValueFormatterParams,
|
||||
ValueGetterParams,
|
||||
} from '@ag-grid-community/core';
|
||||
import type { AgGridReactProps } from '@ag-grid-community/react';
|
||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||
|
||||
import { AgGridReact } from '@ag-grid-community/react';
|
||||
import { useClickOutside, useMergedRef } from '@mantine/hooks';
|
||||
import clsx from 'clsx';
|
||||
import formatDuration from 'format-duration';
|
||||
import { AnimatePresence } from 'motion/react';
|
||||
import { forwardRef, Ref, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { generatePath } from 'react-router';
|
||||
|
||||
import styles from './virtual-table.module.css';
|
||||
|
||||
import i18n from '/@/i18n/i18n';
|
||||
import { ActionsCell } from '/@/renderer/components/virtual-table/cells/actions-cell';
|
||||
import { AlbumArtistCell } from '/@/renderer/components/virtual-table/cells/album-artist-cell';
|
||||
import { ArtistCell } from '/@/renderer/components/virtual-table/cells/artist-cell';
|
||||
import { CombinedTitleCell } from '/@/renderer/components/virtual-table/cells/combined-title-cell';
|
||||
import { FavoriteCell } from '/@/renderer/components/virtual-table/cells/favorite-cell';
|
||||
import { GenericCell } from '/@/renderer/components/virtual-table/cells/generic-cell';
|
||||
import { GenreCell } from '/@/renderer/components/virtual-table/cells/genre-cell';
|
||||
import { NoteCell } from '/@/renderer/components/virtual-table/cells/note-cell';
|
||||
import { RatingCell } from '/@/renderer/components/virtual-table/cells/rating-cell';
|
||||
import { RowIndexCell } from '/@/renderer/components/virtual-table/cells/row-index-cell';
|
||||
import { TitleCell } from '/@/renderer/components/virtual-table/cells/title-cell';
|
||||
import { GenericTableHeader } from '/@/renderer/components/virtual-table/headers/generic-table-header';
|
||||
import { useFixedTableHeader } from '/@/renderer/components/virtual-table/hooks/use-fixed-table-header';
|
||||
import { TablePagination } from '/@/renderer/components/virtual-table/table-pagination';
|
||||
import { useTableChange } from '/@/renderer/hooks/use-song-change';
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { PersistedTableColumn } from '/@/renderer/store/settings.store';
|
||||
import {
|
||||
formatDateAbsolute,
|
||||
formatDateAbsoluteUTC,
|
||||
formatDateRelative,
|
||||
formatSizeString,
|
||||
} from '/@/renderer/utils/format';
|
||||
import {
|
||||
PlayerStatus,
|
||||
TableColumn,
|
||||
ListPagination as TablePaginationType,
|
||||
} from '/@/shared/types/types';
|
||||
|
||||
export * from './hooks/use-click-outside-deselect';
|
||||
export * from './hooks/use-fixed-table-header';
|
||||
export * from './table-config-dropdown';
|
||||
export * from './table-pagination';
|
||||
export * from './utils';
|
||||
|
||||
// const TableWrapper = styled.div`
|
||||
// position: relative;
|
||||
// display: flex;
|
||||
// flex-direction: column;
|
||||
// width: 100%;
|
||||
// height: 100%;
|
||||
// `;
|
||||
|
||||
// const DummyHeader = styled.div<{ height?: number }>`
|
||||
// position: absolute;
|
||||
// height: ${({ height }) => height || 36}px;
|
||||
// `;
|
||||
|
||||
const tableColumns: { [key: string]: ColDef } = {
|
||||
actions: {
|
||||
cellClass: 'ag-cell-favorite',
|
||||
cellRenderer: (params: ICellRendererParams) => ActionsCell(params),
|
||||
colId: TableColumn.ACTIONS,
|
||||
headerComponent: () => <></>,
|
||||
suppressSizeToFit: true,
|
||||
width: 25,
|
||||
},
|
||||
album: {
|
||||
cellRenderer: (params: ICellRendererParams) =>
|
||||
GenericCell(params, { isLink: true, position: 'left' }),
|
||||
colId: TableColumn.ALBUM,
|
||||
headerName: i18n.t('table.column.album'),
|
||||
valueGetter: (params: ValueGetterParams) =>
|
||||
params.data
|
||||
? {
|
||||
link: generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, {
|
||||
albumId: params.data?.albumId || '',
|
||||
}),
|
||||
value: params.data?.album,
|
||||
}
|
||||
: undefined,
|
||||
width: 200,
|
||||
},
|
||||
albumArtist: {
|
||||
cellRenderer: AlbumArtistCell,
|
||||
colId: TableColumn.ALBUM_ARTIST,
|
||||
headerName: i18n.t('table.column.albumArtist'),
|
||||
valueGetter: (params: ValueGetterParams) =>
|
||||
params.data ? params.data.albumArtists : undefined,
|
||||
width: 150,
|
||||
},
|
||||
albumCount: {
|
||||
cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'center' }),
|
||||
colId: TableColumn.ALBUM_COUNT,
|
||||
field: 'albumCount',
|
||||
headerComponent: (params: IHeaderParams) =>
|
||||
GenericTableHeader(params, { position: 'center' }),
|
||||
headerName: i18n.t('table.column.albumCount'),
|
||||
suppressSizeToFit: true,
|
||||
valueGetter: (params: ValueGetterParams) =>
|
||||
params.data ? params.data.albumCount : undefined,
|
||||
width: 80,
|
||||
},
|
||||
artist: {
|
||||
cellRenderer: ArtistCell,
|
||||
colId: TableColumn.ARTIST,
|
||||
headerName: i18n.t('table.column.artist'),
|
||||
valueGetter: (params: ValueGetterParams) => (params.data ? params.data.artists : undefined),
|
||||
width: 150,
|
||||
},
|
||||
biography: {
|
||||
cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'left' }),
|
||||
colId: TableColumn.BIOGRAPHY,
|
||||
field: 'biography',
|
||||
headerName: i18n.t('table.column.biography'),
|
||||
valueGetter: (params: ValueGetterParams) => (params.data ? params.data.biography : ''),
|
||||
width: 200,
|
||||
},
|
||||
bitRate: {
|
||||
cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'center' }),
|
||||
colId: TableColumn.BIT_RATE,
|
||||
field: 'bitRate',
|
||||
headerComponent: (params: IHeaderParams) =>
|
||||
GenericTableHeader(params, { position: 'center' }),
|
||||
headerName: i18n.t('table.column.bitrate'),
|
||||
suppressSizeToFit: true,
|
||||
valueFormatter: (params: ValueFormatterParams) => `${params.value} kbps`,
|
||||
valueGetter: (params: ValueGetterParams) => (params.data ? params.data.bitRate : undefined),
|
||||
width: 90,
|
||||
},
|
||||
bpm: {
|
||||
cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'center' }),
|
||||
colId: TableColumn.BPM,
|
||||
headerComponent: (params: IHeaderParams) =>
|
||||
GenericTableHeader(params, { position: 'center' }),
|
||||
headerName: i18n.t('table.column.bpm'),
|
||||
suppressSizeToFit: true,
|
||||
valueGetter: (params: ValueGetterParams) => (params.data ? params.data.bpm : undefined),
|
||||
width: 60,
|
||||
},
|
||||
channels: {
|
||||
cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'center' }),
|
||||
colId: TableColumn.CHANNELS,
|
||||
field: 'channels',
|
||||
headerComponent: (params: IHeaderParams) =>
|
||||
GenericTableHeader(params, { position: 'center' }),
|
||||
headerName: i18n.t('table.column.channels'),
|
||||
valueGetter: (params: ValueGetterParams) =>
|
||||
params.data ? params.data.channels : undefined,
|
||||
width: 100,
|
||||
},
|
||||
codec: {
|
||||
cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'center' }),
|
||||
colId: TableColumn.CODEC,
|
||||
headerName: i18n.t('table.column.codec'),
|
||||
valueGetter: (params: ValueGetterParams) =>
|
||||
params.data ? params.data.container : undefined,
|
||||
width: 60,
|
||||
},
|
||||
comment: {
|
||||
cellRenderer: NoteCell,
|
||||
colId: TableColumn.COMMENT,
|
||||
headerName: i18n.t('table.column.comment'),
|
||||
valueGetter: (params: ValueGetterParams) => (params.data ? params.data.comment : undefined),
|
||||
width: 150,
|
||||
},
|
||||
dateAdded: {
|
||||
cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'center' }),
|
||||
colId: TableColumn.DATE_ADDED,
|
||||
field: 'createdAt',
|
||||
headerComponent: (params: IHeaderParams) =>
|
||||
GenericTableHeader(params, { position: 'center' }),
|
||||
headerName: i18n.t('table.column.dateAdded'),
|
||||
suppressSizeToFit: true,
|
||||
valueFormatter: (params: ValueFormatterParams) => formatDateAbsolute(params.value),
|
||||
valueGetter: (params: ValueGetterParams) =>
|
||||
params.data ? params.data.createdAt : undefined,
|
||||
width: 130,
|
||||
},
|
||||
discNumber: {
|
||||
cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'center' }),
|
||||
colId: TableColumn.DISC_NUMBER,
|
||||
field: 'discNumber',
|
||||
headerComponent: (params: IHeaderParams) =>
|
||||
GenericTableHeader(params, { position: 'center' }),
|
||||
headerName: i18n.t('table.column.discNumber'),
|
||||
suppressSizeToFit: true,
|
||||
valueGetter: (params: ValueGetterParams) =>
|
||||
params.data ? params.data.discNumber : undefined,
|
||||
width: 60,
|
||||
},
|
||||
duration: {
|
||||
cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'center' }),
|
||||
colId: TableColumn.DURATION,
|
||||
field: 'duration',
|
||||
headerComponent: (params: IHeaderParams) =>
|
||||
GenericTableHeader(params, { position: 'center', preset: 'duration' }),
|
||||
suppressSizeToFit: true,
|
||||
valueFormatter: (params: ValueFormatterParams) => formatDuration(Number(params.value)),
|
||||
valueGetter: (params: ValueGetterParams) =>
|
||||
params.data ? params.data.duration : undefined,
|
||||
width: 70,
|
||||
},
|
||||
genre: {
|
||||
cellRenderer: GenreCell,
|
||||
colId: TableColumn.GENRE,
|
||||
headerName: i18n.t('table.column.genre'),
|
||||
valueGetter: (params: ValueGetterParams) => (params.data ? params.data.genres : undefined),
|
||||
width: 100,
|
||||
},
|
||||
lastPlayedAt: {
|
||||
cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'center' }),
|
||||
colId: TableColumn.LAST_PLAYED,
|
||||
headerComponent: (params: IHeaderParams) =>
|
||||
GenericTableHeader(params, { position: 'center' }),
|
||||
headerName: i18n.t('table.column.lastPlayed'),
|
||||
valueFormatter: (params: ValueFormatterParams) => formatDateRelative(params.value),
|
||||
valueGetter: (params: ValueGetterParams) =>
|
||||
params.data ? params.data.lastPlayedAt : undefined,
|
||||
width: 130,
|
||||
},
|
||||
path: {
|
||||
cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'left' }),
|
||||
colId: TableColumn.PATH,
|
||||
headerName: i18n.t('table.column.path'),
|
||||
valueGetter: (params: ValueGetterParams) => (params.data ? params.data.path : undefined),
|
||||
width: 200,
|
||||
},
|
||||
playCount: {
|
||||
cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'center' }),
|
||||
colId: TableColumn.PLAY_COUNT,
|
||||
field: 'playCount',
|
||||
headerComponent: (params: IHeaderParams) =>
|
||||
GenericTableHeader(params, { position: 'center' }),
|
||||
headerName: i18n.t('table.column.playCount'),
|
||||
suppressSizeToFit: true,
|
||||
valueGetter: (params: ValueGetterParams) =>
|
||||
params.data ? params.data.playCount : undefined,
|
||||
width: 90,
|
||||
},
|
||||
releaseDate: {
|
||||
cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'center' }),
|
||||
colId: TableColumn.RELEASE_DATE,
|
||||
field: 'releaseDate',
|
||||
headerComponent: (params: IHeaderParams) =>
|
||||
GenericTableHeader(params, { position: 'center' }),
|
||||
headerName: i18n.t('table.column.releaseDate'),
|
||||
suppressSizeToFit: true,
|
||||
valueFormatter: (params: ValueFormatterParams) => formatDateAbsoluteUTC(params.value),
|
||||
valueGetter: (params: ValueGetterParams) =>
|
||||
params.data ? params.data.releaseDate : undefined,
|
||||
width: 130,
|
||||
},
|
||||
releaseYear: {
|
||||
cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'center' }),
|
||||
colId: TableColumn.YEAR,
|
||||
field: 'releaseYear',
|
||||
headerComponent: (params: IHeaderParams) =>
|
||||
GenericTableHeader(params, { position: 'center' }),
|
||||
headerName: i18n.t('table.column.releaseYear'),
|
||||
suppressSizeToFit: true,
|
||||
valueGetter: (params: ValueGetterParams) =>
|
||||
params.data ? params.data.releaseYear : undefined,
|
||||
width: 80,
|
||||
},
|
||||
rowIndex: {
|
||||
cellClass: 'row-index',
|
||||
cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'right' }),
|
||||
colId: TableColumn.ROW_INDEX,
|
||||
headerComponent: (params: IHeaderParams) =>
|
||||
GenericTableHeader(params, { position: 'right', preset: 'rowIndex' }),
|
||||
suppressSizeToFit: true,
|
||||
valueGetter: (params) => {
|
||||
return (params.node?.rowIndex || 0) + 1;
|
||||
},
|
||||
width: 65,
|
||||
},
|
||||
rowIndexGeneric: {
|
||||
cellClass: 'row-index',
|
||||
cellClassRules: {
|
||||
'current-playlist-song-cell': (params) => {
|
||||
return (
|
||||
params.context?.currentSong?.uniqueId !== undefined &&
|
||||
params.data?.uniqueId === params.context?.currentSong?.uniqueId
|
||||
);
|
||||
},
|
||||
'current-song-cell': (params) => {
|
||||
return (
|
||||
params.context?.currentSong?.id !== undefined &&
|
||||
params.data?.id === params.context?.currentSong?.id &&
|
||||
params.data?.albumId === params.context?.currentSong?.albumId
|
||||
);
|
||||
},
|
||||
focused: (params) => {
|
||||
return params.context?.isFocused;
|
||||
},
|
||||
playing: (params) => {
|
||||
return params.context?.status === PlayerStatus.PLAYING;
|
||||
},
|
||||
},
|
||||
cellRenderer: RowIndexCell,
|
||||
colId: TableColumn.ROW_INDEX,
|
||||
headerComponent: (params: IHeaderParams) =>
|
||||
GenericTableHeader(params, { position: 'right', preset: 'rowIndex' }),
|
||||
suppressSizeToFit: true,
|
||||
valueGetter: (params) => {
|
||||
return (params.node?.rowIndex || 0) + 1;
|
||||
},
|
||||
width: 65,
|
||||
},
|
||||
size: {
|
||||
cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'center' }),
|
||||
colId: TableColumn.SIZE,
|
||||
headerComponent: (params: IHeaderParams) =>
|
||||
GenericTableHeader(params, { position: 'center' }),
|
||||
headerName: i18n.t('table.column.size'),
|
||||
valueGetter: (params: ValueGetterParams) =>
|
||||
params.data ? formatSizeString(params.data.size) : undefined,
|
||||
width: 80,
|
||||
},
|
||||
songCount: {
|
||||
cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'center' }),
|
||||
colId: TableColumn.SONG_COUNT,
|
||||
field: 'songCount',
|
||||
headerComponent: (params: IHeaderParams) =>
|
||||
GenericTableHeader(params, { position: 'center' }),
|
||||
headerName: i18n.t('table.column.songCount'),
|
||||
suppressSizeToFit: true,
|
||||
valueGetter: (params: ValueGetterParams) =>
|
||||
params.data ? params.data.songCount : undefined,
|
||||
width: 80,
|
||||
},
|
||||
title: {
|
||||
cellRenderer: TitleCell,
|
||||
colId: TableColumn.TITLE,
|
||||
field: 'name',
|
||||
headerName: i18n.t('table.column.title'),
|
||||
valueGetter: (params: ValueGetterParams) => (params.data ? params.data.name : undefined),
|
||||
width: 250,
|
||||
},
|
||||
titleCombined: {
|
||||
cellRenderer: CombinedTitleCell,
|
||||
colId: TableColumn.TITLE_COMBINED,
|
||||
headerName: i18n.t('table.column.title'),
|
||||
initialWidth: 500,
|
||||
minWidth: 150,
|
||||
valueGetter: (params: ValueGetterParams) =>
|
||||
params.data
|
||||
? {
|
||||
albumArtists: params.data?.albumArtists,
|
||||
artists: params.data?.artists,
|
||||
id: params.data?.id,
|
||||
imagePlaceholderUrl: params.data?.imagePlaceholderUrl,
|
||||
imageUrl: params.data?.imageUrl,
|
||||
name: params.data?.name,
|
||||
rowHeight: params.node?.rowHeight,
|
||||
type: params.data?.serverType,
|
||||
}
|
||||
: undefined,
|
||||
width: 250,
|
||||
},
|
||||
trackNumber: {
|
||||
cellClass: 'track-number',
|
||||
cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'right' }),
|
||||
colId: TableColumn.TRACK_NUMBER,
|
||||
field: 'trackNumber',
|
||||
headerComponent: (params: IHeaderParams) =>
|
||||
GenericTableHeader(params, { position: 'center' }),
|
||||
headerName: i18n.t('table.column.trackNumber'),
|
||||
suppressSizeToFit: true,
|
||||
valueGetter: (params: ValueGetterParams) =>
|
||||
params.data ? params.data.trackNumber : undefined,
|
||||
width: 80,
|
||||
},
|
||||
trackNumberDetail: {
|
||||
cellClass: 'row-index',
|
||||
cellClassRules: {
|
||||
'current-song-cell': (params) => {
|
||||
return (
|
||||
params.context?.currentSong?.id !== undefined &&
|
||||
params.data?.id === params.context?.currentSong?.id &&
|
||||
params.data?.albumId === params.context?.currentSong?.albumId
|
||||
);
|
||||
},
|
||||
focused: (params) => {
|
||||
return params.context?.isFocused;
|
||||
},
|
||||
playing: (params) => {
|
||||
return params.context?.status === PlayerStatus.PLAYING;
|
||||
},
|
||||
},
|
||||
cellRenderer: RowIndexCell,
|
||||
colId: TableColumn.TRACK_NUMBER,
|
||||
field: 'trackNumber',
|
||||
headerComponent: (params: IHeaderParams) =>
|
||||
GenericTableHeader(params, { position: 'center' }),
|
||||
headerName: i18n.t('table.column.trackNumber'),
|
||||
suppressSizeToFit: true,
|
||||
valueGetter: (params: ValueGetterParams) =>
|
||||
params.data ? params.data.trackNumber : undefined,
|
||||
width: 80,
|
||||
},
|
||||
userFavorite: {
|
||||
cellClass: (params) => (params.value ? 'visible ag-cell-favorite' : 'ag-cell-favorite'),
|
||||
cellRenderer: FavoriteCell,
|
||||
colId: TableColumn.USER_FAVORITE,
|
||||
field: 'userFavorite',
|
||||
headerComponent: (params: IHeaderParams) =>
|
||||
GenericTableHeader(params, { position: 'center', preset: 'userFavorite' }),
|
||||
headerName: i18n.t('table.column.favorite'),
|
||||
suppressSizeToFit: true,
|
||||
valueGetter: (params: ValueGetterParams) =>
|
||||
params.data ? params.data.userFavorite : undefined,
|
||||
width: 50,
|
||||
},
|
||||
userRating: {
|
||||
cellClass: (params) =>
|
||||
params.value?.userRating ? 'visible ag-cell-rating' : 'ag-cell-rating',
|
||||
cellRenderer: RatingCell,
|
||||
colId: TableColumn.USER_RATING,
|
||||
field: 'userRating',
|
||||
headerComponent: (params: IHeaderParams) =>
|
||||
GenericTableHeader(params, { position: 'center', preset: 'userRating' }),
|
||||
headerName: i18n.t('table.column.rating'),
|
||||
suppressSizeToFit: true,
|
||||
valueGetter: (params: ValueGetterParams) => (params.data ? params.data : undefined),
|
||||
width: 95,
|
||||
},
|
||||
};
|
||||
|
||||
export const getColumnDef = (column: TableColumn) => {
|
||||
return tableColumns[column as keyof typeof tableColumns];
|
||||
};
|
||||
|
||||
export const getColumnDefs = (
|
||||
columns: PersistedTableColumn[],
|
||||
useWidth?: boolean,
|
||||
type?: 'albumDetail' | 'generic',
|
||||
) => {
|
||||
const columnDefs: ColDef[] = [];
|
||||
for (const column of columns) {
|
||||
let presetColumn = tableColumns[column.column as keyof typeof tableColumns];
|
||||
|
||||
if (column.column === TableColumn.TRACK_NUMBER && type === 'albumDetail') {
|
||||
presetColumn = tableColumns['trackNumberDetail' as keyof typeof tableColumns];
|
||||
}
|
||||
|
||||
if (column.column === TableColumn.ROW_INDEX && type === 'generic') {
|
||||
presetColumn = tableColumns['rowIndexGeneric' as keyof typeof tableColumns];
|
||||
}
|
||||
|
||||
if (presetColumn) {
|
||||
columnDefs.push({
|
||||
...presetColumn,
|
||||
[useWidth ? 'width' : 'initialWidth']: column.width,
|
||||
...column.extraProps,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return columnDefs;
|
||||
};
|
||||
|
||||
export interface VirtualTableProps extends AgGridReactProps {
|
||||
autoFitColumns?: boolean;
|
||||
autoHeight?: boolean;
|
||||
deselectOnClickOutside?: boolean;
|
||||
paginationProps?: {
|
||||
pageKey: string;
|
||||
pagination: TablePaginationType;
|
||||
setPagination: any;
|
||||
};
|
||||
shouldUpdateSong?: boolean;
|
||||
stickyHeader?: boolean;
|
||||
transparentHeader?: boolean;
|
||||
}
|
||||
|
||||
export const VirtualTable = forwardRef(
|
||||
(
|
||||
{
|
||||
autoFitColumns,
|
||||
autoHeight,
|
||||
deselectOnClickOutside,
|
||||
onColumnMoved,
|
||||
onGridReady,
|
||||
onGridSizeChanged,
|
||||
onNewColumnsLoaded,
|
||||
paginationProps,
|
||||
shouldUpdateSong,
|
||||
stickyHeader,
|
||||
transparentHeader,
|
||||
...rest
|
||||
}: VirtualTableProps,
|
||||
ref: Ref<AgGridReactType | null>,
|
||||
) => {
|
||||
const tableRef = useRef<AgGridReactType | null>(null);
|
||||
|
||||
const mergedRef = useMergedRef(ref, tableRef);
|
||||
|
||||
const deselectRef = useClickOutside(() => {
|
||||
if (tableRef?.current?.api && deselectOnClickOutside) {
|
||||
tableRef?.current?.api?.deselectAll();
|
||||
}
|
||||
});
|
||||
|
||||
useTableChange(tableRef, shouldUpdateSong === true);
|
||||
|
||||
const defaultColumnDefs: ColDef = useMemo(() => {
|
||||
return {
|
||||
lockPinned: true,
|
||||
lockVisible: true,
|
||||
resizable: true,
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Auto fit columns on column change
|
||||
useEffect(() => {
|
||||
if (!tableRef?.current?.api) return;
|
||||
if (autoFitColumns && tableRef?.current?.api) {
|
||||
tableRef?.current?.api?.sizeColumnsToFit?.();
|
||||
}
|
||||
}, [autoFitColumns]);
|
||||
|
||||
// Reset row heights on row height change
|
||||
useEffect(() => {
|
||||
if (!tableRef?.current?.api) return;
|
||||
tableRef?.current?.api?.resetRowHeights();
|
||||
tableRef?.current?.api?.redrawRows();
|
||||
}, [rest.rowHeight]);
|
||||
|
||||
const handleColumnMoved = useCallback(
|
||||
(e: ColumnMovedEvent) => {
|
||||
if (!e?.api) return;
|
||||
onColumnMoved?.(e);
|
||||
if (autoFitColumns) e.api?.sizeColumnsToFit?.();
|
||||
},
|
||||
[autoFitColumns, onColumnMoved],
|
||||
);
|
||||
|
||||
const handleNewColumnsLoaded = useCallback(
|
||||
(e: NewColumnsLoadedEvent) => {
|
||||
if (!e?.api) return;
|
||||
onNewColumnsLoaded?.(e);
|
||||
if (autoFitColumns) e.api?.sizeColumnsToFit?.();
|
||||
},
|
||||
[autoFitColumns, onNewColumnsLoaded],
|
||||
);
|
||||
|
||||
const handleGridReady = useCallback(
|
||||
(e: GridReadyEvent) => {
|
||||
if (!e?.api) return;
|
||||
onGridReady?.(e);
|
||||
if (autoHeight) e.api.setDomLayout('autoHeight');
|
||||
},
|
||||
[autoHeight, onGridReady],
|
||||
);
|
||||
|
||||
const handleGridSizeChanged = useCallback(
|
||||
(e: GridSizeChangedEvent) => {
|
||||
if (!e?.api) return;
|
||||
onGridSizeChanged?.(e);
|
||||
if (autoFitColumns) e.api?.sizeColumnsToFit?.();
|
||||
},
|
||||
[autoFitColumns, onGridSizeChanged],
|
||||
);
|
||||
|
||||
const handleModelUpdated = useCallback(
|
||||
(e: ModelUpdatedEvent) => {
|
||||
if (!e?.api) return;
|
||||
if (autoFitColumns) e.api?.sizeColumnsToFit?.();
|
||||
},
|
||||
[autoFitColumns],
|
||||
);
|
||||
|
||||
const { tableContainerRef, tableHeaderRef } = useFixedTableHeader({
|
||||
enabled: stickyHeader || false,
|
||||
});
|
||||
|
||||
const mergedWrapperRef = useMergedRef(deselectRef, tableContainerRef);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
styles.tableWrapper,
|
||||
transparentHeader
|
||||
? 'ag-theme-alpine-dark ag-header-transparent'
|
||||
: 'ag-theme-alpine-dark',
|
||||
)}
|
||||
ref={mergedWrapperRef}
|
||||
>
|
||||
<div
|
||||
className={styles.dummyHeader}
|
||||
ref={tableHeaderRef}
|
||||
style={{ height: rest.headerHeight ?? 36 }}
|
||||
/>
|
||||
<AgGridReact
|
||||
animateRows
|
||||
blockLoadDebounceMillis={200}
|
||||
cacheBlockSize={300}
|
||||
cacheOverflowSize={1}
|
||||
defaultColDef={defaultColumnDefs}
|
||||
enableCellChangeFlash={false}
|
||||
headerHeight={36}
|
||||
maintainColumnOrder
|
||||
ref={mergedRef}
|
||||
rowBuffer={30}
|
||||
rowSelection="multiple"
|
||||
suppressAsyncEvents
|
||||
suppressContextMenu
|
||||
suppressCopyRowsToClipboard
|
||||
suppressMoveWhenRowDragging
|
||||
suppressPaginationPanel
|
||||
suppressScrollOnNewData
|
||||
{...rest}
|
||||
onColumnMoved={handleColumnMoved}
|
||||
onGridReady={handleGridReady}
|
||||
onGridSizeChanged={handleGridSizeChanged}
|
||||
onModelUpdated={handleModelUpdated}
|
||||
onNewColumnsLoaded={handleNewColumnsLoaded}
|
||||
/>
|
||||
{paginationProps && (
|
||||
<AnimatePresence initial={false} mode="wait" presenceAffectsLayout>
|
||||
<TablePagination {...paginationProps} tableRef={tableRef} />
|
||||
</AnimatePresence>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -1,467 +0,0 @@
|
||||
import type { ChangeEvent } from 'react';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import i18n from '/@/i18n/i18n';
|
||||
import { useSettingsStore, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
||||
import { MultiSelect } from '/@/shared/components/multi-select/multi-select';
|
||||
import { Option } from '/@/shared/components/option/option';
|
||||
import { Slider } from '/@/shared/components/slider/slider';
|
||||
import { Switch } from '/@/shared/components/switch/switch';
|
||||
import { ItemListKey, TableColumn } from '/@/shared/types/types';
|
||||
|
||||
export const SONG_TABLE_COLUMNS = [
|
||||
{
|
||||
label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.ROW_INDEX,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.image', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.IMAGE,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.title', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.TITLE,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.titleCombined', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.TITLE_COMBINED,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.duration', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.DURATION,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.album', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.ALBUM,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.albumArtist', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.ALBUM_ARTIST,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.artist', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.ARTIST,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.genre', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.GENRE,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.genreBadge', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.GENRE_BADGE,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.year', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.YEAR,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.releaseDate', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.RELEASE_DATE,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.discNumber', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.DISC_NUMBER,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.trackNumber', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.TRACK_NUMBER,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.bitrate', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.BIT_RATE,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.codec', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.CODEC,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.lastPlayed', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.LAST_PLAYED,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.note', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.COMMENT,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.channels', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.CHANNELS,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.bpm', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.BPM,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.dateAdded', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.DATE_ADDED,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.path', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.PATH,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.playCount', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.PLAY_COUNT,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.size', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.SIZE,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.favorite', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.USER_FAVORITE,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.rating', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.USER_RATING,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.actions', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.ACTIONS,
|
||||
},
|
||||
// { label: 'Skip', value: TableColumn.SKIP },
|
||||
];
|
||||
|
||||
export const ALBUM_TABLE_COLUMNS = [
|
||||
{
|
||||
label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.ROW_INDEX,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.image', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.IMAGE,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.title', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.TITLE,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.titleCombined', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.TITLE_COMBINED,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.duration', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.DURATION,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.albumArtist', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.ALBUM_ARTIST,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.artist', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.ARTIST,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.songCount', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.SONG_COUNT,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.genre', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.GENRE,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.genreBadge', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.GENRE_BADGE,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.year', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.YEAR,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.releaseDate', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.RELEASE_DATE,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.lastPlayed', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.LAST_PLAYED,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.dateAdded', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.DATE_ADDED,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.playCount', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.PLAY_COUNT,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.favorite', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.USER_FAVORITE,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.rating', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.USER_RATING,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.actions', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.ACTIONS,
|
||||
},
|
||||
];
|
||||
|
||||
export const ALBUMARTIST_TABLE_COLUMNS = [
|
||||
{
|
||||
label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.ROW_INDEX,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.title', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.TITLE,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.titleCombined', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.TITLE_COMBINED,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.duration', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.DURATION,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.biography', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.BIOGRAPHY,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.genre', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.GENRE,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.lastPlayed', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.LAST_PLAYED,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.playCount', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.PLAY_COUNT,
|
||||
},
|
||||
{
|
||||
label: i18n.t('filter.albumCount', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.ALBUM_COUNT,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.songCount', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.SONG_COUNT,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.favorite', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.USER_FAVORITE,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.rating', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.USER_RATING,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.actions', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.ACTIONS,
|
||||
},
|
||||
];
|
||||
|
||||
export const PLAYLIST_TABLE_COLUMNS = [
|
||||
{
|
||||
label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.ROW_INDEX,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.image', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.IMAGE,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.title', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.TITLE,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.titleCombined', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.TITLE_COMBINED,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.duration', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.DURATION,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.owner', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.OWNER,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.songCount', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.SONG_COUNT,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.actions', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.ACTIONS,
|
||||
},
|
||||
];
|
||||
|
||||
export const ARTIST_TABLE_COLUMNS = [
|
||||
{
|
||||
label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.ROW_INDEX,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.title', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.TITLE,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.actions', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.ACTIONS,
|
||||
},
|
||||
];
|
||||
|
||||
export const GENRE_TABLE_COLUMNS = [
|
||||
{
|
||||
label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.ROW_INDEX,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.title', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.TITLE,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.actions', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.ACTIONS,
|
||||
},
|
||||
];
|
||||
|
||||
interface TableConfigDropdownProps {
|
||||
// tableRef?: MutableRefObject<AgGridReactType<any> | null>;
|
||||
type: ItemListKey;
|
||||
}
|
||||
|
||||
export const TableConfigDropdown = ({ type }: TableConfigDropdownProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
const tableConfig = useSettingsStore((state) => state.lists);
|
||||
|
||||
const handleAddOrRemoveColumns = (values: string[]) => {
|
||||
const existingColumns = tableConfig[type].columns;
|
||||
|
||||
if (values.length === 0) {
|
||||
setSettings({
|
||||
lists: {
|
||||
...useSettingsStore.getState().lists,
|
||||
[type]: {
|
||||
...useSettingsStore.getState().lists[type],
|
||||
columns: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// If adding a column
|
||||
if (values.length > existingColumns.length) {
|
||||
const newColumn = { column: values[values.length - 1] };
|
||||
setSettings({
|
||||
lists: {
|
||||
...useSettingsStore.getState().lists,
|
||||
[type]: {
|
||||
...useSettingsStore.getState().lists[type],
|
||||
columns: [...existingColumns, newColumn],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
// If removing a column
|
||||
else {
|
||||
const removed = existingColumns.filter((column) => !values.includes(column.column));
|
||||
|
||||
const newColumns = existingColumns.filter((column) => !removed.includes(column));
|
||||
|
||||
setSettings({
|
||||
lists: {
|
||||
...useSettingsStore.getState().lists,
|
||||
[type]: {
|
||||
...useSettingsStore.getState().lists[type],
|
||||
columns: newColumns,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateRowHeight = (value: number) => {
|
||||
setSettings({
|
||||
lists: {
|
||||
...useSettingsStore.getState().lists,
|
||||
[type]: {
|
||||
...useSettingsStore.getState().lists[type],
|
||||
rowHeight: value,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleUpdateAutoFit = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setSettings({
|
||||
lists: {
|
||||
...useSettingsStore.getState().lists,
|
||||
[type]: {
|
||||
...useSettingsStore.getState().lists[type],
|
||||
autoFit: e.currentTarget.checked,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleUpdateFollow = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setSettings({
|
||||
lists: {
|
||||
...useSettingsStore.getState().lists,
|
||||
[type]: {
|
||||
...useSettingsStore.getState().lists[type],
|
||||
followCurrentSong: e.currentTarget.checked,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Option>
|
||||
<Option.Label>
|
||||
{t('table.config.general.autoFitColumns', { postProcess: 'sentenceCase' })}
|
||||
</Option.Label>
|
||||
<Option.Control>
|
||||
<Switch
|
||||
defaultChecked={tableConfig[type]?.autoFit}
|
||||
onChange={handleUpdateAutoFit}
|
||||
/>
|
||||
</Option.Control>
|
||||
</Option>
|
||||
{type !== 'albumDetail' && (
|
||||
<Option>
|
||||
<Option.Label>
|
||||
{t('table.config.general.followCurrentSong', {
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
</Option.Label>
|
||||
<Option.Control>
|
||||
<Switch
|
||||
defaultChecked={tableConfig[type]?.followCurrentSong}
|
||||
onChange={handleUpdateFollow}
|
||||
/>
|
||||
</Option.Control>
|
||||
</Option>
|
||||
)}
|
||||
<Option>
|
||||
<Option.Control>
|
||||
<Slider
|
||||
defaultValue={tableConfig[type]?.rowHeight}
|
||||
label={(value) => `Item size: ${value}`}
|
||||
max={100}
|
||||
min={25}
|
||||
onChangeEnd={handleUpdateRowHeight}
|
||||
w="100%"
|
||||
/>
|
||||
</Option.Control>
|
||||
</Option>
|
||||
<Option>
|
||||
<Option.Control>
|
||||
<MultiSelect
|
||||
clearable
|
||||
data={SONG_TABLE_COLUMNS}
|
||||
defaultValue={tableConfig[type]?.columns.map((column) => column.column)}
|
||||
onChange={handleAddOrRemoveColumns}
|
||||
variant="filled"
|
||||
width={300}
|
||||
/>
|
||||
</Option.Control>
|
||||
</Option>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,141 +0,0 @@
|
||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||
|
||||
import { useForm } from '@mantine/form';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import { MutableRefObject } from 'react';
|
||||
|
||||
import { useContainerQuery } from '/@/renderer/hooks';
|
||||
import { ListKey } from '/@/renderer/store';
|
||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||
import { Button } from '/@/shared/components/button/button';
|
||||
import { Flex } from '/@/shared/components/flex/flex';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
||||
import { Pagination } from '/@/shared/components/pagination/pagination';
|
||||
import { Popover } from '/@/shared/components/popover/popover';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import { ListPagination as TablePaginationType } from '/@/shared/types/types';
|
||||
|
||||
interface TablePaginationProps {
|
||||
pageKey: ListKey;
|
||||
pagination: TablePaginationType;
|
||||
setIdPagination?: (id: string, pagination: Partial<TablePaginationType>) => void;
|
||||
setPagination?: (args: { data: Partial<TablePaginationType>; key: ListKey }) => void;
|
||||
tableRef: MutableRefObject<AgGridReactType | null>;
|
||||
}
|
||||
|
||||
export const TablePagination = ({
|
||||
pageKey,
|
||||
pagination,
|
||||
setIdPagination,
|
||||
setPagination,
|
||||
tableRef,
|
||||
}: TablePaginationProps) => {
|
||||
const [isGoToPageOpen, handlers] = useDisclosure(false);
|
||||
const containerQuery = useContainerQuery();
|
||||
|
||||
const goToForm = useForm({
|
||||
initialValues: {
|
||||
pageNumber: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const handlePagination = (index: number) => {
|
||||
const newPage = index - 1;
|
||||
tableRef.current?.api.paginationGoToPage(newPage);
|
||||
setPagination?.({ data: { currentPage: newPage }, key: pageKey });
|
||||
setIdPagination?.(pageKey || '', { currentPage: newPage });
|
||||
};
|
||||
|
||||
const handleGoSubmit = goToForm.onSubmit((values) => {
|
||||
handlers.close();
|
||||
if (
|
||||
!values.pageNumber ||
|
||||
values.pageNumber < 1 ||
|
||||
values.pageNumber > pagination.totalPages
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newPage = values.pageNumber - 1;
|
||||
tableRef.current?.api.paginationGoToPage(newPage);
|
||||
setPagination?.({ data: { currentPage: newPage }, key: pageKey });
|
||||
setIdPagination?.(pageKey || '', { currentPage: newPage });
|
||||
});
|
||||
|
||||
const currentPageStartIndex = pagination.currentPage * pagination.itemsPerPage + 1;
|
||||
const currentPageMaxIndex = (pagination.currentPage + 1) * pagination.itemsPerPage;
|
||||
const currentPageStopIndex =
|
||||
currentPageMaxIndex > pagination.totalItems ? pagination.totalItems : currentPageMaxIndex;
|
||||
|
||||
return (
|
||||
<Flex
|
||||
align="center"
|
||||
justify="space-between"
|
||||
p="1rem"
|
||||
ref={containerQuery.ref}
|
||||
style={{ borderTop: '1px solid var(--theme-generic-border-color)' }}
|
||||
>
|
||||
<Text isMuted size="md">
|
||||
{containerQuery.isMd ? (
|
||||
<>
|
||||
Showing <b>{currentPageStartIndex}</b> - <b>{currentPageStopIndex}</b> of{' '}
|
||||
<b>{pagination.totalItems}</b> items
|
||||
</>
|
||||
) : containerQuery.isSm ? (
|
||||
<>
|
||||
<b>{currentPageStartIndex}</b> - <b>{currentPageStopIndex}</b> of{' '}
|
||||
<b>{pagination.totalItems}</b> items
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<b>{currentPageStartIndex}</b> - <b>{currentPageStopIndex}</b> of{' '}
|
||||
<b>{pagination.totalItems}</b>
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
<Group gap="sm" ref={containerQuery.ref} wrap="nowrap">
|
||||
<Popover
|
||||
onClose={() => handlers.close()}
|
||||
opened={isGoToPageOpen}
|
||||
position="bottom-start"
|
||||
trapFocus
|
||||
>
|
||||
<Popover.Target>
|
||||
<ActionIcon
|
||||
icon="hash"
|
||||
onClick={() => handlers.toggle()}
|
||||
radius="sm"
|
||||
size="sm"
|
||||
style={{ height: '26px', padding: '0', width: '26px' }}
|
||||
/>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<form onSubmit={handleGoSubmit}>
|
||||
<Group>
|
||||
<NumberInput
|
||||
{...goToForm.getInputProps('pageNumber')}
|
||||
hideControls={false}
|
||||
max={pagination.totalPages}
|
||||
min={1}
|
||||
width={70}
|
||||
/>
|
||||
<Button type="submit" variant="filled">
|
||||
Go
|
||||
</Button>
|
||||
</Group>
|
||||
</form>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
<Pagination
|
||||
boundaries={1}
|
||||
onChange={handlePagination}
|
||||
radius="sm"
|
||||
siblings={containerQuery.isMd ? 2 : containerQuery.isSm ? 1 : 0}
|
||||
total={pagination.totalPages - 1}
|
||||
value={pagination.currentPage + 1}
|
||||
/>
|
||||
</Group>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
@@ -1,41 +0,0 @@
|
||||
import { GridApi, RowNode } from '@ag-grid-community/core';
|
||||
|
||||
export const getNodesByDiscNumber = (args: {
|
||||
api: GridApi;
|
||||
discNumber: number;
|
||||
subtitle: null | string;
|
||||
}) => {
|
||||
const { api, discNumber, subtitle } = args;
|
||||
|
||||
const nodes: RowNode<any>[] = [];
|
||||
api.forEachNode((node) => {
|
||||
if (node.data.discNumber === discNumber && node.data.discSubtitle === subtitle)
|
||||
nodes.push(node);
|
||||
});
|
||||
|
||||
return nodes;
|
||||
};
|
||||
|
||||
export const setNodeSelection = (args: {
|
||||
deselectAll?: boolean;
|
||||
isSelected: boolean;
|
||||
nodes: RowNode<any>[];
|
||||
}) => {
|
||||
const { isSelected, nodes } = args;
|
||||
|
||||
nodes.forEach((node) => {
|
||||
node.setSelected(isSelected);
|
||||
});
|
||||
};
|
||||
|
||||
export const toggleNodeSelection = (args: { nodes: RowNode<any>[] }) => {
|
||||
const { nodes } = args;
|
||||
|
||||
nodes.forEach((node) => {
|
||||
if (node.isSelected()) {
|
||||
node.setSelected(false);
|
||||
} else {
|
||||
node.setSelected(true);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
.table-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.dummy-header {
|
||||
position: absolute;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { ALBUMARTIST_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
|
||||
import { ALBUM_ARTIST_TABLE_COLUMNS } from '/@/renderer/components/item-list/item-table-list/default-columns';
|
||||
import { sharedQueries } from '/@/renderer/features/shared/api/shared-api';
|
||||
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
|
||||
import { ListMusicFolderDropdown } from '/@/renderer/features/shared/components/list-music-folder-dropdown';
|
||||
@@ -52,7 +52,7 @@ export const ArtistListHeaderFilters = () => {
|
||||
<Group gap="sm" wrap="nowrap">
|
||||
<ListConfigMenu
|
||||
listKey={ItemListKey.ARTIST}
|
||||
tableColumnsData={ALBUMARTIST_TABLE_COLUMNS}
|
||||
tableColumnsData={ALBUM_ARTIST_TABLE_COLUMNS}
|
||||
/>
|
||||
</Group>
|
||||
</Flex>
|
||||
|
||||
@@ -259,6 +259,7 @@ export const AddToPlaylistAction = ({ items, itemType }: AddToPlaylistActionProp
|
||||
resourceType: itemType,
|
||||
},
|
||||
modal: 'addToPlaylist',
|
||||
size: 'lg',
|
||||
title: t('page.contextMenu.addToPlaylist', { postProcess: 'sentenceCase' }),
|
||||
});
|
||||
}, [itemType, items, t]);
|
||||
|
||||
@@ -1,364 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { AppIconSelection } from '/@/shared/components/icon/icon';
|
||||
|
||||
export enum ContextMenuItemKey {
|
||||
ADD_TO_FAVORITES = 'addToFavorites',
|
||||
ADD_TO_PLAYLIST = 'addToPlaylist',
|
||||
CREATE_PLAYLIST = 'createPlaylist',
|
||||
DELETE_PLAYLIST = 'deletePlaylist',
|
||||
DESELECT_ALL = 'deselectAll',
|
||||
DOWNLOAD = 'download',
|
||||
GO_TO_ALBUM = 'goToAlbum',
|
||||
GO_TO_ALBUM_ARTIST = 'goToAlbumArtist',
|
||||
MOVE_TO_BOTTOM_OF_QUEUE = 'moveToBottomOfQueue',
|
||||
MOVE_TO_NEXT_OF_QUEUE = 'moveToNextOfQueue',
|
||||
MOVE_TO_TOP_OF_QUEUE = 'moveToTopOfQueue',
|
||||
PLAY = 'play',
|
||||
PLAY_LAST = 'playLast',
|
||||
PLAY_NEXT = 'playNext',
|
||||
PLAY_SHUFFLED = 'playShuffled',
|
||||
PLAY_SIMILAR_SONGS = 'playSimilarSongs',
|
||||
REMOVE_FROM_FAVORITES = 'removeFromFavorites',
|
||||
REMOVE_FROM_PLAYLIST = 'removeFromPlaylist',
|
||||
REMOVE_FROM_QUEUE = 'removeFromQueue',
|
||||
SET_RATING = 'setRating',
|
||||
SET_RATING_1 = 'setRating1',
|
||||
SET_RATING_2 = 'setRating2',
|
||||
SET_RATING_3 = 'setRating3',
|
||||
SET_RATING_4 = 'setRating4',
|
||||
SET_RATING_5 = 'setRating5',
|
||||
SHARE_ITEM = 'shareItem',
|
||||
SHOW_DETAILS = 'showDetails',
|
||||
}
|
||||
|
||||
export type ContextMenuHandlers = Partial<Record<ContextMenuItemKeys, () => void>>;
|
||||
|
||||
export interface ContextMenuItem {
|
||||
disabled?: boolean;
|
||||
hidden?: boolean;
|
||||
icon?: React.ReactNode;
|
||||
items?: ContextMenuItem[];
|
||||
key: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export type ContextMenuItemDefinition = {
|
||||
children?: ContextMenuItemDefinition[];
|
||||
disabled?: boolean;
|
||||
key: ContextMenuItemKeys;
|
||||
};
|
||||
|
||||
export type ContextMenuItemKeys =
|
||||
| ContextMenuItemKey.ADD_TO_FAVORITES
|
||||
| ContextMenuItemKey.ADD_TO_PLAYLIST
|
||||
| ContextMenuItemKey.CREATE_PLAYLIST
|
||||
| ContextMenuItemKey.DELETE_PLAYLIST
|
||||
| ContextMenuItemKey.DESELECT_ALL
|
||||
| ContextMenuItemKey.DOWNLOAD
|
||||
| ContextMenuItemKey.GO_TO_ALBUM
|
||||
| ContextMenuItemKey.GO_TO_ALBUM_ARTIST
|
||||
| ContextMenuItemKey.MOVE_TO_BOTTOM_OF_QUEUE
|
||||
| ContextMenuItemKey.MOVE_TO_NEXT_OF_QUEUE
|
||||
| ContextMenuItemKey.MOVE_TO_TOP_OF_QUEUE
|
||||
| ContextMenuItemKey.PLAY
|
||||
| ContextMenuItemKey.PLAY_LAST
|
||||
| ContextMenuItemKey.PLAY_NEXT
|
||||
| ContextMenuItemKey.PLAY_SHUFFLED
|
||||
| ContextMenuItemKey.PLAY_SIMILAR_SONGS
|
||||
| ContextMenuItemKey.REMOVE_FROM_FAVORITES
|
||||
| ContextMenuItemKey.REMOVE_FROM_PLAYLIST
|
||||
| ContextMenuItemKey.REMOVE_FROM_QUEUE
|
||||
| ContextMenuItemKey.SET_RATING
|
||||
| ContextMenuItemKey.SET_RATING_1
|
||||
| ContextMenuItemKey.SET_RATING_2
|
||||
| ContextMenuItemKey.SET_RATING_3
|
||||
| ContextMenuItemKey.SET_RATING_4
|
||||
| ContextMenuItemKey.SET_RATING_5
|
||||
| ContextMenuItemKey.SHARE_ITEM
|
||||
| ContextMenuItemKey.SHOW_DETAILS;
|
||||
|
||||
export type ContextMenuItems = Array<ContextMenuItem>;
|
||||
|
||||
const ICON_MAP: Partial<Record<ContextMenuItemKeys, AppIconSelection>> = {
|
||||
[ContextMenuItemKey.ADD_TO_FAVORITES]: 'favorite',
|
||||
[ContextMenuItemKey.ADD_TO_PLAYLIST]: 'playlistAdd',
|
||||
[ContextMenuItemKey.DELETE_PLAYLIST]: 'playlistDelete',
|
||||
[ContextMenuItemKey.DESELECT_ALL]: 'remove',
|
||||
[ContextMenuItemKey.DOWNLOAD]: 'download',
|
||||
[ContextMenuItemKey.GO_TO_ALBUM]: 'album',
|
||||
[ContextMenuItemKey.GO_TO_ALBUM_ARTIST]: 'artist',
|
||||
[ContextMenuItemKey.MOVE_TO_BOTTOM_OF_QUEUE]: 'arrowDownToLine',
|
||||
[ContextMenuItemKey.MOVE_TO_NEXT_OF_QUEUE]: 'mediaPlayNext',
|
||||
[ContextMenuItemKey.MOVE_TO_TOP_OF_QUEUE]: 'arrowUpToLine',
|
||||
[ContextMenuItemKey.PLAY]: 'mediaPlay',
|
||||
[ContextMenuItemKey.PLAY_LAST]: 'mediaPlayLast',
|
||||
[ContextMenuItemKey.PLAY_NEXT]: 'mediaPlayNext',
|
||||
[ContextMenuItemKey.PLAY_SHUFFLED]: 'mediaShuffle',
|
||||
[ContextMenuItemKey.PLAY_SIMILAR_SONGS]: 'radio',
|
||||
[ContextMenuItemKey.REMOVE_FROM_FAVORITES]: 'unfavorite',
|
||||
[ContextMenuItemKey.REMOVE_FROM_PLAYLIST]: 'playlistDelete',
|
||||
[ContextMenuItemKey.REMOVE_FROM_QUEUE]: 'delete',
|
||||
[ContextMenuItemKey.SET_RATING]: 'star',
|
||||
[ContextMenuItemKey.SHARE_ITEM]: 'share',
|
||||
[ContextMenuItemKey.SHOW_DETAILS]: 'info',
|
||||
};
|
||||
|
||||
// export const convertToContextMenuItems = (
|
||||
// definitions: ContextMenuItemDefinition[],
|
||||
// handlers: ContextMenuHandlers,
|
||||
// ): ContextMenuItemOptions[] => {
|
||||
// const items: ContextMenuItemOptions[] = [];
|
||||
|
||||
// for (const def of definitions) {
|
||||
// if ('divider' in def && def.divider) {
|
||||
// items.push({ key: 'divider' });
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// if (!('key' in def)) {
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// const handler = handlers[def.key];
|
||||
|
||||
// if (!handler) {
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// const icon = ICON_MAP[def.key];
|
||||
// const menuItem: ContextMenuItemOptions = {
|
||||
// disabled: def.disabled,
|
||||
// icon: icon ? <Icon icon={icon} /> : undefined,
|
||||
// key: def.key,
|
||||
// onClick: handler,
|
||||
// };
|
||||
|
||||
// if (def.children) {
|
||||
// menuItem.items = undefined;
|
||||
// }
|
||||
|
||||
// items.push(menuItem);
|
||||
// }
|
||||
|
||||
// // Remove trailing divider
|
||||
// const lastItem = items[items.length - 1];
|
||||
// if (items.length > 0 && lastItem && 'type' in lastItem && lastItem.type === 'divider') {
|
||||
// items.pop();
|
||||
// }
|
||||
|
||||
// return items;
|
||||
// };
|
||||
|
||||
// export const QUEUE_CONTEXT_MENU_ITEMS = (): ContextMenuItemOptions[] => {
|
||||
// return [
|
||||
// { key: ContextMenuItemKey.REMOVE_FROM_QUEUE },
|
||||
// // { key: ContextMenuItemKey.MOVE_TO_NEXT_OF_QUEUE },
|
||||
// // { key: ContextMenuItemKey.MOVE_TO_BOTTOM_OF_QUEUE },
|
||||
// // { key: 'divider_1' },
|
||||
// // { key: ContextMenuItemKey.MOVE_TO_TOP_OF_QUEUE },
|
||||
// // { key: 'divider_2' },
|
||||
// // { key: ContextMenuItemKey.ADD_TO_PLAYLIST },
|
||||
// // { key: ContextMenuItemKey.ADD_TO_FAVORITES },
|
||||
// // { key: 'divider_3' },
|
||||
// // { key: ContextMenuItemKey.REMOVE_FROM_FAVORITES },
|
||||
// // { key: ContextMenuItemKey.SET_RATING },
|
||||
// // { key: ContextMenuItemKey.DESELECT_ALL },
|
||||
// // { key: 'divider_4' },
|
||||
// // { key: ContextMenuItemKey.DOWNLOAD },
|
||||
// // { key: 'divider_5' },
|
||||
// // { key: ContextMenuItemKey.SHARE_ITEM },
|
||||
// // { key: ContextMenuItemKey.GO_TO_ALBUM },
|
||||
// // { key: ContextMenuItemKey.GO_TO_ALBUM_ARTIST },
|
||||
// // { key: 'divider_6' },
|
||||
// // { key: ContextMenuItemKey.SHOW_DETAILS },
|
||||
// ];
|
||||
// };
|
||||
|
||||
// export const SONG_CONTEXT_MENU_ITEMS: ContextMenuItemDefinition[] = [
|
||||
// {
|
||||
// children: [
|
||||
// { key: ContextMenuItemKey.PLAY_LAST },
|
||||
// { key: ContextMenuItemKey.PLAY_NEXT },
|
||||
// { key: ContextMenuItemKey.PLAY_SHUFFLED },
|
||||
// ],
|
||||
// key: ContextMenuItemKey.PLAY,
|
||||
// },
|
||||
// { key: ContextMenuItemKey.DIVIDER },
|
||||
// { key: ContextMenuItemKey.PLAY_SIMILAR_SONGS },
|
||||
// { key: ContextMenuItemKey.DIVIDER },
|
||||
// { key: ContextMenuItemKey.ADD_TO_PLAYLIST },
|
||||
// { key: ContextMenuItemKey.ADD_TO_FAVORITES },
|
||||
// { key: ContextMenuItemKey.DIVIDER },
|
||||
// { key: ContextMenuItemKey.REMOVE_FROM_FAVORITES },
|
||||
// {
|
||||
// children: [
|
||||
// { key: ContextMenuItemKey.SET_RATING_1 },
|
||||
// { key: ContextMenuItemKey.SET_RATING_2 },
|
||||
// { key: ContextMenuItemKey.SET_RATING_3 },
|
||||
// { key: ContextMenuItemKey.SET_RATING_4 },
|
||||
// { key: ContextMenuItemKey.SET_RATING_5 },
|
||||
// ],
|
||||
// key: ContextMenuItemKey.SET_RATING,
|
||||
// },
|
||||
// { key: ContextMenuItemKey.DIVIDER },
|
||||
// { key: ContextMenuItemKey.DOWNLOAD },
|
||||
// { key: ContextMenuItemKey.DIVIDER },
|
||||
// { key: ContextMenuItemKey.SHARE_ITEM },
|
||||
// { key: ContextMenuItemKey.GO_TO_ALBUM },
|
||||
// { key: ContextMenuItemKey.GO_TO_ALBUM_ARTIST },
|
||||
// { key: ContextMenuItemKey.DIVIDER },
|
||||
// { key: ContextMenuItemKey.SHOW_DETAILS },
|
||||
// ];
|
||||
|
||||
// export const SONG_ALBUM_PAGE: ContextMenuItemDefinition[] = [
|
||||
// { key: ContextMenuItemKey.PLAY },
|
||||
// { key: ContextMenuItemKey.PLAY_LAST },
|
||||
// { key: ContextMenuItemKey.PLAY_NEXT },
|
||||
// { key: ContextMenuItemKey.DIVIDER },
|
||||
// { key: ContextMenuItemKey.PLAY_SHUFFLED },
|
||||
// { key: ContextMenuItemKey.DIVIDER },
|
||||
// { key: ContextMenuItemKey.ADD_TO_PLAYLIST },
|
||||
// ];
|
||||
|
||||
// export const PLAYLIST_SONG_CONTEXT_MENU_ITEMS: ContextMenuItemDefinition[] = [
|
||||
// { key: ContextMenuItemKey.PLAY },
|
||||
// { key: ContextMenuItemKey.PLAY_LAST },
|
||||
// { key: ContextMenuItemKey.PLAY_NEXT },
|
||||
// { key: ContextMenuItemKey.PLAY_SHUFFLED },
|
||||
// { key: ContextMenuItemKey.DIVIDER },
|
||||
// { key: ContextMenuItemKey.PLAY_SIMILAR_SONGS },
|
||||
// { key: ContextMenuItemKey.ADD_TO_PLAYLIST },
|
||||
// { key: ContextMenuItemKey.DIVIDER },
|
||||
// { key: ContextMenuItemKey.REMOVE_FROM_PLAYLIST },
|
||||
// { key: ContextMenuItemKey.ADD_TO_FAVORITES },
|
||||
// { key: ContextMenuItemKey.DIVIDER },
|
||||
// { key: ContextMenuItemKey.REMOVE_FROM_FAVORITES },
|
||||
// {
|
||||
// children: [
|
||||
// { key: ContextMenuItemKey.SET_RATING_1 },
|
||||
// { key: ContextMenuItemKey.SET_RATING_2 },
|
||||
// { key: ContextMenuItemKey.SET_RATING_3 },
|
||||
// { key: ContextMenuItemKey.SET_RATING_4 },
|
||||
// { key: ContextMenuItemKey.SET_RATING_5 },
|
||||
// ],
|
||||
// key: ContextMenuItemKey.SET_RATING,
|
||||
// },
|
||||
// { key: ContextMenuItemKey.DOWNLOAD },
|
||||
// { key: ContextMenuItemKey.DIVIDER },
|
||||
// { key: ContextMenuItemKey.SHARE_ITEM },
|
||||
// { key: ContextMenuItemKey.GO_TO_ALBUM },
|
||||
// { key: ContextMenuItemKey.GO_TO_ALBUM_ARTIST },
|
||||
// { key: ContextMenuItemKey.DIVIDER },
|
||||
// { key: ContextMenuItemKey.SHOW_DETAILS },
|
||||
// ];
|
||||
|
||||
// export const SMART_PLAYLIST_SONG_CONTEXT_MENU_ITEMS: ContextMenuItemDefinition[] = [
|
||||
// { key: ContextMenuItemKey.PLAY },
|
||||
// { key: ContextMenuItemKey.PLAY_LAST },
|
||||
// { key: ContextMenuItemKey.PLAY_NEXT },
|
||||
// { key: ContextMenuItemKey.DIVIDER },
|
||||
// { key: ContextMenuItemKey.PLAY_SHUFFLED },
|
||||
// { key: ContextMenuItemKey.DIVIDER },
|
||||
// { key: ContextMenuItemKey.PLAY_SIMILAR_SONGS },
|
||||
// { key: ContextMenuItemKey.DIVIDER },
|
||||
// { key: ContextMenuItemKey.ADD_TO_PLAYLIST },
|
||||
// { key: ContextMenuItemKey.ADD_TO_FAVORITES },
|
||||
// { key: ContextMenuItemKey.DIVIDER },
|
||||
// { key: ContextMenuItemKey.REMOVE_FROM_FAVORITES },
|
||||
// {
|
||||
// children: [
|
||||
// { key: ContextMenuItemKey.SET_RATING_1 },
|
||||
// { key: ContextMenuItemKey.SET_RATING_2 },
|
||||
// { key: ContextMenuItemKey.SET_RATING_3 },
|
||||
// { key: ContextMenuItemKey.SET_RATING_4 },
|
||||
// { key: ContextMenuItemKey.SET_RATING_5 },
|
||||
// ],
|
||||
// key: ContextMenuItemKey.SET_RATING,
|
||||
// },
|
||||
// { key: ContextMenuItemKey.DOWNLOAD },
|
||||
// { key: ContextMenuItemKey.DIVIDER },
|
||||
// { key: ContextMenuItemKey.SHARE_ITEM },
|
||||
// { key: ContextMenuItemKey.GO_TO_ALBUM },
|
||||
// { key: ContextMenuItemKey.GO_TO_ALBUM_ARTIST },
|
||||
// { key: ContextMenuItemKey.DIVIDER },
|
||||
// { key: ContextMenuItemKey.SHOW_DETAILS },
|
||||
// ];
|
||||
|
||||
// export const ALBUM_CONTEXT_MENU_ITEMS: ContextMenuItemDefinition[] = [
|
||||
// { key: ContextMenuItemKey.PLAY },
|
||||
// { key: ContextMenuItemKey.PLAY_LAST },
|
||||
// { key: ContextMenuItemKey.PLAY_NEXT },
|
||||
// { key: ContextMenuItemKey.DIVIDER },
|
||||
// { key: ContextMenuItemKey.PLAY_SHUFFLED },
|
||||
// { key: ContextMenuItemKey.DIVIDER },
|
||||
// { key: ContextMenuItemKey.ADD_TO_PLAYLIST },
|
||||
// { key: ContextMenuItemKey.ADD_TO_FAVORITES },
|
||||
// { key: ContextMenuItemKey.REMOVE_FROM_FAVORITES },
|
||||
// {
|
||||
// children: [
|
||||
// { key: ContextMenuItemKey.SET_RATING_1 },
|
||||
// { key: ContextMenuItemKey.SET_RATING_2 },
|
||||
// { key: ContextMenuItemKey.SET_RATING_3 },
|
||||
// { key: ContextMenuItemKey.SET_RATING_4 },
|
||||
// { key: ContextMenuItemKey.SET_RATING_5 },
|
||||
// ],
|
||||
// key: ContextMenuItemKey.SET_RATING,
|
||||
// },
|
||||
// { key: ContextMenuItemKey.DIVIDER },
|
||||
// { key: ContextMenuItemKey.SHARE_ITEM },
|
||||
// { key: ContextMenuItemKey.GO_TO_ALBUM_ARTIST },
|
||||
// { key: ContextMenuItemKey.DIVIDER },
|
||||
// { key: ContextMenuItemKey.SHOW_DETAILS },
|
||||
// ];
|
||||
|
||||
// export const GENRE_CONTEXT_MENU_ITEMS: ContextMenuItemDefinition[] = [
|
||||
// { key: ContextMenuItemKey.PLAY },
|
||||
// { key: ContextMenuItemKey.PLAY_LAST },
|
||||
// { key: ContextMenuItemKey.PLAY_NEXT },
|
||||
// { key: ContextMenuItemKey.DIVIDER },
|
||||
// { key: ContextMenuItemKey.PLAY_SHUFFLED },
|
||||
// { key: ContextMenuItemKey.DIVIDER },
|
||||
// { key: ContextMenuItemKey.ADD_TO_PLAYLIST },
|
||||
// ];
|
||||
|
||||
// export const ARTIST_CONTEXT_MENU_ITEMS: ContextMenuItemDefinition[] = [
|
||||
// { key: ContextMenuItemKey.PLAY },
|
||||
// { key: ContextMenuItemKey.PLAY_LAST },
|
||||
// { key: ContextMenuItemKey.PLAY_NEXT },
|
||||
// { key: ContextMenuItemKey.DIVIDER },
|
||||
// { key: ContextMenuItemKey.PLAY_SHUFFLED },
|
||||
// { key: ContextMenuItemKey.DIVIDER },
|
||||
// { key: ContextMenuItemKey.ADD_TO_PLAYLIST },
|
||||
// { key: ContextMenuItemKey.ADD_TO_FAVORITES },
|
||||
// { key: ContextMenuItemKey.DIVIDER },
|
||||
// { key: ContextMenuItemKey.REMOVE_FROM_FAVORITES },
|
||||
// {
|
||||
// children: [
|
||||
// { key: ContextMenuItemKey.SET_RATING_1 },
|
||||
// { key: ContextMenuItemKey.SET_RATING_2 },
|
||||
// { key: ContextMenuItemKey.SET_RATING_3 },
|
||||
// { key: ContextMenuItemKey.SET_RATING_4 },
|
||||
// { key: ContextMenuItemKey.SET_RATING_5 },
|
||||
// ],
|
||||
// key: ContextMenuItemKey.SET_RATING,
|
||||
// },
|
||||
// { key: ContextMenuItemKey.DIVIDER },
|
||||
// { key: ContextMenuItemKey.SHARE_ITEM },
|
||||
// { key: ContextMenuItemKey.DIVIDER },
|
||||
// { key: ContextMenuItemKey.SHOW_DETAILS },
|
||||
// ];
|
||||
|
||||
// export const PLAYLIST_CONTEXT_MENU_ITEMS: ContextMenuItemDefinition[] = [
|
||||
// { key: ContextMenuItemKey.PLAY },
|
||||
// { key: ContextMenuItemKey.PLAY_LAST },
|
||||
// { key: ContextMenuItemKey.PLAY_NEXT },
|
||||
// { key: ContextMenuItemKey.DIVIDER },
|
||||
// { key: ContextMenuItemKey.PLAY_SHUFFLED },
|
||||
// { key: ContextMenuItemKey.DIVIDER },
|
||||
// { key: ContextMenuItemKey.SHARE_ITEM },
|
||||
// { key: ContextMenuItemKey.DIVIDER },
|
||||
// { key: ContextMenuItemKey.DELETE_PLAYLIST },
|
||||
// { key: ContextMenuItemKey.DIVIDER },
|
||||
// { key: ContextMenuItemKey.SHOW_DETAILS },
|
||||
// ];
|
||||
@@ -1,30 +0,0 @@
|
||||
import { GridOptions, RowNode } from '@ag-grid-community/core';
|
||||
|
||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||
import { createUseExternalEvents } from '/@/shared/utils/create-use-external-events';
|
||||
|
||||
export type ContextMenuEvents = {
|
||||
closeContextMenu: () => void;
|
||||
openContextMenu: (args: OpenContextMenuProps) => void;
|
||||
};
|
||||
|
||||
export const CONTEXT_MENU_ITEM_MAPPING: { [k in ContextMenuItemKeys]?: string } = {
|
||||
[ContextMenuItemKey.MOVE_TO_BOTTOM_OF_QUEUE]: 'moveToBottom',
|
||||
[ContextMenuItemKey.MOVE_TO_TOP_OF_QUEUE]: 'moveToTop',
|
||||
[ContextMenuItemKey.PLAY_LAST]: 'addLast',
|
||||
[ContextMenuItemKey.PLAY_NEXT]: 'addNext',
|
||||
};
|
||||
|
||||
export type SetContextMenuItems = {
|
||||
children?: boolean;
|
||||
disabled?: boolean;
|
||||
divider?: boolean;
|
||||
id: ContextMenuItemKeys;
|
||||
onClick?: () => void;
|
||||
}[];
|
||||
|
||||
export const [useContextMenuEvents, createEvent] =
|
||||
createUseExternalEvents<ContextMenuEvents>('context-menu');
|
||||
|
||||
export const openContextMenu = createEvent('openContextMenu');
|
||||
export const closeContextMenu = createEvent('closeContextMenu');
|
||||
@@ -6,15 +6,14 @@ import { useLocation } from 'react-router';
|
||||
|
||||
import styles from './full-screen-player.module.css';
|
||||
|
||||
import { TableConfigDropdown } from '/@/renderer/components/virtual-table';
|
||||
import { FullScreenPlayerImage } from '/@/renderer/features/player/components/full-screen-player-image';
|
||||
import { FullScreenPlayerQueue } from '/@/renderer/features/player/components/full-screen-player-queue';
|
||||
import { useFastAverageColor } from '/@/renderer/hooks';
|
||||
import {
|
||||
usePlayerSong,
|
||||
useFullScreenPlayerStore,
|
||||
useFullScreenPlayerStoreActions,
|
||||
useLyricsSettings,
|
||||
usePlayerSong,
|
||||
useSettingsStore,
|
||||
useSettingsStoreActions,
|
||||
useWindowSettings,
|
||||
@@ -348,7 +347,6 @@ const Controls = ({ isPageHovered }: ControlsProps) => {
|
||||
</Option.Control>
|
||||
</Option>
|
||||
<Divider my="sm" />
|
||||
<TableConfigDropdown type="fullScreen" />
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
</Group>
|
||||
|
||||
+18
-108
@@ -1,32 +1,25 @@
|
||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||
|
||||
import { closeAllModals, openModal } from '@mantine/modals';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { ChangeEvent, MouseEvent, MutableRefObject, useCallback } from 'react';
|
||||
import { ChangeEvent, MouseEvent, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate, useParams } from 'react-router';
|
||||
|
||||
import i18n from '/@/i18n/i18n';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { SONG_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
|
||||
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
|
||||
import { openUpdatePlaylistModal } from '/@/renderer/features/playlists/components/update-playlist-form';
|
||||
import { useDeletePlaylist } from '/@/renderer/features/playlists/mutations/delete-playlist-mutation';
|
||||
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
|
||||
import { MoreButton } from '/@/renderer/features/shared/components/more-button';
|
||||
import { OrderToggleButton } from '/@/renderer/features/shared/components/order-toggle-button';
|
||||
import { SearchInput } from '/@/renderer/features/shared/components/search-input';
|
||||
import { useContainerQuery } from '/@/renderer/hooks';
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import {
|
||||
PersistedTableColumn,
|
||||
useCurrentServer,
|
||||
usePlaylistDetailStore,
|
||||
useSetPlaylistDetailFilters,
|
||||
useSetPlaylistDetailTable,
|
||||
useSetPlaylistStore,
|
||||
useSetPlaylistTablePagination,
|
||||
} from '/@/renderer/store';
|
||||
import { Button } from '/@/shared/components/button/button';
|
||||
import { Divider } from '/@/shared/components/divider/divider';
|
||||
@@ -242,7 +235,7 @@ const FILTERS = {
|
||||
interface PlaylistDetailSongListHeaderFiltersProps {
|
||||
handlePlay: (playType: Play) => void;
|
||||
handleToggleShowQueryBuilder: () => void;
|
||||
tableRef: MutableRefObject<AgGridReactType | null>;
|
||||
tableRef: any;
|
||||
}
|
||||
|
||||
export const PlaylistDetailSongListHeaderFilters = ({
|
||||
@@ -269,107 +262,37 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
||||
|
||||
const cq = useContainerQuery();
|
||||
|
||||
const setPagination = useSetPlaylistTablePagination();
|
||||
const setTable = useSetPlaylistDetailTable();
|
||||
|
||||
const sortByLabel =
|
||||
(server?.type &&
|
||||
FILTERS[server.type as keyof typeof FILTERS].find((f) => f.value === sortBy)?.name) ||
|
||||
'Unknown';
|
||||
|
||||
const handleItemSize = (e: number) => {
|
||||
setTable({ rowHeight: e });
|
||||
};
|
||||
|
||||
const debouncedHandleItemSize = debounce(handleItemSize, 20);
|
||||
|
||||
const handleFilterChange = useCallback(async () => {
|
||||
tableRef.current?.api.redrawRows();
|
||||
tableRef.current?.api.ensureIndexVisible(0, 'top');
|
||||
|
||||
if (page.display === ListDisplayType.TABLE_PAGINATED) {
|
||||
setPagination({ data: { currentPage: 0 } });
|
||||
}
|
||||
}, [tableRef, page.display, setPagination]);
|
||||
// tableRef.current?.api.redrawRows();
|
||||
// tableRef.current?.api.ensureIndexVisible(0, 'top');
|
||||
// if (page.display === ListDisplayType.TABLE) {
|
||||
// setPagination({ data: { currentPage: 0 } });
|
||||
// }
|
||||
}, []);
|
||||
|
||||
const handleRefresh = () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.playlists.songList(server?.id || '', playlistId),
|
||||
});
|
||||
handleFilterChange();
|
||||
};
|
||||
|
||||
const handleSetSortBy = useCallback(
|
||||
(e: MouseEvent<HTMLButtonElement>) => {
|
||||
if (!e.currentTarget?.value || !server?.type) return;
|
||||
const handleSetSortBy = useCallback((e: MouseEvent<HTMLButtonElement>) => {}, []);
|
||||
|
||||
const newSortOrder = FILTERS[server.type as keyof typeof FILTERS].find(
|
||||
(f) => f.value === e.currentTarget.value,
|
||||
)?.defaultOrder;
|
||||
const handleToggleSortOrder = useCallback(() => {}, [
|
||||
sortOrder,
|
||||
handleFilterChange,
|
||||
playlistId,
|
||||
setFilter,
|
||||
]);
|
||||
|
||||
setFilter(playlistId, {
|
||||
sortBy: e.currentTarget.value as SongListSort,
|
||||
sortOrder: newSortOrder || SortOrder.ASC,
|
||||
});
|
||||
const handleSearch = debounce((e: ChangeEvent<HTMLInputElement>) => {}, 500);
|
||||
|
||||
handleFilterChange();
|
||||
},
|
||||
[handleFilterChange, playlistId, server?.type, setFilter],
|
||||
);
|
||||
|
||||
const handleToggleSortOrder = useCallback(() => {
|
||||
const newSortOrder = sortOrder === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC;
|
||||
setFilter(playlistId, { sortOrder: newSortOrder });
|
||||
handleFilterChange();
|
||||
}, [sortOrder, handleFilterChange, playlistId, setFilter]);
|
||||
|
||||
const handleSearch = debounce((e: ChangeEvent<HTMLInputElement>) => {
|
||||
setFilter(playlistId, { searchTerm: e.target.value });
|
||||
handleFilterChange();
|
||||
}, 500);
|
||||
|
||||
const handleSetViewType = useCallback(
|
||||
(displayType: ListDisplayType) => {
|
||||
setPage({ detail: { ...page, display: displayType } });
|
||||
},
|
||||
[page, setPage],
|
||||
);
|
||||
|
||||
const handleTableColumns = (values: string[]) => {
|
||||
const existingColumns = page.table.columns;
|
||||
|
||||
if (values.length === 0) {
|
||||
return setTable({
|
||||
columns: [],
|
||||
});
|
||||
}
|
||||
|
||||
// If adding a column
|
||||
if (values.length > existingColumns.length) {
|
||||
const newColumn = {
|
||||
column: values[values.length - 1],
|
||||
width: 100,
|
||||
} as PersistedTableColumn;
|
||||
|
||||
setTable({ columns: [...existingColumns, newColumn] });
|
||||
} else {
|
||||
// If removing a column
|
||||
const removed = existingColumns.filter((column) => !values.includes(column.column));
|
||||
const newColumns = existingColumns.filter((column) => !removed.includes(column));
|
||||
|
||||
setTable({ columns: newColumns });
|
||||
}
|
||||
|
||||
return tableRef.current?.api.sizeColumnsToFit();
|
||||
};
|
||||
|
||||
const handleAutoFitColumns = (autoFitColumns: boolean) => {
|
||||
setTable({ autoFit: autoFitColumns });
|
||||
|
||||
if (autoFitColumns) {
|
||||
tableRef.current?.api.sizeColumnsToFit();
|
||||
}
|
||||
};
|
||||
const handleSetViewType = useCallback((displayType: ListDisplayType) => {}, [page, setPage]);
|
||||
|
||||
const deletePlaylistMutation = useDeletePlaylist({});
|
||||
|
||||
@@ -501,20 +424,7 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
||||
</DropdownMenu>
|
||||
<SearchInput defaultValue={searchTerm} onChange={handleSearch} />
|
||||
</Group>
|
||||
<Group>
|
||||
<ListConfigMenu
|
||||
autoFitColumns={page.table.autoFit}
|
||||
disabledViewTypes={[ListDisplayType.GRID, ListDisplayType.LIST]}
|
||||
displayType={page.display}
|
||||
itemSize={page.table.rowHeight}
|
||||
onChangeAutoFitColumns={handleAutoFitColumns}
|
||||
onChangeDisplayType={handleSetViewType}
|
||||
onChangeItemSize={debouncedHandleItemSize}
|
||||
onChangeTableColumns={handleTableColumns}
|
||||
tableColumns={page.table.columns.map((column) => column.column)}
|
||||
tableColumnsData={SONG_TABLE_COLUMNS}
|
||||
/>
|
||||
</Group>
|
||||
<Group></Group>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { closeAllModals, openModal } from '@mantine/modals';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { PLAYLIST_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
|
||||
import { PLAYLIST_TABLE_COLUMNS } from '/@/renderer/components/item-list/item-table-list/default-columns';
|
||||
import { CreatePlaylistForm } from '/@/renderer/features/playlists/components/create-playlist-form';
|
||||
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
|
||||
import { ListFilters } from '/@/renderer/features/shared/components/list-filters';
|
||||
|
||||
@@ -3,9 +3,12 @@ import { ChangeEvent } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { generatePath, Link, useParams, useSearchParams } from 'react-router';
|
||||
|
||||
import { ALBUM_ARTIST_TABLE_COLUMNS } from '/@/renderer/components/item-list/item-table-list/default-columns';
|
||||
import {
|
||||
ALBUM_ARTIST_TABLE_COLUMNS,
|
||||
ALBUM_TABLE_COLUMNS,
|
||||
SONG_TABLE_COLUMNS,
|
||||
} from '/@/renderer/components/item-list/item-table-list/default-columns';
|
||||
import { PageHeader } from '/@/renderer/components/page-header/page-header';
|
||||
import { ALBUM_TABLE_COLUMNS, SONG_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
|
||||
import { FilterBar } from '/@/renderer/features/shared/components/filter-bar';
|
||||
import { LibraryHeaderBar } from '/@/renderer/features/shared/components/library-header-bar';
|
||||
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
|
||||
|
||||
@@ -111,6 +111,7 @@ const PlaylistRowButton = ({ name, onPlay, to, ...props }: PlaylistRowButtonProp
|
||||
openContextModal({
|
||||
innerProps: modalProps,
|
||||
modal: 'addToPlaylist',
|
||||
size: 'lg',
|
||||
title: t('form.addToPlaylist.title', { postProcess: 'titleCase' }),
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import { RowDoubleClickedEvent } from '@ag-grid-community/core';
|
||||
import { AgGridReact } from '@ag-grid-community/react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { useRef } from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
|
||||
import { getColumnDefs, VirtualTable } from '/@/renderer/components/virtual-table';
|
||||
import { ErrorFallback } from '/@/renderer/features/action-required/components/error-fallback';
|
||||
import { SONG_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items';
|
||||
import { songsQueries } from '/@/renderer/features/songs/api/songs-api';
|
||||
import { usePlayButtonBehavior, useTableSettings } from '/@/renderer/store';
|
||||
import { Spinner } from '/@/shared/components/spinner/spinner';
|
||||
import { LibraryItem, Song } from '/@/shared/types/domain-types';
|
||||
import { Song } from '/@/shared/types/domain-types';
|
||||
|
||||
export type SimilarSongsListProps = {
|
||||
count?: number;
|
||||
@@ -56,26 +53,6 @@ export const SimilarSongsList = ({ count, fullScreen, song }: SimilarSongsListPr
|
||||
return songQuery.isLoading ? (
|
||||
<Spinner container size={25} />
|
||||
) : (
|
||||
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||
{/* <VirtualTable
|
||||
autoFitColumns={tableConfig.autoFit}
|
||||
columnDefs={columnDefs}
|
||||
context={{
|
||||
count,
|
||||
itemType: LibraryItem.SONG,
|
||||
onCellContextMenu,
|
||||
song,
|
||||
}}
|
||||
deselectOnClickOutside={fullScreen}
|
||||
getRowId={(data) => data.data.uniqueId}
|
||||
onCellContextMenu={onCellContextMenu}
|
||||
onCellDoubleClicked={handleRowDoubleClick}
|
||||
ref={tableRef}
|
||||
rowBuffer={50}
|
||||
rowData={songQuery.data ?? []}
|
||||
rowHeight={tableConfig.rowHeight || 40}
|
||||
shouldUpdateSong
|
||||
/> */}
|
||||
</ErrorBoundary>
|
||||
<ErrorBoundary FallbackComponent={ErrorFallback}></ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user