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