Migrate to Mantine v8 and Design Changes (#961)

* mantine v8 migration

* various design changes and improvements
This commit is contained in:
Jeff 2025-06-24 00:04:36 -07:00 committed by GitHub
parent bea55d48a8
commit c1330d92b2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
473 changed files with 12469 additions and 11607 deletions

View file

@ -1,24 +1,21 @@
import type { ICellRendererParams } from '@ag-grid-community/core';
import { RiMoreFill } from 'react-icons/ri';
import { Button } from '/@/renderer/components/button';
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">
<Button
compact
<CellContainer position="center">
<ActionIcon
icon="ellipsisHorizontal"
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
context.onCellContextMenu(undefined, api, e);
}}
size="sm"
variant="subtle"
>
<RiMoreFill />
</Button>
/>
</CellContainer>
);
};

View file

@ -5,16 +5,16 @@ import React from 'react';
import { generatePath } from 'react-router';
import { Link } from 'react-router-dom';
import { Separator } from '/@/renderer/components/separator';
import { Skeleton } from '/@/renderer/components/skeleton';
import { Text } from '/@/renderer/components/text';
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">
<CellContainer position="left">
<Skeleton
height="1rem"
width="80%"
@ -24,9 +24,9 @@ export const AlbumArtistCell = ({ data, value }: ICellRendererParams) => {
}
return (
<CellContainer $position="left">
<CellContainer position="left">
<Text
$secondary
isMuted
overflow="hidden"
size="md"
>
@ -35,9 +35,9 @@ export const AlbumArtistCell = ({ data, value }: ICellRendererParams) => {
{index > 0 && <Separator />}
{item.id ? (
<Text
$link
$secondary
component={Link}
isLink
isMuted
overflow="hidden"
size="md"
to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
@ -48,7 +48,7 @@ export const AlbumArtistCell = ({ data, value }: ICellRendererParams) => {
</Text>
) : (
<Text
$secondary
isMuted
overflow="hidden"
size="md"
>

View file

@ -5,16 +5,16 @@ import React from 'react';
import { generatePath } from 'react-router';
import { Link } from 'react-router-dom';
import { Separator } from '/@/renderer/components/separator';
import { Skeleton } from '/@/renderer/components/skeleton';
import { Text } from '/@/renderer/components/text';
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">
<CellContainer position="left">
<Skeleton
height="1rem"
width="80%"
@ -24,9 +24,9 @@ export const ArtistCell = ({ data, value }: ICellRendererParams) => {
}
return (
<CellContainer $position="left">
<CellContainer position="left">
<Text
$secondary
isMuted
overflow="hidden"
size="md"
>
@ -35,9 +35,9 @@ export const ArtistCell = ({ data, value }: ICellRendererParams) => {
{index > 0 && <Separator />}
{item.id ? (
<Text
$link
$secondary
component={Link}
isLink
isMuted
overflow="hidden"
size="md"
to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
@ -48,7 +48,7 @@ export const ArtistCell = ({ data, value }: ICellRendererParams) => {
</Text>
) : (
<Text
$secondary
isMuted
overflow="hidden"
size="md"
>

View file

@ -0,0 +1,35 @@
.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%;
}

View file

@ -1,62 +1,21 @@
import type { UnstyledButtonProps } from '@mantine/core';
import clsx from 'clsx';
import React, { MouseEvent } from 'react';
import { RiPlayFill } from 'react-icons/ri';
import styled from 'styled-components';
import styles from './combined-title-cell-controls.module.css';
import { usePlayQueueAdd } from '/@/renderer/features/player';
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';
type PlayButtonType = React.ComponentPropsWithoutRef<'button'> & UnstyledButtonProps;
const PlayButton = styled.button<PlayButtonType>`
position: absolute;
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
background-color: rgb(255 255 255);
border: none;
border-radius: 50%;
opacity: 0.8;
transition: scale 0.1s ease-in-out;
&:hover {
opacity: 1;
scale: 1.1;
}
&:active {
opacity: 1;
scale: 1;
}
svg {
fill: rgb(0 0 0);
stroke: rgb(0 0 0);
}
`;
const ListConverControlsContainer = styled.div`
position: absolute;
z-index: 100;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
`;
export const ListCoverControls = ({
className,
context,
itemData,
itemType,
uniqueId,
}: {
className?: string;
context: Record<string, any>;
itemData: any;
itemType: LibraryItem;
@ -66,7 +25,7 @@ export const ListCoverControls = ({
const handlePlayQueueAdd = usePlayQueueAdd();
const isQueue = Boolean(context?.isQueue);
const handlePlay = async (e: MouseEvent<HTMLButtonElement>, playType?: Play) => {
const handlePlay = async (e: React.MouseEvent<HTMLButtonElement>, playType?: Play) => {
e.preventDefault();
e.stopPropagation();
@ -88,12 +47,12 @@ export const ListCoverControls = ({
};
return (
<>
<ListConverControlsContainer className="card-controls">
<PlayButton onClick={isQueue ? handlePlayFromQueue : handlePlay}>
<RiPlayFill size={20} />
</PlayButton>
</ListConverControlsContainer>
</>
<div className={clsx(styles.listControlsContainer, className)}>
<ActionIcon
classNames={{ root: styles.playButton }}
icon="mediaPlay"
onClick={isQueue ? handlePlayFromQueue : handlePlay}
/>
</div>
);
};

View file

@ -0,0 +1,50 @@
.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);
}

View file

@ -1,67 +1,19 @@
import type { ICellRendererParams } from '@ag-grid-community/core';
import { Center } from '@mantine/core';
import { motion } from 'framer-motion';
import React, { useMemo } from 'react';
import { RiAlbumFill } from 'react-icons/ri';
import { generatePath } from 'react-router';
import { Link } from 'react-router-dom';
import { SimpleImg } from 'react-simple-img';
import styled from 'styled-components';
import { Skeleton } from '/@/renderer/components/skeleton';
import { Text } from '/@/renderer/components/text';
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';
const CellContainer = styled(motion.div)<{ height: number }>`
display: grid;
grid-template-areas: 'image info';
grid-template-rows: 1fr;
grid-template-columns: ${(props) => props.height}px minmax(0, 1fr);
grid-auto-columns: 1fr;
gap: 0.5rem;
width: 100%;
max-width: 100%;
height: 100%;
letter-spacing: 0.5px;
.card-controls {
opacity: 0;
}
&:hover {
.card-controls {
opacity: 1;
}
}
`;
const ImageWrapper = styled.div`
position: relative;
display: flex;
grid-area: image;
align-items: center;
justify-content: center;
height: 100%;
`;
const MetadataWrapper = styled.div`
display: flex;
flex-direction: column;
grid-area: info;
justify-content: center;
width: 100%;
`;
const StyledImage = styled(SimpleImg)`
img {
object-fit: var(--image-fit);
}
`;
export const CombinedTitleCell = ({
context,
data,
@ -76,60 +28,55 @@ export const CombinedTitleCell = ({
if (value === undefined) {
return (
<CellContainer height={node.rowHeight || 40}>
<Skeleton>
<ImageWrapper />
</Skeleton>
<MetadataWrapper>
<Skeleton
height="1rem"
width="80%"
/>
<Skeleton
height="1rem"
mt="0.5rem"
width="60%"
/>
</MetadataWrapper>
</CellContainer>
<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 (
<CellContainer height={node.rowHeight || 40}>
<ImageWrapper>
{value.imageUrl ? (
<StyledImage
alt="cover"
height={(node.rowHeight || 40) - 10}
placeholder={value.imagePlaceholderUrl || 'var(--placeholder-bg)'}
src={value.imageUrl}
style={{}}
width={(node.rowHeight || 40) - 10}
/>
) : (
<Center
sx={{
background: 'var(--placeholder-bg)',
borderRadius: 'var(--card-default-radius)',
height: `${(node.rowHeight || 40) - 10}px`,
width: `${(node.rowHeight || 40) - 10}px`,
}}
>
<RiAlbumFill
color="var(--placeholder-fg)"
size={35}
/>
</Center>
)}
<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}
/>
</ImageWrapper>
<MetadataWrapper>
</div>
<div className={styles.metadataWrapper}>
<Text
className="current-song-child"
overflow="hidden"
@ -138,7 +85,7 @@ export const CombinedTitleCell = ({
{value.name}
</Text>
<Text
$secondary
isMuted
overflow="hidden"
size="md"
>
@ -148,12 +95,12 @@ export const CombinedTitleCell = ({
{index > 0 ? SEPARATOR_STRING : null}
{artist.id ? (
<Text
$link
$secondary
component={Link}
isLink
isMuted
overflow="hidden"
size="md"
sx={{ width: 'fit-content' }}
style={{ width: 'fit-content' }}
to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
albumArtistId: artist.id,
})}
@ -162,10 +109,10 @@ export const CombinedTitleCell = ({
</Text>
) : (
<Text
$secondary
isMuted
overflow="hidden"
size="md"
sx={{ width: 'fit-content' }}
style={{ width: 'fit-content' }}
>
{artist.name}
</Text>
@ -173,10 +120,10 @@ export const CombinedTitleCell = ({
</React.Fragment>
))
) : (
<Text $secondary></Text>
<Text isMuted></Text>
)}
</Text>
</MetadataWrapper>
</CellContainer>
</div>
</div>
);
};

View file

@ -1,10 +1,8 @@
import type { ICellRendererParams } from '@ag-grid-community/core';
import { RiHeartFill, RiHeartLine } from 'react-icons/ri';
import { Button } from '/@/renderer/components/button';
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';
import { useCreateFavorite, useDeleteFavorite } from '/@/renderer/features/shared';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
export const FavoriteCell = ({ data, node, value }: ICellRendererParams) => {
const createMutation = useCreateFavorite({});
@ -47,21 +45,16 @@ export const FavoriteCell = ({ data, node, value }: ICellRendererParams) => {
};
return (
<CellContainer $position="center">
<Button
compact
onClick={handleToggleFavorite}
sx={{
svg: {
fill: !value
? 'var(--main-fg-secondary) !important'
: 'var(--primary-color) !important',
},
<CellContainer position="center">
<ActionIcon
icon="favorite"
iconProps={{
fill: !value ? undefined : 'primary',
}}
onClick={handleToggleFavorite}
size="sm"
variant="subtle"
>
{!value ? <RiHeartLine size="1.3em" /> : <RiHeartFill size="1.3em" />}
</Button>
/>
</CellContainer>
);
};

View file

@ -0,0 +1,6 @@
.container {
display: flex;
height: 100%;
padding: 0.5rem 1rem;
border: 1px solid transparent;
}

View file

@ -1,19 +1,12 @@
import { ICellRendererParams } from '@ag-grid-community/core';
import { Group } from '@mantine/core';
import { useState } from 'react';
import { RiCheckboxBlankLine, RiCheckboxLine } from 'react-icons/ri';
import styled from 'styled-components';
import { Button } from '/@/renderer/components/button';
import { Paper } from '/@/renderer/components/paper';
import styles from './full-width-disc-cell.module.css';
import { getNodesByDiscNumber, setNodeSelection } from '/@/renderer/components/virtual-table/utils';
const Container = styled(Paper)`
display: flex;
height: 100%;
padding: 0.5rem 1rem;
border: 1px solid transparent;
`;
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);
@ -31,21 +24,20 @@ export const FullWidthDiscCell = ({ api, data, node }: ICellRendererParams) => {
};
return (
<Container>
<div className={styles.container}>
<Group
position="apart"
justify="space-between"
w="100%"
>
<Button
compact
leftIcon={isSelected ? <RiCheckboxLine /> : <RiCheckboxBlankLine />}
leftSection={isSelected ? <Icon icon="squareCheck" /> : <Icon icon="square" />}
onClick={handleToggleDiscNodes}
size="md"
size="compact-md"
variant="subtle"
>
{data.name}
</Button>
</Group>
</Container>
</div>
);
};

View file

@ -0,0 +1,19 @@
.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;
}

View file

@ -1,24 +1,12 @@
import type { ICellRendererParams } from '@ag-grid-community/core';
import clsx from 'clsx';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
import { Skeleton } from '/@/renderer/components/skeleton';
import { Text } from '/@/renderer/components/text';
import styles from './generic-cell.module.css';
export const CellContainer = styled.div<{ $position?: 'center' | 'left' | 'right' }>`
display: flex;
align-items: center;
justify-content: ${(props) =>
props.$position === 'right'
? 'flex-end'
: props.$position === 'center'
? 'center'
: 'flex-start'};
width: 100%;
height: 100%;
letter-spacing: 0.5px;
`;
import { Skeleton } from '/@/shared/components/skeleton/skeleton';
import { Text } from '/@/shared/components/text/text';
type Options = {
array?: boolean;
@ -36,7 +24,7 @@ export const GenericCell = (
if (value === undefined) {
return (
<CellContainer $position={position || 'left'}>
<CellContainer position={position || 'left'}>
<Skeleton
height="1rem"
width="80%"
@ -46,12 +34,12 @@ export const GenericCell = (
}
return (
<CellContainer $position={position || 'left'}>
<CellContainer position={position || 'left'}>
{isLink ? (
<Text
$link={isLink}
$secondary={!primary}
component={Link}
isLink={isLink}
isMuted={!primary}
overflow="hidden"
size="md"
to={displayedValue.link}
@ -60,8 +48,8 @@ export const GenericCell = (
</Text>
) : (
<Text
$noSelect={false}
$secondary={!primary}
isMuted={!primary}
isNoSelect={false}
overflow="hidden"
size="md"
>
@ -71,3 +59,24 @@ export const GenericCell = (
</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>
);
};

View file

@ -4,17 +4,17 @@ import type { ICellRendererParams } from '@ag-grid-community/core';
import React from 'react';
import { generatePath, Link } from 'react-router-dom';
import { Separator } from '/@/renderer/components/separator';
import { Text } from '/@/renderer/components/text';
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">
<CellContainer position="left">
<Text
$secondary
isMuted
overflow="hidden"
size="md"
>
@ -22,9 +22,9 @@ export const GenreCell = ({ data, value }: ICellRendererParams) => {
<React.Fragment key={`row-${item.id}-${data.uniqueId}`}>
{index > 0 && <Separator />}
<Text
$link
$secondary
component={Link}
isLink
isMuted
overflow="hidden"
size="md"
to={generatePath(genrePath, { genreId: item.id })}

View file

@ -2,10 +2,10 @@ import type { ICellRendererParams } from '@ag-grid-community/core';
import { useMemo } from 'react';
import { Skeleton } from '/@/renderer/components/skeleton';
import { Text } from '/@/renderer/components/text';
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(() => {
@ -18,7 +18,7 @@ export const NoteCell = ({ value }: ICellRendererParams) => {
if (value === undefined) {
return (
<CellContainer $position="left">
<CellContainer position="left">
<Skeleton
height="1rem"
width="80%"
@ -28,9 +28,9 @@ export const NoteCell = ({ value }: ICellRendererParams) => {
}
return (
<CellContainer $position="left">
<CellContainer position="left">
<Text
$secondary
isMuted
overflow="hidden"
>
{formattedValue}

View file

@ -1,8 +1,8 @@
import type { ICellRendererParams } from '@ag-grid-community/core';
import { Rating } from '/@/renderer/components/rating';
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';
import { useSetRating } from '/@/renderer/features/shared';
import { Rating } from '/@/shared/components/rating/rating';
export const RatingCell = ({ node, value }: ICellRendererParams) => {
const updateRatingMutation = useSetRating({});
@ -25,7 +25,7 @@ export const RatingCell = ({ node, value }: ICellRendererParams) => {
};
return (
<CellContainer $position="center">
<CellContainer position="center">
<Rating
onChange={handleUpdateRating}
size="xs"

View file

@ -1,9 +1,8 @@
import type { ICellRendererParams } from '@ag-grid-community/core';
import { RiPlayFill } from 'react-icons/ri';
import { Text } from '/@/renderer/components/text';
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 (
@ -14,7 +13,7 @@ import { CellContainer } from '/@/renderer/components/virtual-table/cells/generi
// >
// <g>
// <rect
// fill="var(--primary-color)"
// fill="var(--theme-colors-primary-filled)"
// height="80"
// id="bar-1"
// width="12"
@ -33,7 +32,7 @@ import { CellContainer } from '/@/renderer/components/virtual-table/cells/generi
// />
// </rect>
// <rect
// fill="var(--primary-color)"
// fill="var(--theme-colors-primary-filled)"
// height="80"
// id="bar-2"
// width="12"
@ -52,7 +51,7 @@ import { CellContainer } from '/@/renderer/components/virtual-table/cells/generi
// />
// </rect>
// <rect
// fill="var(--primary-color)"
// fill="var(--theme-colors-primary-filled)"
// height="80"
// id="bar-3"
// width="12"
@ -71,7 +70,7 @@ import { CellContainer } from '/@/renderer/components/virtual-table/cells/generi
// />
// </rect>
// <rect
// fill="var(--primary-color)"
// fill="var(--theme-colors-primary-filled)"
// height="80"
// id="bar-4"
// width="12"
@ -143,23 +142,28 @@ export const RowIndexCell = ({ eGridCell, value }: ICellRendererParams) => {
classList.contains('current-song-cell') || classList.contains('current-playlist-song-cell');
return (
<CellContainer $position="right">
{isPlaying &&
(isCurrentSong ? (
<RiPlayFill
color="var(--primary-color)"
size="1.2rem"
/>
) : null)}
<Text
$secondary
align="right"
className="current-song-child current-song-index"
overflow="hidden"
size="md"
>
{value}
</Text>
<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>
);
};

View file

@ -1,13 +1,13 @@
import type { ICellRendererParams } from '@ag-grid-community/core';
import { Skeleton } from '/@/renderer/components/skeleton';
import { Text } from '/@/renderer/components/text';
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">
<CellContainer position="left">
<Skeleton
height="1rem"
width="80%"
@ -17,7 +17,7 @@ export const TitleCell = ({ value }: ICellRendererParams) => {
}
return (
<CellContainer $position="left">
<CellContainer position="left">
<Text
className="current-song-child"
overflow="hidden"

View file

@ -1,11 +1,16 @@
import type { IHeaderParams } from '@ag-grid-community/core';
import { FiClock } from 'react-icons/fi';
import { Icon } from '/@/shared/components/icon/icon';
export interface ICustomHeaderParams extends IHeaderParams {
menuIcon: string;
}
export const DurationHeader = () => {
return <FiClock size={15} />;
return (
<Icon
icon="duration"
size="sm"
/>
);
};

View file

@ -0,0 +1,38 @@
.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;
}

View file

@ -1,12 +1,11 @@
import type { IHeaderParams } from '@ag-grid-community/core';
import type { ReactNode } from 'react';
import { AiOutlineNumber } from 'react-icons/ai';
import { FiClock } from 'react-icons/fi';
import { RiHeartLine, RiMoreFill, RiStarLine } from 'react-icons/ri';
import styled from 'styled-components';
import clsx from 'clsx';
import { _Text } from '/@/renderer/components/text';
import styles from './generic-table-header.module.css';
import { Icon } from '/@/shared/components/icon/icon';
type Options = {
children?: ReactNode;
@ -16,63 +15,35 @@ type Options = {
type Presets = 'actions' | 'duration' | 'rowIndex' | 'userFavorite' | 'userRating';
export const HeaderWrapper = styled.div<{ $position: Options['position'] }>`
display: flex;
justify-content: ${(props) =>
props.$position === 'right'
? 'flex-end'
: props.$position === 'center'
? 'center'
: 'flex-start'};
width: 100%;
font-family: var(--content-font-family);
text-transform: uppercase;
`;
const HeaderText = styled(_Text)<{ $position: Options['position'] }>`
width: 100%;
height: 100%;
font-weight: 500;
line-height: inherit;
color: var(--ag-header-foreground-color);
text-align: ${(props) =>
props.$position === 'right'
? 'flex-end'
: props.$position === 'center'
? 'center'
: 'flex-start'};
text-transform: uppercase;
`;
const headerPresets = {
actions: (
<RiMoreFill
color="var(--ag-header-foreground-color)"
size="1em"
<Icon
icon="ellipsisHorizontal"
size="sm"
/>
),
duration: (
<FiClock
color="var(--ag-header-foreground-color)"
size="1em"
<Icon
icon="duration"
size="sm"
/>
),
rowIndex: (
<AiOutlineNumber
color="var(--ag-header-foreground-color)"
size="1em"
<Icon
icon="hash"
size="sm"
/>
),
userFavorite: (
<RiHeartLine
color="var(--ag-header-foreground-color)"
size="1em"
<Icon
icon="favorite"
size="sm"
/>
),
userRating: (
<RiStarLine
color="var(--ag-header-foreground-color)"
size="1em"
<Icon
icon="star"
size="sm"
/>
),
};
@ -82,18 +53,18 @@ export const GenericTableHeader = (
{ children, position, preset }: Options,
) => {
if (preset) {
return <HeaderWrapper $position={position}>{headerPresets[preset]}</HeaderWrapper>;
return (
<div className={clsx(styles.headerWrapper, styles[position ?? 'left'])}>
{headerPresets[preset]}
</div>
);
}
return (
<HeaderWrapper $position={position}>
<HeaderText
$position={position}
overflow="hidden"
weight={500}
>
<div className={clsx(styles.headerWrapper, styles[position ?? 'left'])}>
<div className={clsx(styles.headerText, styles[position ?? 'left'])}>
{children || displayName}
</HeaderText>
</HeaderWrapper>
</div>
</div>
);
};

View file

@ -1,4 +1,4 @@
import { useInView } from 'framer-motion';
import { useInView } from 'motion/react';
import { useEffect, useRef } from 'react';
import { useWindowSettings } from '/@/renderer/store/settings.store';

View file

@ -15,11 +15,13 @@ import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/li
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 'framer-motion';
import { AnimatePresence } from 'motion/react';
import { forwardRef, Ref, useCallback, useEffect, useMemo, useRef } from 'react';
import { generatePath } from 'react-router';
import styled from 'styled-components';
import styles from './virtual-table.module.css';
import i18n from '/@/i18n/i18n';
import { ActionsCell } from '/@/renderer/components/virtual-table/cells/actions-cell';
@ -57,18 +59,18 @@ 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 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 DummyHeader = styled.div<{ height?: number }>`
// position: absolute;
// height: ${({ height }) => height || 36}px;
// `;
const tableColumns: { [key: string]: ColDef } = {
actions: {
@ -593,15 +595,20 @@ export const VirtualTable = forwardRef(
const mergedWrapperRef = useMergedRef(deselectRef, tableContainerRef);
return (
<TableWrapper
className={
<div
className={clsx(
styles.tableWrapper,
transparentHeader
? 'ag-theme-alpine-dark ag-header-transparent'
: 'ag-theme-alpine-dark'
}
: 'ag-theme-alpine-dark',
)}
ref={mergedWrapperRef}
>
<DummyHeader ref={tableHeaderRef} />
<div
className={styles.dummyHeader}
ref={tableHeaderRef}
style={{ height: rest.headerHeight ?? 36 }}
/>
<AgGridReact
animateRows
blockLoadDebounceMillis={200}
@ -639,7 +646,7 @@ export const VirtualTable = forwardRef(
/>
</AnimatePresence>
)}
</TableWrapper>
</div>
);
},
);

View file

@ -3,11 +3,11 @@ import type { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
import i18n from '/@/i18n/i18n';
import { Option } from '/@/renderer/components/option';
import { MultiSelect } from '/@/renderer/components/select';
import { Slider } from '/@/renderer/components/slider';
import { Switch } from '/@/renderer/components/switch';
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 { TableColumn, TableType } from '/@/shared/types/types';
export const SONG_TABLE_COLUMNS = [
@ -292,7 +292,7 @@ export const TableConfigDropdown = ({ type }: TableConfigDropdownProps) => {
const { setSettings } = useSettingsStoreActions();
const tableConfig = useSettingsStore((state) => state.tables);
const handleAddOrRemoveColumns = (values: TableColumn[]) => {
const handleAddOrRemoveColumns = (values: string[]) => {
const existingColumns = tableConfig[type].columns;
if (values.length === 0) {
@ -421,8 +421,8 @@ export const TableConfigDropdown = ({ type }: TableConfigDropdownProps) => {
clearable
data={SONG_TABLE_COLUMNS}
defaultValue={tableConfig[type]?.columns.map((column) => column.column)}
dropdownPosition="bottom"
onChange={handleAddOrRemoveColumns}
variant="filled"
width={300}
/>
</Option.Control>

View file

@ -1,20 +1,19 @@
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { Group } from '@mantine/core';
import { useForm } from '@mantine/form';
import { useDisclosure } from '@mantine/hooks';
import { MutableRefObject } from 'react';
import { useTranslation } from 'react-i18next';
import { RiHashtag } from 'react-icons/ri';
import { Button } from '/@/renderer/components/button';
import { NumberInput } from '/@/renderer/components/input';
import { MotionFlex } from '/@/renderer/components/motion';
import { Pagination } from '/@/renderer/components/pagination';
import { Popover } from '/@/renderer/components/popover';
import { Text } from '/@/renderer/components/text';
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 { TablePagination as TablePaginationType } from '/@/shared/types/types';
interface TablePaginationProps {
@ -32,7 +31,6 @@ export const TablePagination = ({
setPagination,
tableRef,
}: TablePaginationProps) => {
const { t } = useTranslation();
const [isGoToPageOpen, handlers] = useDisclosure(false);
const containerQuery = useContainerQuery();
@ -71,19 +69,15 @@ export const TablePagination = ({
currentPageMaxIndex > pagination.totalItems ? pagination.totalItems : currentPageMaxIndex;
return (
<MotionFlex
<Flex
align="center"
animate={{ y: 0 }}
exit={{ y: 50 }}
initial={{ y: 50 }}
justify="space-between"
layout
p="1rem"
ref={containerQuery.ref}
sx={{ borderTop: '1px solid var(--generic-border-color)' }}
style={{ borderTop: '1px solid var(--theme-generic-border-color)' }}
>
<Text
$secondary
isMuted
size="md"
>
{containerQuery.isMd ? (
@ -104,9 +98,9 @@ export const TablePagination = ({
)}
</Text>
<Group
noWrap
gap="sm"
ref={containerQuery.ref}
spacing="sm"
wrap="nowrap"
>
<Popover
onClose={() => handlers.close()}
@ -115,18 +109,13 @@ export const TablePagination = ({
trapFocus
>
<Popover.Target>
<Button
<ActionIcon
icon="hash"
onClick={() => handlers.toggle()}
radius="sm"
size="sm"
sx={{ height: '26px', padding: '0', width: '26px' }}
tooltip={{
label: t('action.goToPage', { postProcess: 'sentenceCase' }),
}}
variant="default"
>
<RiHashtag size={15} />
</Button>
style={{ height: '26px', padding: '0', width: '26px' }}
/>
</Popover.Target>
<Popover.Dropdown>
<form onSubmit={handleGoSubmit}>
@ -149,9 +138,7 @@ export const TablePagination = ({
</Popover.Dropdown>
</Popover>
<Pagination
$hideDividers={!containerQuery.isSm}
boundaries={1}
noWrap
onChange={handlePagination}
radius="sm"
siblings={containerQuery.isMd ? 2 : containerQuery.isSm ? 1 : 0}
@ -159,6 +146,6 @@ export const TablePagination = ({
value={pagination.currentPage + 1}
/>
</Group>
</MotionFlex>
</Flex>
);
};

View file

@ -0,0 +1,11 @@
.table-wrapper {
position: relative;
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
.dummy-header {
position: absolute;
}