various cleanup

This commit is contained in:
jeffvli
2025-11-15 19:32:17 -08:00
parent 68318340e1
commit 88a0e0d1c1
44 changed files with 32 additions and 4021 deletions
@@ -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);
}
-350
View File
@@ -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>
@@ -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>
);
};