mirror of
https://github.com/antebudimir/feishin.git
synced 2025-12-31 18:13:31 +00:00
restructure files onto electron-vite boilerplate
This commit is contained in:
parent
91ce2cd8a1
commit
1cf587bc8f
457 changed files with 9927 additions and 11705 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import type { AccordionProps as MantineAccordionProps } from '@mantine/core';
|
||||
|
||||
import { Accordion as MantineAccordion } from '@mantine/core';
|
||||
import styled from 'styled-components';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,27 +1,36 @@
|
|||
import type { Song } from '/@/renderer/api/types';
|
||||
import type { CrossfadeStyle } from '/@/renderer/types';
|
||||
import type { ReactPlayerProps } from 'react-player';
|
||||
|
||||
import isElectron from 'is-electron';
|
||||
import {
|
||||
useImperativeHandle,
|
||||
forwardRef,
|
||||
useRef,
|
||||
useState,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import isElectron from 'is-electron';
|
||||
import type { ReactPlayerProps } from 'react-player';
|
||||
import ReactPlayer from 'react-player/lazy';
|
||||
import type { Song } from '/@/renderer/api/types';
|
||||
|
||||
import { api } from '/@/renderer/api';
|
||||
import {
|
||||
crossfadeHandler,
|
||||
gaplessHandler,
|
||||
} from '/@/renderer/components/audio-player/utils/list-handlers';
|
||||
import { useSettingsStore, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
||||
import type { CrossfadeStyle } from '/@/renderer/types';
|
||||
import { PlaybackStyle, PlayerStatus } from '/@/renderer/types';
|
||||
import { toast } from '/@/renderer/components/toast';
|
||||
import { useWebAudio } from '/@/renderer/features/player/hooks/use-webaudio';
|
||||
import { getServerById, TranscodingConfig, usePlaybackSettings, useSpeed } from '/@/renderer/store';
|
||||
import { toast } from '/@/renderer/components/toast';
|
||||
import { api } from '/@/renderer/api';
|
||||
import { useSettingsStore, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
||||
import { PlaybackStyle, PlayerStatus } from '/@/renderer/types';
|
||||
|
||||
export type AudioPlayerProgress = {
|
||||
loaded: number;
|
||||
loadedSeconds: number;
|
||||
played: number;
|
||||
playedSeconds: number;
|
||||
};
|
||||
|
||||
interface AudioPlayerProps extends ReactPlayerProps {
|
||||
crossfadeDuration: number;
|
||||
|
|
@ -34,13 +43,6 @@ interface AudioPlayerProps extends ReactPlayerProps {
|
|||
volume: number;
|
||||
}
|
||||
|
||||
export type AudioPlayerProgress = {
|
||||
loaded: number;
|
||||
loadedSeconds: number;
|
||||
played: number;
|
||||
playedSeconds: number;
|
||||
};
|
||||
|
||||
const getDuration = (ref: any) => {
|
||||
return ref.current?.player?.player?.player?.duration;
|
||||
};
|
||||
|
|
@ -53,7 +55,7 @@ const getDuration = (ref: any) => {
|
|||
const EMPTY_SOURCE =
|
||||
'data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU2LjM2LjEwMAAAAAAAAAAAAAAA//OEAAAAAAAAAAAAAAAAAAAAAAAASW5mbwAAAA8AAAAEAAABIADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV6urq6urq6urq6urq6urq6urq6urq6urq6v////////////////////////////////8AAAAATGF2YzU2LjQxAAAAAAAAAAAAAAAAJAAAAAAAAAAAASDs90hvAAAAAAAAAAAAAAAAAAAA//MUZAAAAAGkAAAAAAAAA0gAAAAATEFN//MUZAMAAAGkAAAAAAAAA0gAAAAARTMu//MUZAYAAAGkAAAAAAAAA0gAAAAAOTku//MUZAkAAAGkAAAAAAAAA0gAAAAANVVV';
|
||||
|
||||
const useSongUrl = (transcode: TranscodingConfig, current: boolean, song?: Song): string | null => {
|
||||
const useSongUrl = (transcode: TranscodingConfig, current: boolean, song?: Song): null | string => {
|
||||
const prior = useRef(['', '']);
|
||||
|
||||
return useMemo(() => {
|
||||
|
|
@ -94,15 +96,15 @@ const useSongUrl = (transcode: TranscodingConfig, current: boolean, song?: Song)
|
|||
export const AudioPlayer = forwardRef(
|
||||
(
|
||||
{
|
||||
status,
|
||||
playbackStyle,
|
||||
crossfadeStyle,
|
||||
crossfadeDuration,
|
||||
currentPlayer,
|
||||
autoNext,
|
||||
crossfadeDuration,
|
||||
crossfadeStyle,
|
||||
currentPlayer,
|
||||
muted,
|
||||
playbackStyle,
|
||||
player1,
|
||||
player2,
|
||||
muted,
|
||||
status,
|
||||
volume,
|
||||
}: AudioPlayerProps,
|
||||
ref: any,
|
||||
|
|
@ -120,7 +122,7 @@ export const AudioPlayer = forwardRef(
|
|||
const stream1 = useSongUrl(transcode, currentPlayer === 1, player1);
|
||||
const stream2 = useSongUrl(transcode, currentPlayer === 2, player2);
|
||||
|
||||
const { webAudio, setWebAudio } = useWebAudio();
|
||||
const { setWebAudio, webAudio } = useWebAudio();
|
||||
const [player1Source, setPlayer1Source] = useState<MediaElementAudioSourceNode | null>(
|
||||
null,
|
||||
);
|
||||
|
|
@ -415,43 +417,43 @@ export const AudioPlayer = forwardRef(
|
|||
return (
|
||||
<>
|
||||
<ReactPlayer
|
||||
ref={player1Ref}
|
||||
config={{
|
||||
file: { attributes: { crossOrigin: 'anonymous' }, forceAudio: true },
|
||||
}}
|
||||
height={0}
|
||||
muted={muted}
|
||||
playbackRate={playbackSpeed}
|
||||
playing={currentPlayer === 1 && status === PlayerStatus.PLAYING}
|
||||
progressInterval={isTransitioning ? 10 : 250}
|
||||
url={stream1 || EMPTY_SOURCE}
|
||||
volume={webAudio ? 1 : volume}
|
||||
width={0}
|
||||
// If there is no stream url, we do not need to handle when the audio finishes
|
||||
onEnded={stream1 ? handleOnEnded : undefined}
|
||||
onProgress={
|
||||
playbackStyle === PlaybackStyle.GAPLESS ? handleGapless1 : handleCrossfade1
|
||||
}
|
||||
onReady={handlePlayer1Start}
|
||||
playbackRate={playbackSpeed}
|
||||
playing={currentPlayer === 1 && status === PlayerStatus.PLAYING}
|
||||
progressInterval={isTransitioning ? 10 : 250}
|
||||
ref={player1Ref}
|
||||
url={stream1 || EMPTY_SOURCE}
|
||||
volume={webAudio ? 1 : volume}
|
||||
width={0}
|
||||
/>
|
||||
<ReactPlayer
|
||||
ref={player2Ref}
|
||||
config={{
|
||||
file: { attributes: { crossOrigin: 'anonymous' }, forceAudio: true },
|
||||
}}
|
||||
height={0}
|
||||
muted={muted}
|
||||
playbackRate={playbackSpeed}
|
||||
playing={currentPlayer === 2 && status === PlayerStatus.PLAYING}
|
||||
progressInterval={isTransitioning ? 10 : 250}
|
||||
url={stream2 || EMPTY_SOURCE}
|
||||
volume={webAudio ? 1 : volume}
|
||||
width={0}
|
||||
onEnded={stream2 ? handleOnEnded : undefined}
|
||||
onProgress={
|
||||
playbackStyle === PlaybackStyle.GAPLESS ? handleGapless2 : handleCrossfade2
|
||||
}
|
||||
onReady={handlePlayer2Start}
|
||||
playbackRate={playbackSpeed}
|
||||
playing={currentPlayer === 2 && status === PlayerStatus.PLAYING}
|
||||
progressInterval={isTransitioning ? 10 : 250}
|
||||
ref={player2Ref}
|
||||
url={stream2 || EMPTY_SOURCE}
|
||||
volume={webAudio ? 1 : volume}
|
||||
width={0}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/* eslint-disable no-nested-ternary */
|
||||
import type { Dispatch } from 'react';
|
||||
|
||||
import { CrossfadeStyle } from '/@/renderer/types';
|
||||
|
||||
export const gaplessHandler = (args: {
|
||||
|
|
@ -10,7 +10,7 @@ export const gaplessHandler = (args: {
|
|||
nextPlayerRef: any;
|
||||
setIsTransitioning: Dispatch<boolean>;
|
||||
}) => {
|
||||
const { nextPlayerRef, currentTime, duration, isTransitioning, setIsTransitioning, isFlac } =
|
||||
const { currentTime, duration, isFlac, isTransitioning, nextPlayerRef, setIsTransitioning } =
|
||||
args;
|
||||
|
||||
if (!isTransitioning) {
|
||||
|
|
@ -46,17 +46,17 @@ export const crossfadeHandler = (args: {
|
|||
volume: number;
|
||||
}) => {
|
||||
const {
|
||||
currentTime,
|
||||
player,
|
||||
currentPlayer,
|
||||
currentPlayerRef,
|
||||
nextPlayerRef,
|
||||
currentTime,
|
||||
duration,
|
||||
fadeDuration,
|
||||
fadeType,
|
||||
duration,
|
||||
volume,
|
||||
isTransitioning,
|
||||
nextPlayerRef,
|
||||
player,
|
||||
setIsTransitioning,
|
||||
volume,
|
||||
} = args;
|
||||
|
||||
if (!isTransitioning || currentPlayer !== player) {
|
||||
|
|
@ -79,22 +79,18 @@ export const crossfadeHandler = (args: {
|
|||
let percentageOfFadeLeft;
|
||||
let n;
|
||||
switch (fadeType) {
|
||||
case 'equalPower':
|
||||
// https://dsp.stackexchange.com/a/14755
|
||||
percentageOfFadeLeft = (timeLeft / fadeDuration) * 2;
|
||||
currentPlayerVolumeCalculation = Math.sqrt(0.5 * percentageOfFadeLeft) * volume;
|
||||
nextPlayerVolumeCalculation = Math.sqrt(0.5 * (2 - percentageOfFadeLeft)) * volume;
|
||||
break;
|
||||
case 'linear':
|
||||
currentPlayerVolumeCalculation = (timeLeft / fadeDuration) * volume;
|
||||
nextPlayerVolumeCalculation = ((fadeDuration - timeLeft) / fadeDuration) * volume;
|
||||
break;
|
||||
case 'dipped':
|
||||
// https://math.stackexchange.com/a/4622
|
||||
percentageOfFadeLeft = timeLeft / fadeDuration;
|
||||
currentPlayerVolumeCalculation = percentageOfFadeLeft ** 2 * volume;
|
||||
nextPlayerVolumeCalculation = (percentageOfFadeLeft - 1) ** 2 * volume;
|
||||
break;
|
||||
case 'equalPower':
|
||||
// https://dsp.stackexchange.com/a/14755
|
||||
percentageOfFadeLeft = (timeLeft / fadeDuration) * 2;
|
||||
currentPlayerVolumeCalculation = Math.sqrt(0.5 * percentageOfFadeLeft) * volume;
|
||||
nextPlayerVolumeCalculation = Math.sqrt(0.5 * (2 - percentageOfFadeLeft)) * volume;
|
||||
break;
|
||||
case fadeType.match(/constantPower.*/)?.input:
|
||||
// https://math.stackexchange.com/a/26159
|
||||
n =
|
||||
|
|
@ -114,6 +110,10 @@ export const crossfadeHandler = (args: {
|
|||
Math.cos((Math.PI / 4) * ((2 * percentageOfFadeLeft - 1) ** (2 * n + 1) + 1)) *
|
||||
volume;
|
||||
break;
|
||||
case 'linear':
|
||||
currentPlayerVolumeCalculation = (timeLeft / fadeDuration) * volume;
|
||||
nextPlayerVolumeCalculation = ((fadeDuration - timeLeft) / fadeDuration) * volume;
|
||||
break;
|
||||
|
||||
default:
|
||||
currentPlayerVolumeCalculation = (timeLeft / fadeDuration) * volume;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { BadgeProps as MantineBadgeProps } from '@mantine/core';
|
||||
|
||||
import { createPolymorphicComponent, Badge as MantineBadge } from '@mantine/core';
|
||||
import styled from 'styled-components';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import type { Ref } from 'react';
|
||||
import React, { useRef, useCallback, useState, forwardRef } from 'react';
|
||||
import type { ButtonProps as MantineButtonProps, TooltipProps } from '@mantine/core';
|
||||
import { Button as MantineButton, createPolymorphicComponent } from '@mantine/core';
|
||||
import type { Ref } from 'react';
|
||||
|
||||
import { createPolymorphicComponent, Button as MantineButton } from '@mantine/core';
|
||||
import { useTimeout } from '@mantine/hooks';
|
||||
import React, { forwardRef, useCallback, useRef, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Spinner } from '/@/renderer/components/spinner';
|
||||
import { Tooltip } from '/@/renderer/components/tooltip';
|
||||
|
||||
|
|
@ -94,8 +96,8 @@ export const _Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|||
{...tooltip}
|
||||
>
|
||||
<StyledButton
|
||||
ref={ref}
|
||||
loaderPosition="center"
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
<ButtonChildWrapper $loading={props.loading}>{children}</ButtonChildWrapper>
|
||||
|
|
@ -111,8 +113,8 @@ export const _Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|||
|
||||
return (
|
||||
<StyledButton
|
||||
ref={ref}
|
||||
loaderPosition="center"
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
<ButtonChildWrapper $loading={props.loading}>{children}</ButtonChildWrapper>
|
||||
|
|
@ -153,7 +155,7 @@ export const TimeoutButton = ({ timeoutProps, ...props }: HoldButtonProps) => {
|
|||
setIsRunning(false);
|
||||
};
|
||||
|
||||
const { start, clear } = useTimeout(callback, timeoutProps.duration);
|
||||
const { clear, start } = useTimeout(callback, timeoutProps.duration);
|
||||
|
||||
const startTimeout = useCallback(() => {
|
||||
if (isRunning) {
|
||||
|
|
@ -174,8 +176,8 @@ export const TimeoutButton = ({ timeoutProps, ...props }: HoldButtonProps) => {
|
|||
|
||||
return (
|
||||
<Button
|
||||
sx={{ color: 'var(--danger-color)' }}
|
||||
onClick={startTimeout}
|
||||
sx={{ color: 'var(--danger-color)' }}
|
||||
{...props}
|
||||
>
|
||||
{isRunning ? 'Cancel' : props.children}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
import { useCallback } from 'react';
|
||||
import type { CardRoute, CardRow, Play, PlayQueueAddOptions } from '/@/renderer/types';
|
||||
|
||||
import { Center } from '@mantine/core';
|
||||
import { useCallback } from 'react';
|
||||
import { RiAlbumFill } from 'react-icons/ri';
|
||||
import { generatePath, useNavigate } from 'react-router';
|
||||
import { SimpleImg } from 'react-simple-img';
|
||||
import styled from 'styled-components';
|
||||
import type { CardRow, CardRoute, Play, PlayQueueAddOptions } from '/@/renderer/types';
|
||||
import { Skeleton } from '/@/renderer/components/skeleton';
|
||||
import { CardControls } from '/@/renderer/components/card/card-controls';
|
||||
|
||||
import { Album, AlbumArtist, Artist, LibraryItem } from '/@/renderer/api/types';
|
||||
import { CardControls } from '/@/renderer/components/card/card-controls';
|
||||
import { CardRows } from '/@/renderer/components/card/card-rows';
|
||||
import { Skeleton } from '/@/renderer/components/skeleton';
|
||||
|
||||
const CardWrapper = styled.div<{
|
||||
link?: boolean;
|
||||
|
|
@ -104,7 +106,7 @@ const Row = styled.div<{ $secondary?: boolean }>`
|
|||
|
||||
interface BaseGridCardProps {
|
||||
controls: {
|
||||
cardRows: CardRow<Album | Artist | AlbumArtist>[];
|
||||
cardRows: CardRow<Album | AlbumArtist | Artist>[];
|
||||
itemType: LibraryItem;
|
||||
playButtonBehavior: Play;
|
||||
route: CardRoute;
|
||||
|
|
@ -116,14 +118,14 @@ interface BaseGridCardProps {
|
|||
}
|
||||
|
||||
export const AlbumCard = ({
|
||||
controls,
|
||||
data,
|
||||
handlePlayQueueAdd,
|
||||
loading,
|
||||
size,
|
||||
handlePlayQueueAdd,
|
||||
data,
|
||||
controls,
|
||||
}: BaseGridCardProps) => {
|
||||
const navigate = useNavigate();
|
||||
const { itemType, cardRows, route } = controls;
|
||||
const { cardRows, itemType, route } = controls;
|
||||
|
||||
const handleNavigate = useCallback(() => {
|
||||
navigate(
|
||||
|
|
@ -194,9 +196,9 @@ export const AlbumCard = ({
|
|||
<CardWrapper>
|
||||
<StyledCard style={{ alignItems: 'center', display: 'flex' }}>
|
||||
<Skeleton
|
||||
visible
|
||||
height={size}
|
||||
radius="sm"
|
||||
visible
|
||||
width={size}
|
||||
>
|
||||
<ImageSection />
|
||||
|
|
@ -204,10 +206,10 @@ export const AlbumCard = ({
|
|||
<DetailSection style={{ width: '100%' }}>
|
||||
{(cardRows || []).map((_row: CardRow<Album>, index: number) => (
|
||||
<Skeleton
|
||||
visible
|
||||
height={15}
|
||||
my={3}
|
||||
radius="md"
|
||||
visible
|
||||
width={!data ? (index > 0 ? '50%' : '90%') : '100%'}
|
||||
>
|
||||
<Row />
|
||||
|
|
|
|||
|
|
@ -1,21 +1,23 @@
|
|||
import type { MouseEvent } from 'react';
|
||||
import React from 'react';
|
||||
import type { UnstyledButtonProps } from '@mantine/core';
|
||||
import { Group } from '@mantine/core';
|
||||
import { RiPlayFill, RiMore2Fill, RiHeartFill, RiHeartLine } from 'react-icons/ri';
|
||||
import styled from 'styled-components';
|
||||
import { _Button } from '/@/renderer/components/button';
|
||||
import type { PlayQueueAddOptions } from '/@/renderer/types';
|
||||
import { Play } from '/@/renderer/types';
|
||||
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
||||
import type { UnstyledButtonProps } from '@mantine/core';
|
||||
import type { MouseEvent } from 'react';
|
||||
|
||||
import { Group } from '@mantine/core';
|
||||
import React from 'react';
|
||||
import { RiHeartFill, RiHeartLine, RiMore2Fill, RiPlayFill } from 'react-icons/ri';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { LibraryItem } from '/@/renderer/api/types';
|
||||
import { useHandleGeneralContextMenu } from '/@/renderer/features/context-menu/hooks/use-handle-context-menu';
|
||||
import { _Button } from '/@/renderer/components/button';
|
||||
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 { Play } from '/@/renderer/types';
|
||||
|
||||
type PlayButtonType = UnstyledButtonProps & React.ComponentPropsWithoutRef<'button'>;
|
||||
type PlayButtonType = React.ComponentPropsWithoutRef<'button'> & UnstyledButtonProps;
|
||||
|
||||
const PlayButton = styled.button<PlayButtonType>`
|
||||
display: flex;
|
||||
|
|
@ -104,9 +106,9 @@ const FavoriteWrapper = styled.span<{ isFavorite: boolean }>`
|
|||
`;
|
||||
|
||||
export const CardControls = ({
|
||||
handlePlayQueueAdd,
|
||||
itemData,
|
||||
itemType,
|
||||
handlePlayQueueAdd,
|
||||
}: {
|
||||
handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void;
|
||||
itemData: any;
|
||||
|
|
@ -156,14 +158,14 @@ export const CardControls = ({
|
|||
</FavoriteWrapper>
|
||||
</SecondaryButton>
|
||||
<SecondaryButton
|
||||
p={5}
|
||||
sx={{ svg: { fill: 'white !important' } }}
|
||||
variant="subtle"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleContextMenu(e, [itemData]);
|
||||
}}
|
||||
p={5}
|
||||
sx={{ svg: { fill: 'white !important' } }}
|
||||
variant="subtle"
|
||||
>
|
||||
<RiMore2Fill
|
||||
color="white"
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import React from 'react';
|
||||
import formatDuration from 'format-duration';
|
||||
import React from 'react';
|
||||
import { generatePath } from 'react-router';
|
||||
import { Link } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Album, AlbumArtist, Artist, Playlist, Song } from '/@/renderer/api/types';
|
||||
import { Text } from '/@/renderer/components/text';
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
|
|
@ -23,7 +24,7 @@ const Row = styled.div<{ $secondary?: boolean }>`
|
|||
|
||||
interface CardRowsProps {
|
||||
data: any;
|
||||
rows: CardRow<Album>[] | CardRow<Artist>[] | CardRow<AlbumArtist>[];
|
||||
rows: CardRow<Album>[] | CardRow<AlbumArtist>[] | CardRow<Artist>[];
|
||||
}
|
||||
|
||||
export const CardRows = ({ data, rows }: CardRowsProps) => {
|
||||
|
|
@ -33,8 +34,8 @@ export const CardRows = ({ data, rows }: CardRowsProps) => {
|
|||
if (row.arrayProperty && row.route) {
|
||||
return (
|
||||
<Row
|
||||
key={`row-${row.property}-${index}`}
|
||||
$secondary={index > 0}
|
||||
key={`row-${row.property}-${index}`}
|
||||
>
|
||||
{data[row.property].map((item: any, itemIndex: number) => (
|
||||
<React.Fragment key={`${data.id}-${item.id}`}>
|
||||
|
|
@ -55,6 +56,7 @@ export const CardRows = ({ data, rows }: CardRowsProps) => {
|
|||
$noSelect
|
||||
$secondary={index > 0}
|
||||
component={Link}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
overflow="hidden"
|
||||
size={index > 0 ? 'sm' : 'md'}
|
||||
to={generatePath(
|
||||
|
|
@ -69,7 +71,6 @@ export const CardRows = ({ data, rows }: CardRowsProps) => {
|
|||
};
|
||||
}, {}),
|
||||
)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{row.arrayProperty &&
|
||||
(row.format
|
||||
|
|
@ -87,9 +88,9 @@ export const CardRows = ({ data, rows }: CardRowsProps) => {
|
|||
<Row key={`row-${row.property}`}>
|
||||
{data[row.property].map((item: any) => (
|
||||
<Text
|
||||
key={`${data.id}-${item.id}`}
|
||||
$noSelect
|
||||
$secondary={index > 0}
|
||||
key={`${data.id}-${item.id}`}
|
||||
overflow="hidden"
|
||||
size={index > 0 ? 'sm' : 'md'}
|
||||
>
|
||||
|
|
@ -108,6 +109,7 @@ export const CardRows = ({ data, rows }: CardRowsProps) => {
|
|||
$link
|
||||
$noSelect
|
||||
component={Link}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
overflow="hidden"
|
||||
to={generatePath(
|
||||
row.route.route,
|
||||
|
|
@ -118,7 +120,6 @@ export const CardRows = ({ data, rows }: CardRowsProps) => {
|
|||
};
|
||||
}, {}),
|
||||
)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{data && (row.format ? row.format(data) : data[row.property])}
|
||||
</Text>
|
||||
|
|
|
|||
|
|
@ -3,15 +3,16 @@ import { RiAlbumFill, RiPlayListFill, RiUserVoiceFill } from 'react-icons/ri';
|
|||
import { generatePath, Link } from 'react-router-dom';
|
||||
import { SimpleImg } from 'react-simple-img';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
import { Album, AlbumArtist, Artist, LibraryItem } from '/@/renderer/api/types';
|
||||
import { CardRows } from '/@/renderer/components/card';
|
||||
import { Skeleton } from '/@/renderer/components/skeleton';
|
||||
import { GridCardControls } from '/@/renderer/components/virtual-grid/grid-card/grid-card-controls';
|
||||
import { CardRow, PlayQueueAddOptions, Play, CardRoute } from '/@/renderer/types';
|
||||
import { CardRoute, CardRow, Play, PlayQueueAddOptions } from '/@/renderer/types';
|
||||
|
||||
interface BaseGridCardProps {
|
||||
controls: {
|
||||
cardRows: CardRow<Album>[] | CardRow<Artist>[] | CardRow<AlbumArtist>[];
|
||||
cardRows: CardRow<Album>[] | CardRow<AlbumArtist>[] | CardRow<Artist>[];
|
||||
handleFavorite: (options: {
|
||||
id: string[];
|
||||
isFavorite: boolean;
|
||||
|
|
@ -101,8 +102,8 @@ const DetailContainer = styled.div`
|
|||
`;
|
||||
|
||||
export const PosterCard = ({
|
||||
data,
|
||||
controls,
|
||||
data,
|
||||
isLoading,
|
||||
uniqueId,
|
||||
}: BaseGridCardProps & { uniqueId: string }) => {
|
||||
|
|
@ -123,10 +124,10 @@ export const PosterCard = ({
|
|||
case LibraryItem.ALBUM:
|
||||
Placeholder = RiAlbumFill;
|
||||
break;
|
||||
case LibraryItem.ARTIST:
|
||||
case LibraryItem.ALBUM_ARTIST:
|
||||
Placeholder = RiUserVoiceFill;
|
||||
break;
|
||||
case LibraryItem.ALBUM_ARTIST:
|
||||
case LibraryItem.ARTIST:
|
||||
Placeholder = RiUserVoiceFill;
|
||||
break;
|
||||
case LibraryItem.PLAYLIST:
|
||||
|
|
@ -184,8 +185,8 @@ export const PosterCard = ({
|
|||
return (
|
||||
<PosterCardContainer key={`placeholder-${uniqueId}-${data.id}`}>
|
||||
<Skeleton
|
||||
visible
|
||||
radius="sm"
|
||||
visible
|
||||
>
|
||||
<ImageContainerSkeleton />
|
||||
</Skeleton>
|
||||
|
|
@ -193,10 +194,10 @@ export const PosterCard = ({
|
|||
<Stack spacing="sm">
|
||||
{(controls?.cardRows || []).map((row, index) => (
|
||||
<Skeleton
|
||||
key={`${index}-${row.arrayProperty}`}
|
||||
visible
|
||||
height={14}
|
||||
key={`${index}-${row.arrayProperty}`}
|
||||
radius="sm"
|
||||
visible
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { CheckboxProps, Checkbox as MantineCheckbox } from '@mantine/core';
|
||||
import { forwardRef } from 'react';
|
||||
import { Checkbox as MantineCheckbox, CheckboxProps } from '@mantine/core';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledCheckbox = styled(MantineCheckbox)`
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { ComponentPropsWithoutRef, forwardRef, ReactNode, Ref } from 'react';
|
||||
import { Box, Group, UnstyledButton, UnstyledButtonProps } from '@mantine/core';
|
||||
import { motion, Variants } from 'framer-motion';
|
||||
import { ComponentPropsWithoutRef, forwardRef, ReactNode, Ref } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
interface ContextMenuProps {
|
||||
|
|
@ -61,11 +61,11 @@ export const ContextMenuButton = forwardRef(
|
|||
(
|
||||
{
|
||||
children,
|
||||
rightIcon,
|
||||
leftIcon,
|
||||
rightIcon,
|
||||
...props
|
||||
}: UnstyledButtonProps &
|
||||
ComponentPropsWithoutRef<'button'> & {
|
||||
}: ComponentPropsWithoutRef<'button'> &
|
||||
UnstyledButtonProps & {
|
||||
leftIcon?: ReactNode;
|
||||
rightIcon?: ReactNode;
|
||||
},
|
||||
|
|
@ -74,11 +74,11 @@ export const ContextMenuButton = forwardRef(
|
|||
return (
|
||||
<StyledContextMenuButton
|
||||
{...props}
|
||||
key={props.key}
|
||||
ref={ref}
|
||||
as="button"
|
||||
disabled={props.disabled}
|
||||
key={props.key}
|
||||
onClick={props.onClick}
|
||||
ref={ref}
|
||||
>
|
||||
<Group position="apart">
|
||||
<Group spacing="md">
|
||||
|
|
@ -108,14 +108,14 @@ const variants: Variants = {
|
|||
};
|
||||
|
||||
export const ContextMenu = forwardRef(
|
||||
({ yPos, xPos, minWidth, maxWidth, children }: ContextMenuProps, ref: Ref<HTMLDivElement>) => {
|
||||
({ children, maxWidth, minWidth, xPos, yPos }: ContextMenuProps, ref: Ref<HTMLDivElement>) => {
|
||||
return (
|
||||
<ContextMenuContainer
|
||||
ref={ref}
|
||||
animate="open"
|
||||
initial="closed"
|
||||
maxWidth={maxWidth}
|
||||
minWidth={minWidth}
|
||||
ref={ref}
|
||||
variants={variants}
|
||||
xPos={xPos}
|
||||
yPos={yPos}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { DatePickerProps as MantineDatePickerProps } from '@mantine/dates';
|
||||
|
||||
import { DatePicker as MantineDatePicker } from '@mantine/dates';
|
||||
import styled from 'styled-components';
|
||||
|
||||
|
|
@ -34,7 +35,7 @@ const StyledDatePicker = styled(MantineDatePicker)<DatePickerProps>`
|
|||
}
|
||||
`;
|
||||
|
||||
export const DatePicker = ({ width, maxWidth, ...props }: DatePickerProps) => {
|
||||
export const DatePicker = ({ maxWidth, width, ...props }: DatePickerProps) => {
|
||||
return (
|
||||
<StyledDatePicker
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { DialogProps as MantineDialogProps } from '@mantine/core';
|
||||
|
||||
import { Dialog as MantineDialog } from '@mantine/core';
|
||||
import styled from 'styled-components';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,24 +1,25 @@
|
|||
import { ReactNode } from 'react';
|
||||
import type {
|
||||
MenuProps as MantineMenuProps,
|
||||
MenuItemProps as MantineMenuItemProps,
|
||||
MenuLabelProps as MantineMenuLabelProps,
|
||||
MenuDividerProps as MantineMenuDividerProps,
|
||||
MenuDropdownProps as MantineMenuDropdownProps,
|
||||
MenuItemProps as MantineMenuItemProps,
|
||||
MenuLabelProps as MantineMenuLabelProps,
|
||||
MenuProps as MantineMenuProps,
|
||||
} from '@mantine/core';
|
||||
import { Menu as MantineMenu, createPolymorphicComponent } from '@mantine/core';
|
||||
|
||||
import { createPolymorphicComponent, Menu as MantineMenu } from '@mantine/core';
|
||||
import { ReactNode } from 'react';
|
||||
import { RiArrowLeftSFill } from 'react-icons/ri';
|
||||
import styled from 'styled-components';
|
||||
|
||||
type MenuProps = MantineMenuProps;
|
||||
type MenuLabelProps = MantineMenuLabelProps;
|
||||
type MenuDividerProps = MantineMenuDividerProps;
|
||||
type MenuDropdownProps = MantineMenuDropdownProps;
|
||||
interface MenuItemProps extends MantineMenuItemProps {
|
||||
$danger?: boolean;
|
||||
$isActive?: boolean;
|
||||
children: ReactNode;
|
||||
}
|
||||
type MenuDividerProps = MantineMenuDividerProps;
|
||||
type MenuDropdownProps = MantineMenuDropdownProps;
|
||||
type MenuLabelProps = MantineMenuLabelProps;
|
||||
type MenuProps = MantineMenuProps;
|
||||
|
||||
const StyledMenu = styled(MantineMenu)<MenuProps>``;
|
||||
|
||||
|
|
@ -81,7 +82,6 @@ const StyledMenuDivider = styled(MantineMenu.Divider)`
|
|||
export const DropdownMenu = ({ children, ...props }: MenuProps) => {
|
||||
return (
|
||||
<StyledMenu
|
||||
withinPortal
|
||||
styles={{
|
||||
dropdown: {
|
||||
filter: 'drop-shadow(0 0 5px rgb(0, 0, 0, 50%))',
|
||||
|
|
@ -90,6 +90,7 @@ export const DropdownMenu = ({ children, ...props }: MenuProps) => {
|
|||
transitionProps={{
|
||||
transition: 'fade',
|
||||
}}
|
||||
withinPortal
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
|
|
@ -101,7 +102,7 @@ const MenuLabel = ({ children, ...props }: MenuLabelProps) => {
|
|||
return <StyledMenuLabel {...props}>{children}</StyledMenuLabel>;
|
||||
};
|
||||
|
||||
const pMenuItem = ({ $isActive, $danger, children, ...props }: MenuItemProps) => {
|
||||
const pMenuItem = ({ $danger, $isActive, children, ...props }: MenuItemProps) => {
|
||||
return (
|
||||
<StyledMenuItem
|
||||
$danger={$danger}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,22 @@
|
|||
import type { MouseEvent } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { Group, Image, Stack } from '@mantine/core';
|
||||
import type { Variants } from 'framer-motion';
|
||||
import type { MouseEvent } from 'react';
|
||||
|
||||
import { Group, Image, Stack } from '@mantine/core';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RiArrowLeftSLine, RiArrowRightSLine } from 'react-icons/ri';
|
||||
import { Link, generatePath } from 'react-router-dom';
|
||||
import { generatePath, Link } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Album, LibraryItem } from '/@/renderer/api/types';
|
||||
import { Badge } from '/@/renderer/components/badge';
|
||||
import { Button } from '/@/renderer/components/button';
|
||||
import { TextTitle } from '/@/renderer/components/text-title';
|
||||
import { Badge } from '/@/renderer/components/badge';
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { usePlayQueueAdd } from '/@/renderer/features/player/hooks/use-playqueue-add';
|
||||
import { Play } from '/@/renderer/types';
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { usePlayButtonBehavior } from '/@/renderer/store';
|
||||
import { Play } from '/@/renderer/types';
|
||||
|
||||
const Carousel = styled(motion.div)`
|
||||
position: relative;
|
||||
|
|
@ -152,11 +154,11 @@ export const FeatureCarousel = ({ data }: FeatureCarouselProps) => {
|
|||
>
|
||||
{data && (
|
||||
<Carousel
|
||||
key={`image-${itemIndex}`}
|
||||
animate="animate"
|
||||
custom={direction}
|
||||
exit="exit"
|
||||
initial="initial"
|
||||
key={`image-${itemIndex}`}
|
||||
variants={variants}
|
||||
>
|
||||
<Grid>
|
||||
|
|
@ -218,9 +220,6 @@ export const FeatureCarousel = ({ data }: FeatureCarouselProps) => {
|
|||
</Group>
|
||||
<Group position="apart">
|
||||
<Button
|
||||
size="lg"
|
||||
style={{ borderRadius: '5rem' }}
|
||||
variant="outline"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
|
@ -234,6 +233,9 @@ export const FeatureCarousel = ({ data }: FeatureCarouselProps) => {
|
|||
playType,
|
||||
});
|
||||
}}
|
||||
size="lg"
|
||||
style={{ borderRadius: '5rem' }}
|
||||
variant="outline"
|
||||
>
|
||||
{t(
|
||||
playType === Play.NOW
|
||||
|
|
@ -246,18 +248,18 @@ export const FeatureCarousel = ({ data }: FeatureCarouselProps) => {
|
|||
</Button>
|
||||
<Group spacing="sm">
|
||||
<Button
|
||||
onClick={handlePrevious}
|
||||
radius="lg"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={handlePrevious}
|
||||
>
|
||||
<RiArrowLeftSLine size="2rem" />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleNext}
|
||||
radius="lg"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={handleNext}
|
||||
>
|
||||
<RiArrowRightSLine size="2rem" />
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { Group, Stack } from '@mantine/core';
|
||||
import throttle from 'lodash/throttle';
|
||||
import {
|
||||
isValidElement,
|
||||
memo,
|
||||
|
|
@ -9,14 +11,13 @@ import {
|
|||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Group, Stack } from '@mantine/core';
|
||||
import throttle from 'lodash/throttle';
|
||||
import { RiArrowLeftSLine, RiArrowRightSLine } from 'react-icons/ri';
|
||||
import styled from 'styled-components';
|
||||
import { SwiperOptions, Virtual } from 'swiper';
|
||||
import 'swiper/css';
|
||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||
import { Swiper as SwiperCore } from 'swiper/types';
|
||||
|
||||
import { Album, AlbumArtist, Artist, LibraryItem, RelatedArtist } from '/@/renderer/api/types';
|
||||
import { Button } from '/@/renderer/components/button';
|
||||
import { PosterCard } from '/@/renderer/components/card/poster-card';
|
||||
|
|
@ -44,14 +45,14 @@ const CarouselContainer = styled(Stack)`
|
|||
interface TitleProps {
|
||||
handleNext?: () => void;
|
||||
handlePrev?: () => void;
|
||||
label?: string | ReactNode;
|
||||
label?: ReactNode | string;
|
||||
pagination: {
|
||||
hasNextPage: boolean;
|
||||
hasPreviousPage: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
const Title = ({ label, handleNext, handlePrev, pagination }: TitleProps) => {
|
||||
const Title = ({ handleNext, handlePrev, label, pagination }: TitleProps) => {
|
||||
return (
|
||||
<Group position="apart">
|
||||
{isValidElement(label) ? (
|
||||
|
|
@ -69,18 +70,18 @@ const Title = ({ label, handleNext, handlePrev, pagination }: TitleProps) => {
|
|||
<Button
|
||||
compact
|
||||
disabled={!pagination.hasPreviousPage}
|
||||
onClick={handlePrev}
|
||||
size="lg"
|
||||
variant="default"
|
||||
onClick={handlePrev}
|
||||
>
|
||||
<RiArrowLeftSLine />
|
||||
</Button>
|
||||
<Button
|
||||
compact
|
||||
disabled={!pagination.hasNextPage}
|
||||
onClick={handleNext}
|
||||
size="lg"
|
||||
variant="default"
|
||||
onClick={handleNext}
|
||||
>
|
||||
<RiArrowRightSLine />
|
||||
</Button>
|
||||
|
|
@ -90,7 +91,7 @@ const Title = ({ label, handleNext, handlePrev, pagination }: TitleProps) => {
|
|||
};
|
||||
|
||||
export interface SwiperGridCarouselProps {
|
||||
cardRows: CardRow<Album>[] | CardRow<Artist>[] | CardRow<AlbumArtist>[];
|
||||
cardRows: CardRow<Album>[] | CardRow<AlbumArtist>[] | CardRow<Artist>[];
|
||||
data: Album[] | AlbumArtist[] | Artist[] | RelatedArtist[] | undefined;
|
||||
isLoading?: boolean;
|
||||
itemType: LibraryItem;
|
||||
|
|
@ -100,7 +101,7 @@ export interface SwiperGridCarouselProps {
|
|||
children?: ReactNode;
|
||||
hasPagination?: boolean;
|
||||
icon?: ReactNode;
|
||||
label: string | ReactNode;
|
||||
label: ReactNode | string;
|
||||
};
|
||||
uniqueId: string;
|
||||
}
|
||||
|
|
@ -108,15 +109,15 @@ export interface SwiperGridCarouselProps {
|
|||
export const SwiperGridCarousel = ({
|
||||
cardRows,
|
||||
data,
|
||||
isLoading,
|
||||
itemType,
|
||||
route,
|
||||
swiperProps,
|
||||
title,
|
||||
isLoading,
|
||||
uniqueId,
|
||||
}: SwiperGridCarouselProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const swiperRef = useRef<SwiperCore | any>(null);
|
||||
const swiperRef = useRef<any | SwiperCore>(null);
|
||||
const playButtonBehavior = usePlayButtonBehavior();
|
||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||
const [slideCount, setSlideCount] = useState(4);
|
||||
|
|
@ -140,7 +141,7 @@ export const SwiperGridCarousel = ({
|
|||
itemType: LibraryItem;
|
||||
serverId: string;
|
||||
}) => {
|
||||
const { id, itemType, isFavorite, serverId } = options;
|
||||
const { id, isFavorite, itemType, serverId } = options;
|
||||
if (isFavorite) {
|
||||
deleteFavoriteMutation.mutate({
|
||||
query: {
|
||||
|
|
@ -205,7 +206,7 @@ export const SwiperGridCarousel = ({
|
|||
}, [slideCount, swiperProps?.slidesPerView]);
|
||||
|
||||
const handleOnSlideChange = useCallback((e: SwiperCore) => {
|
||||
const { slides, isEnd, isBeginning, params } = e;
|
||||
const { isBeginning, isEnd, params, slides } = e;
|
||||
if (isEnd || isBeginning) return;
|
||||
|
||||
const slideCount = (params.slidesPerView as number | undefined) || 4;
|
||||
|
|
@ -216,7 +217,7 @@ export const SwiperGridCarousel = ({
|
|||
}, []);
|
||||
|
||||
const handleOnZoomChange = useCallback((e: SwiperCore) => {
|
||||
const { slides, isEnd, isBeginning, params } = e;
|
||||
const { isBeginning, isEnd, params, slides } = e;
|
||||
if (isEnd || isBeginning) return;
|
||||
|
||||
const slideCount = (params.slidesPerView as number | undefined) || 4;
|
||||
|
|
@ -227,7 +228,7 @@ export const SwiperGridCarousel = ({
|
|||
}, []);
|
||||
|
||||
const handleOnReachEnd = useCallback((e: SwiperCore) => {
|
||||
const { slides, params } = e;
|
||||
const { params, slides } = e;
|
||||
|
||||
const slideCount = (params.slidesPerView as number | undefined) || 4;
|
||||
setPagination({
|
||||
|
|
@ -237,7 +238,7 @@ export const SwiperGridCarousel = ({
|
|||
}, []);
|
||||
|
||||
const handleOnReachBeginning = useCallback((e: SwiperCore) => {
|
||||
const { slides, params } = e;
|
||||
const { params, slides } = e;
|
||||
|
||||
const slideCount = (params.slidesPerView as number | undefined) || 4;
|
||||
setPagination({
|
||||
|
|
@ -279,8 +280,8 @@ export const SwiperGridCarousel = ({
|
|||
|
||||
return (
|
||||
<CarouselContainer
|
||||
ref={containerRef}
|
||||
className="grid-carousel"
|
||||
ref={containerRef}
|
||||
spacing="md"
|
||||
>
|
||||
{title ? (
|
||||
|
|
@ -292,12 +293,7 @@ export const SwiperGridCarousel = ({
|
|||
/>
|
||||
) : null}
|
||||
<Swiper
|
||||
ref={swiperRef}
|
||||
resizeObserver
|
||||
modules={[Virtual]}
|
||||
slidesPerView={slideCount}
|
||||
spaceBetween={20}
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
onBeforeInit={(swiper) => {
|
||||
swiperRef.current = swiper;
|
||||
}}
|
||||
|
|
@ -305,6 +301,11 @@ export const SwiperGridCarousel = ({
|
|||
onReachEnd={handleOnReachEnd}
|
||||
onSlideChange={handleOnSlideChange}
|
||||
onZoomChange={handleOnZoomChange}
|
||||
ref={swiperRef}
|
||||
resizeObserver
|
||||
slidesPerView={slideCount}
|
||||
spaceBetween={20}
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
{...swiperProps}
|
||||
>
|
||||
{slides.map((slideContent, index) => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { HoverCard as MantineHoverCard, HoverCardProps } from '@mantine/core';
|
||||
import { HoverCardProps, HoverCard as MantineHoverCard } from '@mantine/core';
|
||||
|
||||
export const HoverCard = ({ children, ...props }: HoverCardProps) => {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,23 +1,30 @@
|
|||
import React, { forwardRef } from 'react';
|
||||
import type {
|
||||
TextInputProps as MantineTextInputProps,
|
||||
NumberInputProps as MantineNumberInputProps,
|
||||
PasswordInputProps as MantinePasswordInputProps,
|
||||
FileInputProps as MantineFileInputProps,
|
||||
JsonInputProps as MantineJsonInputProps,
|
||||
NumberInputProps as MantineNumberInputProps,
|
||||
PasswordInputProps as MantinePasswordInputProps,
|
||||
TextareaProps as MantineTextareaProps,
|
||||
TextInputProps as MantineTextInputProps,
|
||||
} from '@mantine/core';
|
||||
|
||||
import {
|
||||
TextInput as MantineTextInput,
|
||||
NumberInput as MantineNumberInput,
|
||||
PasswordInput as MantinePasswordInput,
|
||||
FileInput as MantineFileInput,
|
||||
JsonInput as MantineJsonInput,
|
||||
NumberInput as MantineNumberInput,
|
||||
PasswordInput as MantinePasswordInput,
|
||||
Textarea as MantineTextarea,
|
||||
TextInput as MantineTextInput,
|
||||
} from '@mantine/core';
|
||||
import React, { forwardRef } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
interface TextInputProps extends MantineTextInputProps {
|
||||
interface FileInputProps extends MantineFileInputProps {
|
||||
children?: React.ReactNode;
|
||||
maxWidth?: number | string;
|
||||
width?: number | string;
|
||||
}
|
||||
|
||||
interface JsonInputProps extends MantineJsonInputProps {
|
||||
children?: React.ReactNode;
|
||||
maxWidth?: number | string;
|
||||
width?: number | string;
|
||||
|
|
@ -35,24 +42,18 @@ interface PasswordInputProps extends MantinePasswordInputProps {
|
|||
width?: number | string;
|
||||
}
|
||||
|
||||
interface FileInputProps extends MantineFileInputProps {
|
||||
children?: React.ReactNode;
|
||||
maxWidth?: number | string;
|
||||
width?: number | string;
|
||||
}
|
||||
|
||||
interface JsonInputProps extends MantineJsonInputProps {
|
||||
children?: React.ReactNode;
|
||||
maxWidth?: number | string;
|
||||
width?: number | string;
|
||||
}
|
||||
|
||||
interface TextareaProps extends MantineTextareaProps {
|
||||
children?: React.ReactNode;
|
||||
maxWidth?: number | string;
|
||||
width?: number | string;
|
||||
}
|
||||
|
||||
interface TextInputProps extends MantineTextInputProps {
|
||||
children?: React.ReactNode;
|
||||
maxWidth?: number | string;
|
||||
width?: number | string;
|
||||
}
|
||||
|
||||
const StyledTextInput = styled(MantineTextInput)<TextInputProps>`
|
||||
& .mantine-TextInput-wrapper {
|
||||
border-color: var(--primary-color);
|
||||
|
|
@ -276,7 +277,7 @@ const StyledTextarea = styled(MantineTextarea)<TextareaProps>`
|
|||
`;
|
||||
|
||||
export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
|
||||
({ children, width, maxWidth, ...props }: TextInputProps, ref) => {
|
||||
({ children, maxWidth, width, ...props }: TextInputProps, ref) => {
|
||||
return (
|
||||
<StyledTextInput
|
||||
ref={ref}
|
||||
|
|
@ -291,11 +292,11 @@ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
|
|||
);
|
||||
|
||||
export const NumberInput = forwardRef<HTMLInputElement, NumberInputProps>(
|
||||
({ children, width, maxWidth, ...props }: NumberInputProps, ref) => {
|
||||
({ children, maxWidth, width, ...props }: NumberInputProps, ref) => {
|
||||
return (
|
||||
<StyledNumberInput
|
||||
ref={ref}
|
||||
hideControls
|
||||
ref={ref}
|
||||
spellCheck={false}
|
||||
{...props}
|
||||
sx={{ maxWidth, width }}
|
||||
|
|
@ -307,7 +308,7 @@ export const NumberInput = forwardRef<HTMLInputElement, NumberInputProps>(
|
|||
);
|
||||
|
||||
export const PasswordInput = forwardRef<HTMLInputElement, PasswordInputProps>(
|
||||
({ children, width, maxWidth, ...props }: PasswordInputProps, ref) => {
|
||||
({ children, maxWidth, width, ...props }: PasswordInputProps, ref) => {
|
||||
return (
|
||||
<StyledPasswordInput
|
||||
ref={ref}
|
||||
|
|
@ -321,7 +322,7 @@ export const PasswordInput = forwardRef<HTMLInputElement, PasswordInputProps>(
|
|||
);
|
||||
|
||||
export const FileInput = forwardRef<HTMLButtonElement, FileInputProps>(
|
||||
({ children, width, maxWidth, ...props }: FileInputProps, ref) => {
|
||||
({ children, maxWidth, width, ...props }: FileInputProps, ref) => {
|
||||
return (
|
||||
<StyledFileInput
|
||||
ref={ref}
|
||||
|
|
@ -340,7 +341,7 @@ export const FileInput = forwardRef<HTMLButtonElement, FileInputProps>(
|
|||
);
|
||||
|
||||
export const JsonInput = forwardRef<HTMLTextAreaElement, JsonInputProps>(
|
||||
({ children, width, maxWidth, ...props }: JsonInputProps, ref) => {
|
||||
({ children, maxWidth, width, ...props }: JsonInputProps, ref) => {
|
||||
return (
|
||||
<StyledJsonInput
|
||||
ref={ref}
|
||||
|
|
@ -354,7 +355,7 @@ export const JsonInput = forwardRef<HTMLTextAreaElement, JsonInputProps>(
|
|||
);
|
||||
|
||||
export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
({ children, width, maxWidth, ...props }: TextareaProps, ref) => {
|
||||
({ children, maxWidth, width, ...props }: TextareaProps, ref) => {
|
||||
return (
|
||||
<StyledTextarea
|
||||
ref={ref}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import React, { ReactNode } from 'react';
|
||||
import {
|
||||
ModalProps as MantineModalProps,
|
||||
Stack,
|
||||
Modal as MantineModal,
|
||||
Flex,
|
||||
Group,
|
||||
Modal as MantineModal,
|
||||
ModalProps as MantineModalProps,
|
||||
Stack,
|
||||
} from '@mantine/core';
|
||||
import { closeAllModals, ContextModalProps } from '@mantine/modals';
|
||||
import React, { ReactNode } from 'react';
|
||||
|
||||
import { Button } from '/@/renderer/components/button';
|
||||
|
||||
export interface ModalProps extends Omit<MantineModalProps, 'onClose'> {
|
||||
|
|
@ -59,12 +60,12 @@ interface ConfirmModalProps {
|
|||
}
|
||||
|
||||
export const ConfirmModal = ({
|
||||
loading,
|
||||
children,
|
||||
disabled,
|
||||
labels,
|
||||
loading,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
children,
|
||||
}: ConfirmModalProps) => {
|
||||
const handleCancel = () => {
|
||||
if (onCancel) {
|
||||
|
|
@ -80,16 +81,16 @@ export const ConfirmModal = ({
|
|||
<Group position="right">
|
||||
<Button
|
||||
data-focus
|
||||
variant="default"
|
||||
onClick={handleCancel}
|
||||
variant="default"
|
||||
>
|
||||
{labels?.cancel ? labels.cancel : 'Cancel'}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
loading={loading}
|
||||
variant="filled"
|
||||
onClick={onConfirm}
|
||||
variant="filled"
|
||||
>
|
||||
{labels?.confirm ? labels.confirm : 'Confirm'}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { ReactNode } from 'react';
|
||||
import { Flex, Group } from '@mantine/core';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export const Option = ({ children }: any) => {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { Flex, FlexProps } from '@mantine/core';
|
|||
import { AnimatePresence, motion, Variants } from 'framer-motion';
|
||||
import { ReactNode, useRef } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { useShouldPadTitlebar, useTheme } from '/@/renderer/hooks';
|
||||
import { useWindowSettings } from '/@/renderer/store/settings.store';
|
||||
import { Platform } from '/@/renderer/types';
|
||||
|
|
@ -49,7 +50,7 @@ const BackgroundImage = styled.div<{ $background: string }>`
|
|||
background: ${(props) => props.$background || 'var(--titlebar-bg)'};
|
||||
`;
|
||||
|
||||
const BackgroundImageOverlay = styled.div<{ theme: 'light' | 'dark' }>`
|
||||
const BackgroundImageOverlay = styled.div<{ theme: 'dark' | 'light' }>`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
|
@ -63,7 +64,7 @@ const BackgroundImageOverlay = styled.div<{ theme: 'light' | 'dark' }>`
|
|||
`;
|
||||
|
||||
export interface PageHeaderProps
|
||||
extends Omit<FlexProps, 'onAnimationStart' | 'onDragStart' | 'onDragEnd' | 'onDrag'> {
|
||||
extends Omit<FlexProps, 'onAnimationStart' | 'onDrag' | 'onDragEnd' | 'onDragStart'> {
|
||||
animated?: boolean;
|
||||
backgroundColor?: string;
|
||||
children?: ReactNode;
|
||||
|
|
@ -93,11 +94,11 @@ const variants: Variants = {
|
|||
|
||||
export const PageHeader = ({
|
||||
animated,
|
||||
position,
|
||||
height,
|
||||
backgroundColor,
|
||||
isHidden,
|
||||
children,
|
||||
height,
|
||||
isHidden,
|
||||
position,
|
||||
...props
|
||||
}: PageHeaderProps) => {
|
||||
const ref = useRef(null);
|
||||
|
|
@ -108,9 +109,9 @@ export const PageHeader = ({
|
|||
return (
|
||||
<>
|
||||
<Container
|
||||
ref={ref}
|
||||
$height={height}
|
||||
$position={position}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
<Header
|
||||
|
|
@ -132,7 +133,7 @@ export const PageHeader = ({
|
|||
{backgroundColor && (
|
||||
<>
|
||||
<BackgroundImage $background={backgroundColor || 'var(--titlebar-bg)'} />
|
||||
<BackgroundImageOverlay theme={theme as 'light' | 'dark'} />
|
||||
<BackgroundImageOverlay theme={theme as 'dark' | 'light'} />
|
||||
</>
|
||||
)}
|
||||
</Container>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { ReactNode } from 'react';
|
||||
import type { PaperProps as MantinePaperProps } from '@mantine/core';
|
||||
|
||||
import { Paper as MantinePaper } from '@mantine/core';
|
||||
import { ReactNode } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export interface PaperProps extends MantinePaperProps {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import type {
|
||||
PopoverProps as MantinePopoverProps,
|
||||
PopoverDropdownProps as MantinePopoverDropdownProps,
|
||||
PopoverProps as MantinePopoverProps,
|
||||
} from '@mantine/core';
|
||||
|
||||
import { Popover as MantinePopover } from '@mantine/core';
|
||||
import styled from 'styled-components';
|
||||
|
||||
type PopoverProps = MantinePopoverProps;
|
||||
type PopoverDropdownProps = MantinePopoverDropdownProps;
|
||||
type PopoverProps = MantinePopoverProps;
|
||||
|
||||
const StyledPopover = styled(MantinePopover)``;
|
||||
|
||||
|
|
@ -21,13 +22,13 @@ const StyledDropdown = styled(MantinePopover.Dropdown)<PopoverDropdownProps>`
|
|||
export const Popover = ({ children, ...props }: PopoverProps) => {
|
||||
return (
|
||||
<StyledPopover
|
||||
withinPortal
|
||||
styles={{
|
||||
dropdown: {
|
||||
filter: 'drop-shadow(0 0 5px rgb(0, 0, 0, 50%))',
|
||||
},
|
||||
}}
|
||||
transitionProps={{ transition: 'fade' }}
|
||||
withinPortal
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import { Group, Stack } from '@mantine/core';
|
||||
import { Select } from '/@/renderer/components/select';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { RiAddFill, RiAddLine, RiDeleteBinFill, RiMore2Line, RiRestartLine } from 'react-icons/ri';
|
||||
|
||||
import i18n from '/@/i18n/i18n';
|
||||
import { Button } from '/@/renderer/components/button';
|
||||
import { DropdownMenu } from '/@/renderer/components/dropdown-menu';
|
||||
import { QueryBuilderOption } from '/@/renderer/components/query-builder/query-builder-option';
|
||||
import { Select } from '/@/renderer/components/select';
|
||||
import { QueryBuilderGroup, QueryBuilderRule } from '/@/renderer/types';
|
||||
import i18n from '/@/i18n/i18n';
|
||||
|
||||
const FILTER_GROUP_OPTIONS_DATA = [
|
||||
{
|
||||
|
|
@ -63,22 +64,22 @@ interface QueryBuilderProps {
|
|||
|
||||
export const QueryBuilder = ({
|
||||
data,
|
||||
filters,
|
||||
groupIndex,
|
||||
level,
|
||||
onAddRule,
|
||||
onDeleteRuleGroup,
|
||||
onDeleteRule,
|
||||
onAddRuleGroup,
|
||||
onChangeType,
|
||||
onChangeField,
|
||||
operators,
|
||||
onChangeOperator,
|
||||
onChangeType,
|
||||
onChangeValue,
|
||||
onClearFilters,
|
||||
onDeleteRule,
|
||||
onDeleteRuleGroup,
|
||||
onResetFilters,
|
||||
operators,
|
||||
playlists,
|
||||
groupIndex,
|
||||
uniqueId,
|
||||
filters,
|
||||
}: QueryBuilderProps) => {
|
||||
const handleAddRule = () => {
|
||||
onAddRule({ groupIndex, level });
|
||||
|
|
@ -92,7 +93,7 @@ export const QueryBuilder = ({
|
|||
onDeleteRuleGroup({ groupIndex, level, uniqueId });
|
||||
};
|
||||
|
||||
const handleChangeType = (value: string | null) => {
|
||||
const handleChangeType = (value: null | string) => {
|
||||
onChangeType({ groupIndex, level, value });
|
||||
};
|
||||
|
||||
|
|
@ -105,17 +106,17 @@ export const QueryBuilder = ({
|
|||
<Select
|
||||
data={FILTER_GROUP_OPTIONS_DATA}
|
||||
maxWidth={175}
|
||||
onChange={handleChangeType}
|
||||
size="sm"
|
||||
value={data.type}
|
||||
width="20%"
|
||||
onChange={handleChangeType}
|
||||
/>
|
||||
<Button
|
||||
onClick={handleAddRule}
|
||||
px={5}
|
||||
size="sm"
|
||||
tooltip={{ label: 'Add rule' }}
|
||||
variant="default"
|
||||
onClick={handleAddRule}
|
||||
>
|
||||
<RiAddLine size={20} />
|
||||
</Button>
|
||||
|
|
@ -170,10 +171,10 @@ export const QueryBuilder = ({
|
|||
<AnimatePresence initial={false}>
|
||||
{data?.rules?.map((rule: QueryBuilderRule) => (
|
||||
<motion.div
|
||||
key={rule.uniqueId}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -25 }}
|
||||
initial={{ opacity: 0, x: -25 }}
|
||||
key={rule.uniqueId}
|
||||
transition={{ duration: 0.2, ease: 'easeInOut' }}
|
||||
>
|
||||
<QueryBuilderOption
|
||||
|
|
@ -182,12 +183,12 @@ export const QueryBuilder = ({
|
|||
groupIndex={groupIndex || []}
|
||||
level={level}
|
||||
noRemove={data?.rules?.length === 1}
|
||||
operators={operators}
|
||||
selectData={playlists}
|
||||
onChangeField={onChangeField}
|
||||
onChangeOperator={onChangeOperator}
|
||||
onChangeValue={onChangeValue}
|
||||
onDeleteRule={onDeleteRule}
|
||||
operators={operators}
|
||||
selectData={playlists}
|
||||
/>
|
||||
</motion.div>
|
||||
))}
|
||||
|
|
@ -196,10 +197,10 @@ export const QueryBuilder = ({
|
|||
<AnimatePresence initial={false}>
|
||||
{data.group?.map((group: QueryBuilderGroup, index: number) => (
|
||||
<motion.div
|
||||
key={group.uniqueId}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -25 }}
|
||||
initial={{ opacity: 0, x: -25 }}
|
||||
key={group.uniqueId}
|
||||
transition={{ duration: 0.2, ease: 'easeInOut' }}
|
||||
>
|
||||
<QueryBuilder
|
||||
|
|
@ -207,9 +208,6 @@ export const QueryBuilder = ({
|
|||
filters={filters}
|
||||
groupIndex={[...(groupIndex || []), index]}
|
||||
level={level + 1}
|
||||
operators={operators}
|
||||
playlists={playlists}
|
||||
uniqueId={group.uniqueId}
|
||||
onAddRule={onAddRule}
|
||||
onAddRuleGroup={onAddRuleGroup}
|
||||
onChangeField={onChangeField}
|
||||
|
|
@ -220,6 +218,9 @@ export const QueryBuilder = ({
|
|||
onDeleteRule={onDeleteRule}
|
||||
onDeleteRuleGroup={onDeleteRuleGroup}
|
||||
onResetFilters={onResetFilters}
|
||||
operators={operators}
|
||||
playlists={playlists}
|
||||
uniqueId={group.uniqueId}
|
||||
/>
|
||||
</motion.div>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { Group } from '@mantine/core';
|
||||
import { useState } from 'react';
|
||||
import { RiSubtractLine } from 'react-icons/ri';
|
||||
|
||||
import { Button } from '/@/renderer/components/button';
|
||||
import { NumberInput, TextInput } from '/@/renderer/components/input';
|
||||
import { Select } from '/@/renderer/components/select';
|
||||
|
|
@ -31,62 +32,10 @@ interface QueryOptionProps {
|
|||
selectData?: { label: string; value: string }[];
|
||||
}
|
||||
|
||||
const QueryValueInput = ({ onChange, type, data, ...props }: any) => {
|
||||
const QueryValueInput = ({ data, onChange, type, ...props }: any) => {
|
||||
const [numberRange, setNumberRange] = useState([0, 0]);
|
||||
|
||||
switch (type) {
|
||||
case 'string':
|
||||
return (
|
||||
<TextInput
|
||||
size="sm"
|
||||
onChange={onChange}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
case 'number':
|
||||
return (
|
||||
<NumberInput
|
||||
size="sm"
|
||||
onChange={onChange}
|
||||
{...props}
|
||||
defaultValue={props.defaultValue && Number(props.defaultValue)}
|
||||
/>
|
||||
);
|
||||
case 'date':
|
||||
return (
|
||||
<TextInput
|
||||
size="sm"
|
||||
onChange={onChange}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
case 'dateRange':
|
||||
return (
|
||||
<>
|
||||
<NumberInput
|
||||
{...props}
|
||||
defaultValue={props.defaultValue && Number(props.defaultValue?.[0])}
|
||||
maxWidth={81}
|
||||
width="10%"
|
||||
onChange={(e) => {
|
||||
const newRange = [e || 0, numberRange[1]];
|
||||
setNumberRange(newRange);
|
||||
onChange(newRange);
|
||||
}}
|
||||
/>
|
||||
<NumberInput
|
||||
{...props}
|
||||
defaultValue={props.defaultValue && Number(props.defaultValue?.[1])}
|
||||
maxWidth={81}
|
||||
width="10%"
|
||||
onChange={(e) => {
|
||||
const newRange = [numberRange[0], e || 0];
|
||||
setNumberRange(newRange);
|
||||
onChange(newRange);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
case 'boolean':
|
||||
return (
|
||||
<Select
|
||||
|
|
@ -98,6 +47,50 @@ const QueryValueInput = ({ onChange, type, data, ...props }: any) => {
|
|||
{...props}
|
||||
/>
|
||||
);
|
||||
case 'date':
|
||||
return (
|
||||
<TextInput
|
||||
onChange={onChange}
|
||||
size="sm"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
case 'dateRange':
|
||||
return (
|
||||
<>
|
||||
<NumberInput
|
||||
{...props}
|
||||
defaultValue={props.defaultValue && Number(props.defaultValue?.[0])}
|
||||
maxWidth={81}
|
||||
onChange={(e) => {
|
||||
const newRange = [e || 0, numberRange[1]];
|
||||
setNumberRange(newRange);
|
||||
onChange(newRange);
|
||||
}}
|
||||
width="10%"
|
||||
/>
|
||||
<NumberInput
|
||||
{...props}
|
||||
defaultValue={props.defaultValue && Number(props.defaultValue?.[1])}
|
||||
maxWidth={81}
|
||||
onChange={(e) => {
|
||||
const newRange = [numberRange[0], e || 0];
|
||||
setNumberRange(newRange);
|
||||
onChange(newRange);
|
||||
}}
|
||||
width="10%"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
case 'number':
|
||||
return (
|
||||
<NumberInput
|
||||
onChange={onChange}
|
||||
size="sm"
|
||||
{...props}
|
||||
defaultValue={props.defaultValue && Number(props.defaultValue)}
|
||||
/>
|
||||
);
|
||||
case 'playlist':
|
||||
return (
|
||||
<Select
|
||||
|
|
@ -106,6 +99,14 @@ const QueryValueInput = ({ onChange, type, data, ...props }: any) => {
|
|||
{...props}
|
||||
/>
|
||||
);
|
||||
case 'string':
|
||||
return (
|
||||
<TextInput
|
||||
onChange={onChange}
|
||||
size="sm"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
return <></>;
|
||||
|
|
@ -115,14 +116,14 @@ const QueryValueInput = ({ onChange, type, data, ...props }: any) => {
|
|||
export const QueryBuilderOption = ({
|
||||
data,
|
||||
filters,
|
||||
level,
|
||||
onDeleteRule,
|
||||
operators,
|
||||
groupIndex,
|
||||
level,
|
||||
noRemove,
|
||||
onChangeField,
|
||||
onChangeOperator,
|
||||
onChangeValue,
|
||||
onDeleteRule,
|
||||
operators,
|
||||
selectData,
|
||||
}: QueryOptionProps) => {
|
||||
const { field, operator, uniqueId, value } = data;
|
||||
|
|
@ -192,51 +193,51 @@ export const QueryBuilderOption = ({
|
|||
spacing="sm"
|
||||
>
|
||||
<Select
|
||||
searchable
|
||||
data={filters}
|
||||
maxWidth={170}
|
||||
onChange={handleChangeField}
|
||||
searchable
|
||||
size="sm"
|
||||
value={field}
|
||||
width="25%"
|
||||
onChange={handleChangeField}
|
||||
/>
|
||||
<Select
|
||||
searchable
|
||||
data={operatorsByFieldType || []}
|
||||
disabled={!field}
|
||||
maxWidth={170}
|
||||
onChange={handleChangeOperator}
|
||||
searchable
|
||||
size="sm"
|
||||
value={operator}
|
||||
width="25%"
|
||||
onChange={handleChangeOperator}
|
||||
/>
|
||||
{field ? (
|
||||
<QueryValueInput
|
||||
data={selectData || []}
|
||||
defaultValue={value}
|
||||
maxWidth={170}
|
||||
onChange={handleChangeValue}
|
||||
size="sm"
|
||||
type={operator === 'inTheRange' ? 'dateRange' : fieldType}
|
||||
width="25%"
|
||||
onChange={handleChangeValue}
|
||||
/>
|
||||
) : (
|
||||
<TextInput
|
||||
disabled
|
||||
defaultValue={value}
|
||||
disabled
|
||||
maxWidth={170}
|
||||
onChange={handleChangeValue}
|
||||
size="sm"
|
||||
width="25%"
|
||||
onChange={handleChangeValue}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
disabled={noRemove}
|
||||
onClick={handleDeleteRule}
|
||||
px={5}
|
||||
size="sm"
|
||||
tooltip={{ label: 'Remove rule' }}
|
||||
variant="default"
|
||||
onClick={handleDeleteRule}
|
||||
>
|
||||
<RiSubtractLine size={20} />
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
import { useCallback } from 'react';
|
||||
import { Rating as MantineRating, RatingProps } from '@mantine/core';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { useCallback } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledRating = styled(MantineRating)`
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
/* eslint-disable react/display-name */
|
||||
import type { ScrollAreaProps as MantineScrollAreaProps } from '@mantine/core';
|
||||
|
||||
import { ScrollArea as MantineScrollArea } from '@mantine/core';
|
||||
import { useMergedRef } from '@mantine/hooks';
|
||||
import { useInView } from 'framer-motion';
|
||||
import { useOverlayScrollbars } from 'overlayscrollbars-react';
|
||||
import { CSSProperties, forwardRef, ReactNode, Ref, useEffect, useRef, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { PageHeader, PageHeaderProps } from '/@/renderer/components/page-header';
|
||||
import { useWindowSettings } from '/@/renderer/store/settings.store';
|
||||
import { Platform } from '/@/renderer/types';
|
||||
|
|
@ -51,7 +54,7 @@ const StyledNativeScrollArea = styled.div<{
|
|||
$scrollBarOffset?: string;
|
||||
$windowBarStyle?: Platform;
|
||||
}>`
|
||||
height: 100%;
|
||||
height: calc(100vh - 90px);
|
||||
`;
|
||||
|
||||
export const ScrollArea = forwardRef(({ children, ...props }: ScrollAreaProps, ref: Ref<any>) => {
|
||||
|
|
@ -80,10 +83,10 @@ export const NativeScrollArea = forwardRef(
|
|||
(
|
||||
{
|
||||
children,
|
||||
noHeader,
|
||||
pageHeaderProps,
|
||||
scrollBarOffset,
|
||||
scrollHideDelay,
|
||||
noHeader,
|
||||
...props
|
||||
}: NativeScrollAreaProps,
|
||||
ref: Ref<HTMLDivElement>,
|
||||
|
|
@ -156,9 +159,9 @@ export const NativeScrollArea = forwardRef(
|
|||
/>
|
||||
)}
|
||||
<StyledNativeScrollArea
|
||||
ref={mergedRef}
|
||||
$scrollBarOffset={scrollBarOffset}
|
||||
$windowBarStyle={windowBarStyle}
|
||||
ref={mergedRef}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { ChangeEvent, KeyboardEvent } from 'react';
|
||||
import { ActionIcon, TextInputProps } from '@mantine/core';
|
||||
import { useFocusWithin, useHotkeys, useMergedRef } from '@mantine/hooks';
|
||||
import { ChangeEvent, KeyboardEvent } from 'react';
|
||||
import { RiCloseFill, RiSearchLine } from 'react-icons/ri';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
import { TextInput } from '/@/renderer/components/input';
|
||||
import { useSettingsStore } from '/@/renderer/store';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
interface SearchInputProps extends TextInputProps {
|
||||
initialWidth?: number;
|
||||
|
|
@ -18,7 +19,7 @@ export const SearchInput = ({
|
|||
openedWidth,
|
||||
...props
|
||||
}: SearchInputProps) => {
|
||||
const { ref, focused } = useFocusWithin();
|
||||
const { focused, ref } = useFocusWithin();
|
||||
const mergedRef = useMergedRef<HTMLInputElement>(ref);
|
||||
const binding = useSettingsStore((state) => state.hotkeys.bindings.localSearch, shallow);
|
||||
|
||||
|
|
@ -40,6 +41,8 @@ export const SearchInput = ({
|
|||
ref={mergedRef}
|
||||
{...props}
|
||||
icon={showIcon && <RiSearchLine />}
|
||||
onChange={onChange}
|
||||
onKeyDown={handleEscape}
|
||||
rightSection={
|
||||
isOpened ? (
|
||||
<ActionIcon
|
||||
|
|
@ -64,8 +67,6 @@ export const SearchInput = ({
|
|||
},
|
||||
}}
|
||||
width={isOpened ? openedWidth || 150 : initialWidth || 35}
|
||||
onChange={onChange}
|
||||
onKeyDown={handleEscape}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { forwardRef } from 'react';
|
||||
import type { SegmentedControlProps as MantineSegmentedControlProps } from '@mantine/core';
|
||||
|
||||
import { SegmentedControl as MantineSegmentedControl } from '@mantine/core';
|
||||
import { forwardRef } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
type SegmentedControlProps = MantineSegmentedControlProps;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { MultiSelect, MultiSelectProps, Select, SelectProps } from '/@/renderer/components/select';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { MultiSelect, MultiSelectProps, Select, SelectProps } from '/@/renderer/components/select';
|
||||
|
||||
export const SelectWithInvalidData = ({ data, defaultValue, ...props }: SelectProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
import type {
|
||||
SelectProps as MantineSelectProps,
|
||||
MultiSelectProps as MantineMultiSelectProps,
|
||||
SelectProps as MantineSelectProps,
|
||||
} from '@mantine/core';
|
||||
import { Select as MantineSelect, MultiSelect as MantineMultiSelect } from '@mantine/core';
|
||||
|
||||
import { MultiSelect as MantineMultiSelect, Select as MantineSelect } from '@mantine/core';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export interface SelectProps extends MantineSelectProps {
|
||||
export interface MultiSelectProps extends MantineMultiSelectProps {
|
||||
maxWidth?: number | string;
|
||||
width?: number | string;
|
||||
}
|
||||
|
||||
export interface MultiSelectProps extends MantineMultiSelectProps {
|
||||
export interface SelectProps extends MantineSelectProps {
|
||||
maxWidth?: number | string;
|
||||
width?: number | string;
|
||||
}
|
||||
|
|
@ -37,10 +38,9 @@ const StyledSelect = styled(MantineSelect)`
|
|||
}
|
||||
`;
|
||||
|
||||
export const Select = ({ width, maxWidth, ...props }: SelectProps) => {
|
||||
export const Select = ({ maxWidth, width, ...props }: SelectProps) => {
|
||||
return (
|
||||
<StyledSelect
|
||||
withinPortal
|
||||
styles={{
|
||||
dropdown: {
|
||||
background: 'var(--dropdown-menu-bg)',
|
||||
|
|
@ -70,6 +70,7 @@ export const Select = ({ width, maxWidth, ...props }: SelectProps) => {
|
|||
}}
|
||||
sx={{ maxWidth, width }}
|
||||
transitionProps={{ duration: 100, transition: 'fade' }}
|
||||
withinPortal
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
|
@ -92,10 +93,9 @@ const StyledMultiSelect = styled(MantineMultiSelect)`
|
|||
}
|
||||
`;
|
||||
|
||||
export const MultiSelect = ({ width, maxWidth, ...props }: MultiSelectProps) => {
|
||||
export const MultiSelect = ({ maxWidth, width, ...props }: MultiSelectProps) => {
|
||||
return (
|
||||
<StyledMultiSelect
|
||||
withinPortal
|
||||
styles={{
|
||||
dropdown: {
|
||||
background: 'var(--dropdown-menu-bg)',
|
||||
|
|
@ -131,6 +131,7 @@ export const MultiSelect = ({ width, maxWidth, ...props }: MultiSelectProps) =>
|
|||
}}
|
||||
sx={{ maxWidth, width }}
|
||||
transitionProps={{ duration: 100, transition: 'fade' }}
|
||||
withinPortal
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { SkeletonProps as MantineSkeletonProps } from '@mantine/core';
|
||||
|
||||
import { Skeleton as MantineSkeleton } from '@mantine/core';
|
||||
import styled from 'styled-components';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { SliderProps as MantineSliderProps } from '@mantine/core';
|
||||
|
||||
import { Slider as MantineSlider } from '@mantine/core';
|
||||
import styled from 'styled-components';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { Center } from '@mantine/core';
|
||||
import type { IconType } from 'react-icons';
|
||||
|
||||
import { Center } from '@mantine/core';
|
||||
import { RiLoader5Fill } from 'react-icons/ri';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { rotating } from '/@/renderer/styles';
|
||||
|
||||
interface SpinnerProps extends IconType {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import clsx from 'clsx';
|
||||
import { HTMLAttributes, ReactNode, useRef, useState } from 'react';
|
||||
|
||||
import styles from './spoiler.module.scss';
|
||||
|
||||
import { useIsOverflow } from '/@/renderer/hooks';
|
||||
|
||||
interface SpoilerProps extends HTMLAttributes<HTMLDivElement> {
|
||||
|
|
@ -9,7 +11,7 @@ interface SpoilerProps extends HTMLAttributes<HTMLDivElement> {
|
|||
maxHeight?: number;
|
||||
}
|
||||
|
||||
export const Spoiler = ({ maxHeight, defaultOpened, children, ...props }: SpoilerProps) => {
|
||||
export const Spoiler = ({ children, defaultOpened, maxHeight, ...props }: SpoilerProps) => {
|
||||
const ref = useRef(null);
|
||||
const isOverflow = useIsOverflow(ref);
|
||||
const [isExpanded, setIsExpanded] = useState(!!defaultOpened);
|
||||
|
|
@ -25,12 +27,12 @@ export const Spoiler = ({ maxHeight, defaultOpened, children, ...props }: Spoile
|
|||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={spoilerClassNames}
|
||||
onClick={handleToggleExpand}
|
||||
ref={ref}
|
||||
role="button"
|
||||
style={{ maxHeight: maxHeight ?? '100px', whiteSpace: 'pre-wrap' }}
|
||||
tabIndex={-1}
|
||||
onClick={handleToggleExpand}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { SwitchProps as MantineSwitchProps } from '@mantine/core';
|
||||
|
||||
import { Switch as MantineSwitch } from '@mantine/core';
|
||||
import styled from 'styled-components';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Tabs as MantineTabs, TabsProps as MantineTabsProps, TabsPanelProps } from '@mantine/core';
|
||||
import { Suspense } from 'react';
|
||||
import { TabsPanelProps, TabsProps as MantineTabsProps, Tabs as MantineTabs } from '@mantine/core';
|
||||
import styled from 'styled-components';
|
||||
|
||||
type TabsProps = MantineTabsProps;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import type { ComponentPropsWithoutRef, ReactNode } from 'react';
|
||||
import type { TitleProps as MantineTitleProps } from '@mantine/core';
|
||||
import type { ComponentPropsWithoutRef, ReactNode } from 'react';
|
||||
|
||||
import { createPolymorphicComponent, Title as MantineHeader } from '@mantine/core';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { textEllipsis } from '/@/renderer/styles';
|
||||
|
||||
type MantineTextTitleDivProps = MantineTitleProps & ComponentPropsWithoutRef<'div'>;
|
||||
type MantineTextTitleDivProps = ComponentPropsWithoutRef<'div'> & MantineTitleProps;
|
||||
|
||||
interface TextTitleProps extends MantineTextTitleDivProps {
|
||||
$link?: boolean;
|
||||
|
|
@ -30,7 +32,7 @@ const StyledTextTitle = styled(MantineHeader)<TextTitleProps>`
|
|||
}
|
||||
`;
|
||||
|
||||
const _TextTitle = ({ children, $secondary, overflow, $noSelect, ...rest }: TextTitleProps) => {
|
||||
const _TextTitle = ({ $noSelect, $secondary, children, overflow, ...rest }: TextTitleProps) => {
|
||||
return (
|
||||
<StyledTextTitle
|
||||
$noSelect={$noSelect}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import type { ComponentPropsWithoutRef, ReactNode } from 'react';
|
||||
import type { Font } from '/@/renderer/styles';
|
||||
import type { TextProps as MantineTextProps } from '@mantine/core';
|
||||
import type { ComponentPropsWithoutRef, ReactNode } from 'react';
|
||||
|
||||
import { createPolymorphicComponent, Text as MantineText } from '@mantine/core';
|
||||
import styled from 'styled-components';
|
||||
import type { Font } from '/@/renderer/styles';
|
||||
|
||||
import { textEllipsis } from '/@/renderer/styles';
|
||||
|
||||
type MantineTextDivProps = MantineTextProps & ComponentPropsWithoutRef<'div'>;
|
||||
type MantineTextDivProps = ComponentPropsWithoutRef<'div'> & MantineTextProps;
|
||||
|
||||
interface TextProps extends MantineTextDivProps {
|
||||
$link?: boolean;
|
||||
|
|
@ -32,7 +34,7 @@ const StyledText = styled(MantineText)<TextProps>`
|
|||
}
|
||||
`;
|
||||
|
||||
export const _Text = ({ children, $secondary, overflow, font, $noSelect, ...rest }: TextProps) => {
|
||||
export const _Text = ({ $noSelect, $secondary, children, font, overflow, ...rest }: TextProps) => {
|
||||
return (
|
||||
<StyledText
|
||||
$noSelect={$noSelect}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
import type { NotificationProps as MantineNotificationProps } from '@mantine/notifications';
|
||||
|
||||
import {
|
||||
showNotification,
|
||||
updateNotification,
|
||||
hideNotification,
|
||||
cleanNotifications,
|
||||
cleanNotificationsQueue,
|
||||
hideNotification,
|
||||
showNotification,
|
||||
updateNotification,
|
||||
} from '@mantine/notifications';
|
||||
|
||||
interface NotificationProps extends MantineNotificationProps {
|
||||
type?: 'success' | 'error' | 'warning' | 'info';
|
||||
type?: 'error' | 'info' | 'success' | 'warning';
|
||||
}
|
||||
|
||||
const showToast = ({ type, ...props }: NotificationProps) => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { TooltipProps } from '@mantine/core';
|
||||
|
||||
import { Tooltip as MantineTooltip } from '@mantine/core';
|
||||
import styled from 'styled-components';
|
||||
|
||||
|
|
@ -12,7 +13,6 @@ export const Tooltip = ({ children, ...rest }: TooltipProps) => {
|
|||
return (
|
||||
<StyledTooltip
|
||||
multiline
|
||||
withinPortal
|
||||
pl={10}
|
||||
pr={10}
|
||||
py={5}
|
||||
|
|
@ -31,6 +31,7 @@ export const Tooltip = ({ children, ...rest }: TooltipProps) => {
|
|||
duration: 250,
|
||||
transition: 'fade',
|
||||
}}
|
||||
withinPortal
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
import { Center, Stack } from '@mantine/core';
|
||||
import { RiAlbumFill, RiUserVoiceFill, RiPlayListFill } from 'react-icons/ri';
|
||||
import { RiAlbumFill, RiPlayListFill, RiUserVoiceFill } from 'react-icons/ri';
|
||||
import { generatePath, useNavigate } from 'react-router-dom';
|
||||
import { SimpleImg } from 'react-simple-img';
|
||||
import { ListChildComponentProps } from 'react-window';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Album, AlbumArtist, Artist, LibraryItem, Playlist, Song } from '/@/renderer/api/types';
|
||||
import { CardRows } from '/@/renderer/components/card';
|
||||
import { Skeleton } from '/@/renderer/components/skeleton';
|
||||
import { GridCardControls } from '/@/renderer/components/virtual-grid/grid-card/grid-card-controls';
|
||||
import { CardRow, PlayQueueAddOptions, Play, CardRoute } from '/@/renderer/types';
|
||||
import { CardRoute, CardRow, Play, PlayQueueAddOptions } from '/@/renderer/types';
|
||||
|
||||
interface BaseGridCardProps {
|
||||
columnIndex: number;
|
||||
|
|
@ -131,11 +132,11 @@ const DetailContainer = styled.div`
|
|||
`;
|
||||
|
||||
export const DefaultCard = ({
|
||||
listChildProps,
|
||||
data,
|
||||
columnIndex,
|
||||
controls,
|
||||
data,
|
||||
isHidden,
|
||||
listChildProps,
|
||||
}: BaseGridCardProps) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
|
@ -156,10 +157,10 @@ export const DefaultCard = ({
|
|||
case LibraryItem.ALBUM:
|
||||
Placeholder = RiAlbumFill;
|
||||
break;
|
||||
case LibraryItem.ARTIST:
|
||||
case LibraryItem.ALBUM_ARTIST:
|
||||
Placeholder = RiUserVoiceFill;
|
||||
break;
|
||||
case LibraryItem.ALBUM_ARTIST:
|
||||
case LibraryItem.ARTIST:
|
||||
Placeholder = RiUserVoiceFill;
|
||||
break;
|
||||
case LibraryItem.PLAYLIST:
|
||||
|
|
@ -172,8 +173,8 @@ export const DefaultCard = ({
|
|||
|
||||
return (
|
||||
<DefaultCardContainer
|
||||
key={`card-${columnIndex}-${listChildProps.index}`}
|
||||
$itemGap={controls.itemGap}
|
||||
key={`card-${columnIndex}-${listChildProps.index}`}
|
||||
onClick={() => navigate(path)}
|
||||
>
|
||||
<InnerCardContainer>
|
||||
|
|
@ -221,25 +222,25 @@ export const DefaultCard = ({
|
|||
|
||||
return (
|
||||
<DefaultCardContainer
|
||||
key={`card-${columnIndex}-${listChildProps.index}`}
|
||||
$isHidden={isHidden}
|
||||
$itemGap={controls.itemGap}
|
||||
key={`card-${columnIndex}-${listChildProps.index}`}
|
||||
>
|
||||
<InnerCardContainer>
|
||||
<ImageContainer>
|
||||
<Skeleton
|
||||
visible
|
||||
radius="sm"
|
||||
visible
|
||||
/>
|
||||
</ImageContainer>
|
||||
<DetailContainer>
|
||||
<Stack spacing="sm">
|
||||
{(controls?.cardRows || []).map((row, index) => (
|
||||
<Skeleton
|
||||
key={`${index}-${columnIndex}-${row.arrayProperty}`}
|
||||
visible
|
||||
height={14}
|
||||
key={`${index}-${columnIndex}-${row.arrayProperty}`}
|
||||
radius="sm"
|
||||
visible
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
|
|
|
|||
|
|
@ -1,20 +1,23 @@
|
|||
import React, { MouseEvent, useState } from 'react';
|
||||
import type { UnstyledButtonProps } from '@mantine/core';
|
||||
import { RiPlayFill, RiHeartFill, RiHeartLine, RiMoreFill } from 'react-icons/ri';
|
||||
import styled from 'styled-components';
|
||||
import { _Button } from '/@/renderer/components/button';
|
||||
import type { PlayQueueAddOptions } from '/@/renderer/types';
|
||||
import { Play } from '/@/renderer/types';
|
||||
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
||||
import { LibraryItem } from '/@/renderer/api/types';
|
||||
import { useHandleGridContextMenu } from '/@/renderer/features/context-menu/hooks/use-handle-context-menu';
|
||||
import type { UnstyledButtonProps } from '@mantine/core';
|
||||
|
||||
import React, { MouseEvent, useState } from 'react';
|
||||
import { RiHeartFill, RiHeartLine, RiMoreFill, RiPlayFill } from 'react-icons/ri';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import {
|
||||
PLAYLIST_CONTEXT_MENU_ITEMS,
|
||||
ALBUM_CONTEXT_MENU_ITEMS,
|
||||
ARTIST_CONTEXT_MENU_ITEMS,
|
||||
PLAYLIST_CONTEXT_MENU_ITEMS,
|
||||
} from '../../../features/context-menu/context-menu-items';
|
||||
|
||||
type PlayButtonType = UnstyledButtonProps & React.ComponentPropsWithoutRef<'button'>;
|
||||
import { LibraryItem } from '/@/renderer/api/types';
|
||||
import { _Button } from '/@/renderer/components/button';
|
||||
import { useHandleGridContextMenu } from '/@/renderer/features/context-menu/hooks/use-handle-context-menu';
|
||||
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
||||
import { Play } from '/@/renderer/types';
|
||||
|
||||
type PlayButtonType = React.ComponentPropsWithoutRef<'button'> & UnstyledButtonProps;
|
||||
|
||||
const PlayButton = styled.button<PlayButtonType>`
|
||||
position: absolute;
|
||||
|
|
@ -108,10 +111,10 @@ const FavoriteWrapper = styled.span<{ isFavorite: boolean }>`
|
|||
`;
|
||||
|
||||
export const GridCardControls = ({
|
||||
handleFavorite,
|
||||
handlePlayQueueAdd,
|
||||
itemData,
|
||||
itemType,
|
||||
handlePlayQueueAdd,
|
||||
handleFavorite,
|
||||
resetInfiniteLoaderCache,
|
||||
}: {
|
||||
handleFavorite: (options: {
|
||||
|
|
@ -178,9 +181,9 @@ export const GridCardControls = ({
|
|||
<BottomControls>
|
||||
{itemType !== LibraryItem.PLAYLIST && (
|
||||
<SecondaryButton
|
||||
onClick={(e) => handleFavorites(e, itemData?.serverId)}
|
||||
p={5}
|
||||
variant="subtle"
|
||||
onClick={(e) => handleFavorites(e, itemData?.serverId)}
|
||||
>
|
||||
<FavoriteWrapper isFavorite={itemData?.isFavorite}>
|
||||
{isFavorite ? (
|
||||
|
|
@ -196,13 +199,13 @@ export const GridCardControls = ({
|
|||
)}
|
||||
|
||||
<SecondaryButton
|
||||
p={5}
|
||||
variant="subtle"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleContextMenu(e, [itemData]);
|
||||
}}
|
||||
p={5}
|
||||
variant="subtle"
|
||||
>
|
||||
<RiMoreFill
|
||||
color="white"
|
||||
|
|
|
|||
|
|
@ -1,24 +1,26 @@
|
|||
import { memo } from 'react';
|
||||
import type { ListChildComponentProps } from 'react-window';
|
||||
|
||||
import { memo } from 'react';
|
||||
import { areEqual } from 'react-window';
|
||||
|
||||
import { DefaultCard } from '/@/renderer/components/virtual-grid/grid-card/default-card';
|
||||
import { PosterCard } from '/@/renderer/components/virtual-grid/grid-card/poster-card';
|
||||
import { GridCardData, ListDisplayType } from '/@/renderer/types';
|
||||
|
||||
export const GridCard = memo(({ data, index, style }: ListChildComponentProps) => {
|
||||
const {
|
||||
columnCount,
|
||||
itemCount,
|
||||
cardRows,
|
||||
itemData,
|
||||
itemType,
|
||||
itemGap,
|
||||
playButtonBehavior,
|
||||
handlePlayQueueAdd,
|
||||
handleFavorite,
|
||||
route,
|
||||
columnCount,
|
||||
display,
|
||||
handleFavorite,
|
||||
handlePlayQueueAdd,
|
||||
itemCount,
|
||||
itemData,
|
||||
itemGap,
|
||||
itemType,
|
||||
playButtonBehavior,
|
||||
resetInfiniteLoaderCache,
|
||||
route,
|
||||
} = data as GridCardData;
|
||||
|
||||
const cards = [];
|
||||
|
|
@ -35,7 +37,6 @@ export const GridCard = memo(({ data, index, style }: ListChildComponentProps) =
|
|||
for (let i = startIndex; i <= stopIndex + columnCountToAdd; i += 1) {
|
||||
cards.push(
|
||||
<View
|
||||
key={`card-${i}-${index}`}
|
||||
columnIndex={i}
|
||||
controls={{
|
||||
cardRows,
|
||||
|
|
@ -49,6 +50,7 @@ export const GridCard = memo(({ data, index, style }: ListChildComponentProps) =
|
|||
}}
|
||||
data={itemData[i]}
|
||||
isHidden={i > stopIndex}
|
||||
key={`card-${i}-${index}`}
|
||||
listChildProps={{ index }}
|
||||
/>,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,11 +4,12 @@ import { generatePath, useNavigate } from 'react-router-dom';
|
|||
import { SimpleImg } from 'react-simple-img';
|
||||
import { ListChildComponentProps } from 'react-window';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Album, AlbumArtist, Artist, LibraryItem, Playlist, Song } from '/@/renderer/api/types';
|
||||
import { CardRows } from '/@/renderer/components/card';
|
||||
import { Skeleton } from '/@/renderer/components/skeleton';
|
||||
import { GridCardControls } from '/@/renderer/components/virtual-grid/grid-card/grid-card-controls';
|
||||
import { CardRow, PlayQueueAddOptions, Play, CardRoute } from '/@/renderer/types';
|
||||
import { CardRoute, CardRow, Play, PlayQueueAddOptions } from '/@/renderer/types';
|
||||
|
||||
interface BaseGridCardProps {
|
||||
columnIndex: number;
|
||||
|
|
@ -119,11 +120,11 @@ const DetailContainer = styled.div`
|
|||
`;
|
||||
|
||||
export const PosterCard = ({
|
||||
listChildProps,
|
||||
data,
|
||||
columnIndex,
|
||||
controls,
|
||||
data,
|
||||
isHidden,
|
||||
listChildProps,
|
||||
}: BaseGridCardProps) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
|
@ -144,10 +145,10 @@ export const PosterCard = ({
|
|||
case LibraryItem.ALBUM:
|
||||
Placeholder = RiAlbumFill;
|
||||
break;
|
||||
case LibraryItem.ARTIST:
|
||||
case LibraryItem.ALBUM_ARTIST:
|
||||
Placeholder = RiUserVoiceFill;
|
||||
break;
|
||||
case LibraryItem.ALBUM_ARTIST:
|
||||
case LibraryItem.ARTIST:
|
||||
Placeholder = RiUserVoiceFill;
|
||||
break;
|
||||
case LibraryItem.PLAYLIST:
|
||||
|
|
@ -160,8 +161,8 @@ export const PosterCard = ({
|
|||
|
||||
return (
|
||||
<PosterCardContainer
|
||||
key={`card-${columnIndex}-${listChildProps.index}`}
|
||||
$itemGap={controls.itemGap}
|
||||
key={`card-${columnIndex}-${listChildProps.index}`}
|
||||
>
|
||||
<LinkContainer onClick={() => navigate(path)}>
|
||||
<ImageContainer $isFavorite={data?.userFavorite}>
|
||||
|
|
@ -207,13 +208,13 @@ export const PosterCard = ({
|
|||
|
||||
return (
|
||||
<PosterCardContainer
|
||||
key={`card-${columnIndex}-${listChildProps.index}`}
|
||||
$isHidden={isHidden}
|
||||
$itemGap={controls.itemGap}
|
||||
key={`card-${columnIndex}-${listChildProps.index}`}
|
||||
>
|
||||
<Skeleton
|
||||
visible
|
||||
radius="sm"
|
||||
visible
|
||||
>
|
||||
<ImageContainer />
|
||||
</Skeleton>
|
||||
|
|
@ -221,10 +222,10 @@ export const PosterCard = ({
|
|||
<Stack spacing="sm">
|
||||
{(controls?.cardRows || []).map((row, index) => (
|
||||
<Skeleton
|
||||
key={`${index}-${columnIndex}-${row.arrayProperty}`}
|
||||
visible
|
||||
height={14}
|
||||
key={`${index}-${columnIndex}-${row.arrayProperty}`}
|
||||
radius="sm"
|
||||
visible
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import type { CardRoute, CardRow, ListDisplayType, PlayQueueAddOptions } from '/@/renderer/types';
|
||||
import type { Ref } from 'react';
|
||||
import type { FixedSizeListProps } from 'react-window';
|
||||
|
||||
import debounce from 'lodash/debounce';
|
||||
import memoize from 'memoize-one';
|
||||
import type { FixedSizeListProps } from 'react-window';
|
||||
import { FixedSizeList } from 'react-window';
|
||||
import styled from 'styled-components';
|
||||
import { GridCard } from '/@/renderer/components/virtual-grid/grid-card';
|
||||
import type { CardRow, ListDisplayType, CardRoute, PlayQueueAddOptions } from '/@/renderer/types';
|
||||
|
||||
import { Album, AlbumArtist, Artist, LibraryItem } from '/@/renderer/api/types';
|
||||
import { GridCard } from '/@/renderer/components/virtual-grid/grid-card';
|
||||
|
||||
const createItemData = memoize(
|
||||
(
|
||||
|
|
@ -43,27 +45,27 @@ const createItemData = memoize(
|
|||
const createScrollHandler = memoize((onScroll) => debounce(onScroll, 250));
|
||||
|
||||
export const VirtualGridWrapper = ({
|
||||
refInstance,
|
||||
cardRows,
|
||||
itemGap,
|
||||
itemType,
|
||||
itemWidth,
|
||||
display,
|
||||
itemHeight,
|
||||
itemCount,
|
||||
columnCount,
|
||||
rowCount,
|
||||
initialScrollOffset,
|
||||
display,
|
||||
handleFavorite,
|
||||
handlePlayQueueAdd,
|
||||
itemData,
|
||||
route,
|
||||
onScroll,
|
||||
height,
|
||||
width,
|
||||
initialScrollOffset,
|
||||
itemCount,
|
||||
itemData,
|
||||
itemGap,
|
||||
itemHeight,
|
||||
itemType,
|
||||
itemWidth,
|
||||
onScroll,
|
||||
refInstance,
|
||||
resetInfiniteLoaderCache,
|
||||
route,
|
||||
rowCount,
|
||||
width,
|
||||
...rest
|
||||
}: Omit<FixedSizeListProps, 'ref' | 'itemSize' | 'children' | 'height' | 'width'> & {
|
||||
}: Omit<FixedSizeListProps, 'children' | 'height' | 'itemSize' | 'ref' | 'width'> & {
|
||||
cardRows: CardRow<Album | AlbumArtist | Artist>[];
|
||||
columnCount: number;
|
||||
display: ListDisplayType;
|
||||
|
|
@ -112,9 +114,9 @@ export const VirtualGridWrapper = ({
|
|||
itemCount={rowCount}
|
||||
itemData={memoizedItemData}
|
||||
itemSize={itemHeight}
|
||||
onScroll={memoizedOnScroll}
|
||||
overscanCount={5}
|
||||
width={(width && Number(width)) || 0}
|
||||
onScroll={memoizedOnScroll}
|
||||
>
|
||||
{GridCard}
|
||||
</FixedSizeList>
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
import type { CardRoute, CardRow, PlayQueueAddOptions } from '/@/renderer/types';
|
||||
import type { FixedSizeListProps } from 'react-window';
|
||||
|
||||
import debounce from 'lodash/debounce';
|
||||
import {
|
||||
useState,
|
||||
useRef,
|
||||
useMemo,
|
||||
useCallback,
|
||||
forwardRef,
|
||||
Ref,
|
||||
useCallback,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import debounce from 'lodash/debounce';
|
||||
import type { FixedSizeListProps } from 'react-window';
|
||||
import InfiniteLoader from 'react-window-infinite-loader';
|
||||
import { VirtualGridWrapper } from '/@/renderer/components/virtual-grid/virtual-grid-wrapper';
|
||||
import type { CardRoute, CardRow, PlayQueueAddOptions } from '/@/renderer/types';
|
||||
import { ListDisplayType } from '/@/renderer/types';
|
||||
import { AnyLibraryItem, Genre, LibraryItem } from '/@/renderer/api/types';
|
||||
|
||||
type LibraryItemOrGenre = AnyLibraryItem | Genre;
|
||||
import { AnyLibraryItem, Genre, LibraryItem } from '/@/renderer/api/types';
|
||||
import { VirtualGridWrapper } from '/@/renderer/components/virtual-grid/virtual-grid-wrapper';
|
||||
import { ListDisplayType } from '/@/renderer/types';
|
||||
|
||||
export type VirtualInfiniteGridRef = {
|
||||
resetLoadMoreItemsCache: () => void;
|
||||
|
|
@ -24,8 +24,10 @@ export type VirtualInfiniteGridRef = {
|
|||
updateItemData: (rule: (item: LibraryItemOrGenre) => LibraryItemOrGenre) => void;
|
||||
};
|
||||
|
||||
type LibraryItemOrGenre = AnyLibraryItem | Genre;
|
||||
|
||||
interface VirtualGridProps
|
||||
extends Omit<FixedSizeListProps, 'children' | 'itemSize' | 'height' | 'width'> {
|
||||
extends Omit<FixedSizeListProps, 'children' | 'height' | 'itemSize' | 'width'> {
|
||||
cardRows: CardRow<any>[];
|
||||
display?: ListDisplayType;
|
||||
fetchFn: (options: { columnCount: number; skip: number; take: number }) => Promise<any>;
|
||||
|
|
@ -49,22 +51,22 @@ interface VirtualGridProps
|
|||
export const VirtualInfiniteGrid = forwardRef(
|
||||
(
|
||||
{
|
||||
cardRows,
|
||||
display,
|
||||
fetchFn,
|
||||
fetchInitialData,
|
||||
handleFavorite,
|
||||
handlePlayQueueAdd,
|
||||
height,
|
||||
initialScrollOffset,
|
||||
itemCount,
|
||||
itemGap,
|
||||
itemSize,
|
||||
itemType,
|
||||
cardRows,
|
||||
route,
|
||||
onScroll,
|
||||
display,
|
||||
handlePlayQueueAdd,
|
||||
minimumBatchSize,
|
||||
fetchFn,
|
||||
fetchInitialData,
|
||||
loading,
|
||||
initialScrollOffset,
|
||||
handleFavorite,
|
||||
height,
|
||||
minimumBatchSize,
|
||||
onScroll,
|
||||
route,
|
||||
width,
|
||||
}: VirtualGridProps,
|
||||
ref: Ref<VirtualInfiniteGridRef>,
|
||||
|
|
@ -78,7 +80,7 @@ export const VirtualInfiniteGrid = forwardRef(
|
|||
fetchInitialData?.() || [],
|
||||
);
|
||||
|
||||
const { itemHeight, rowCount, columnCount } = useMemo(() => {
|
||||
const { columnCount, itemHeight, rowCount } = useMemo(() => {
|
||||
const itemsPerRow = width ? Math.floor(width / (itemSize + itemGap * 2)) : 5;
|
||||
const widthPerItem = Number(width) / itemsPerRow;
|
||||
const itemHeight = widthPerItem + cardRows.length * 26;
|
||||
|
|
@ -165,11 +167,11 @@ export const VirtualInfiniteGrid = forwardRef(
|
|||
return (
|
||||
<>
|
||||
<InfiniteLoader
|
||||
ref={loader}
|
||||
isItemLoaded={(index) => isItemLoaded(index)}
|
||||
itemCount={itemCount || 0}
|
||||
loadMoreItems={debouncedLoadMoreItems}
|
||||
minimumBatchSize={minimumBatchSize}
|
||||
ref={loader}
|
||||
threshold={30}
|
||||
>
|
||||
{({ onItemsRendered, ref: infiniteLoaderRef }) => (
|
||||
|
|
@ -187,6 +189,8 @@ export const VirtualInfiniteGrid = forwardRef(
|
|||
itemHeight={itemHeight}
|
||||
itemType={itemType}
|
||||
itemWidth={itemSize}
|
||||
onItemsRendered={onItemsRendered}
|
||||
onScroll={onScroll}
|
||||
refInstance={(list) => {
|
||||
infiniteLoaderRef(list);
|
||||
listRef.current = list;
|
||||
|
|
@ -200,8 +204,6 @@ export const VirtualInfiniteGrid = forwardRef(
|
|||
route={route}
|
||||
rowCount={rowCount}
|
||||
width={width}
|
||||
onItemsRendered={onItemsRendered}
|
||||
onScroll={onScroll}
|
||||
/>
|
||||
)}
|
||||
</InfiniteLoader>
|
||||
|
|
|
|||
|
|
@ -1,19 +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';
|
||||
|
||||
export const ActionsCell = ({ context, api }: ICellRendererParams) => {
|
||||
export const ActionsCell = ({ api, context }: ICellRendererParams) => {
|
||||
return (
|
||||
<CellContainer $position="center">
|
||||
<Button
|
||||
compact
|
||||
variant="subtle"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
context.onCellContextMenu(undefined, api, e);
|
||||
}}
|
||||
variant="subtle"
|
||||
>
|
||||
<RiMoreFill />
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
import React from 'react';
|
||||
import type { AlbumArtist, Artist } from '/@/renderer/api/types';
|
||||
import type { ICellRendererParams } from '@ag-grid-community/core';
|
||||
|
||||
import React from 'react';
|
||||
import { generatePath } from 'react-router';
|
||||
import { Link } from 'react-router-dom';
|
||||
import type { AlbumArtist, Artist } from '/@/renderer/api/types';
|
||||
|
||||
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 { Skeleton } from '/@/renderer/components/skeleton';
|
||||
import { Separator } from '/@/renderer/components/separator';
|
||||
|
||||
export const AlbumArtistCell = ({ value, data }: ICellRendererParams) => {
|
||||
export const AlbumArtistCell = ({ data, value }: ICellRendererParams) => {
|
||||
if (value === undefined) {
|
||||
return (
|
||||
<CellContainer $position="left">
|
||||
|
|
@ -28,7 +30,7 @@ export const AlbumArtistCell = ({ value, data }: ICellRendererParams) => {
|
|||
overflow="hidden"
|
||||
size="md"
|
||||
>
|
||||
{value?.map((item: Artist | AlbumArtist, index: number) => (
|
||||
{value?.map((item: AlbumArtist | Artist, index: number) => (
|
||||
<React.Fragment key={`row-${item.id}-${data.uniqueId}`}>
|
||||
{index > 0 && <Separator />}
|
||||
{item.id ? (
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
import React from 'react';
|
||||
import type { AlbumArtist, Artist } from '/@/renderer/api/types';
|
||||
import type { ICellRendererParams } from '@ag-grid-community/core';
|
||||
|
||||
import React from 'react';
|
||||
import { generatePath } from 'react-router';
|
||||
import { Link } from 'react-router-dom';
|
||||
import type { AlbumArtist, Artist } from '/@/renderer/api/types';
|
||||
|
||||
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 { Skeleton } from '/@/renderer/components/skeleton';
|
||||
import { Separator } from '/@/renderer/components/separator';
|
||||
|
||||
export const ArtistCell = ({ value, data }: ICellRendererParams) => {
|
||||
export const ArtistCell = ({ data, value }: ICellRendererParams) => {
|
||||
if (value === undefined) {
|
||||
return (
|
||||
<CellContainer $position="left">
|
||||
|
|
@ -28,7 +30,7 @@ export const ArtistCell = ({ value, data }: ICellRendererParams) => {
|
|||
overflow="hidden"
|
||||
size="md"
|
||||
>
|
||||
{value?.map((item: Artist | AlbumArtist, index: number) => (
|
||||
{value?.map((item: AlbumArtist | Artist, index: number) => (
|
||||
<React.Fragment key={`row-${item.id}-${data.uniqueId}`}>
|
||||
{index > 0 && <Separator />}
|
||||
{item.id ? (
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
import React, { MouseEvent } from 'react';
|
||||
import type { UnstyledButtonProps } from '@mantine/core';
|
||||
|
||||
import React, { MouseEvent } from 'react';
|
||||
import { RiPlayFill } from 'react-icons/ri';
|
||||
import styled from 'styled-components';
|
||||
import { Play } from '/@/renderer/types';
|
||||
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
||||
|
||||
import { LibraryItem } from '/@/renderer/api/types';
|
||||
import { usePlayQueueAdd } from '/@/renderer/features/player';
|
||||
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
||||
import { Play } from '/@/renderer/types';
|
||||
|
||||
type PlayButtonType = UnstyledButtonProps & React.ComponentPropsWithoutRef<'button'>;
|
||||
type PlayButtonType = React.ComponentPropsWithoutRef<'button'> & UnstyledButtonProps;
|
||||
|
||||
const PlayButton = styled.button<PlayButtonType>`
|
||||
position: absolute;
|
||||
|
|
@ -50,9 +52,9 @@ const ListConverControlsContainer = styled.div`
|
|||
`;
|
||||
|
||||
export const ListCoverControls = ({
|
||||
context,
|
||||
itemData,
|
||||
itemType,
|
||||
context,
|
||||
uniqueId,
|
||||
}: {
|
||||
context: Record<string, any>;
|
||||
|
|
|
|||
|
|
@ -1,18 +1,20 @@
|
|||
import React, { useMemo } from 'react';
|
||||
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 { AlbumArtist, Artist } from '/@/renderer/api/types';
|
||||
import { Text } from '/@/renderer/components/text';
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { Skeleton } from '/@/renderer/components/skeleton';
|
||||
import { SEPARATOR_STRING } from '/@/renderer/api/utils';
|
||||
import { Skeleton } from '/@/renderer/components/skeleton';
|
||||
import { Text } from '/@/renderer/components/text';
|
||||
import { ListCoverControls } from '/@/renderer/components/virtual-table/cells/combined-title-cell-controls';
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
|
||||
const CellContainer = styled(motion.div)<{ height: number }>`
|
||||
display: grid;
|
||||
|
|
@ -61,11 +63,11 @@ const StyledImage = styled(SimpleImg)`
|
|||
`;
|
||||
|
||||
export const CombinedTitleCell = ({
|
||||
value,
|
||||
rowIndex,
|
||||
node,
|
||||
context,
|
||||
data,
|
||||
node,
|
||||
rowIndex,
|
||||
value,
|
||||
}: ICellRendererParams) => {
|
||||
const artists = useMemo(() => {
|
||||
if (!value) return null;
|
||||
|
|
@ -141,7 +143,7 @@ export const CombinedTitleCell = ({
|
|||
size="md"
|
||||
>
|
||||
{artists?.length ? (
|
||||
artists.map((artist: Artist | AlbumArtist, index: number) => (
|
||||
artists.map((artist: AlbumArtist | Artist, index: number) => (
|
||||
<React.Fragment key={`queue-${rowIndex}-artist-${artist.id}`}>
|
||||
{index > 0 ? SEPARATOR_STRING : null}
|
||||
{artist.id ? (
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
/* eslint-disable import/no-cycle */
|
||||
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';
|
||||
|
||||
export const FavoriteCell = ({ value, data, node }: ICellRendererParams) => {
|
||||
export const FavoriteCell = ({ data, node, value }: ICellRendererParams) => {
|
||||
const createMutation = useCreateFavorite({});
|
||||
const deleteMutation = useDeleteFavorite({});
|
||||
|
||||
|
|
@ -49,6 +51,7 @@ export const FavoriteCell = ({ value, data, node }: ICellRendererParams) => {
|
|||
<CellContainer $position="center">
|
||||
<Button
|
||||
compact
|
||||
onClick={handleToggleFavorite}
|
||||
sx={{
|
||||
svg: {
|
||||
fill: !value
|
||||
|
|
@ -57,7 +60,6 @@ export const FavoriteCell = ({ value, data, node }: ICellRendererParams) => {
|
|||
},
|
||||
}}
|
||||
variant="subtle"
|
||||
onClick={handleToggleFavorite}
|
||||
>
|
||||
{!value ? <RiHeartLine size="1.3em" /> : <RiHeartFill size="1.3em" />}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import { useState } from 'react';
|
||||
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 { getNodesByDiscNumber, setNodeSelection } from '../utils';
|
||||
|
||||
import { Button } from '/@/renderer/components/button';
|
||||
import { Paper } from '/@/renderer/components/paper';
|
||||
import { getNodesByDiscNumber, setNodeSelection } from '../utils';
|
||||
|
||||
const Container = styled(Paper)`
|
||||
display: flex;
|
||||
|
|
@ -14,7 +16,7 @@ const Container = styled(Paper)`
|
|||
border: 1px solid transparent;
|
||||
`;
|
||||
|
||||
export const FullWidthDiscCell = ({ node, data, api }: ICellRendererParams) => {
|
||||
export const FullWidthDiscCell = ({ api, data, node }: ICellRendererParams) => {
|
||||
const [isSelected, setIsSelected] = useState(false);
|
||||
|
||||
const handleToggleDiscNodes = () => {
|
||||
|
|
@ -38,9 +40,9 @@ export const FullWidthDiscCell = ({ node, data, api }: ICellRendererParams) => {
|
|||
<Button
|
||||
compact
|
||||
leftIcon={isSelected ? <RiCheckboxLine /> : <RiCheckboxBlankLine />}
|
||||
onClick={handleToggleDiscNodes}
|
||||
size="md"
|
||||
variant="subtle"
|
||||
onClick={handleToggleDiscNodes}
|
||||
>
|
||||
{data.name}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import type { ICellRendererParams } from '@ag-grid-community/core';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Skeleton } from '/@/renderer/components/skeleton';
|
||||
import { Text } from '/@/renderer/components/text';
|
||||
|
||||
export const CellContainer = styled.div<{ $position?: 'left' | 'center' | 'right' }>`
|
||||
export const CellContainer = styled.div<{ $position?: 'center' | 'left' | 'right' }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: ${(props) =>
|
||||
|
|
@ -22,13 +24,13 @@ type Options = {
|
|||
array?: boolean;
|
||||
isArray?: boolean;
|
||||
isLink?: boolean;
|
||||
position?: 'left' | 'center' | 'right';
|
||||
position?: 'center' | 'left' | 'right';
|
||||
primary?: boolean;
|
||||
};
|
||||
|
||||
export const GenericCell = (
|
||||
{ value, valueFormatted }: ICellRendererParams,
|
||||
{ position, primary, isLink }: Options,
|
||||
{ isLink, position, primary }: Options,
|
||||
) => {
|
||||
const displayedValue = valueFormatted || value;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
import React from 'react';
|
||||
import type { ICellRendererParams } from '@ag-grid-community/core';
|
||||
import { generatePath, Link } from 'react-router-dom';
|
||||
import type { AlbumArtist, Artist } from '/@/renderer/api/types';
|
||||
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 { Separator } from '/@/renderer/components/separator';
|
||||
import { useGenreRoute } from '/@/renderer/hooks/use-genre-route';
|
||||
|
||||
export const GenreCell = ({ value, data }: ICellRendererParams) => {
|
||||
export const GenreCell = ({ data, value }: ICellRendererParams) => {
|
||||
const genrePath = useGenreRoute();
|
||||
return (
|
||||
<CellContainer $position="left">
|
||||
|
|
@ -16,7 +18,7 @@ export const GenreCell = ({ value, data }: ICellRendererParams) => {
|
|||
overflow="hidden"
|
||||
size="md"
|
||||
>
|
||||
{value?.map((item: Artist | AlbumArtist, index: number) => (
|
||||
{value?.map((item: AlbumArtist | Artist, index: number) => (
|
||||
<React.Fragment key={`row-${item.id}-${data.uniqueId}`}>
|
||||
{index > 0 && <Separator />}
|
||||
<Text
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import type { ICellRendererParams } from '@ag-grid-community/core';
|
||||
import { Skeleton } from '/@/renderer/components/skeleton';
|
||||
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';
|
||||
|
||||
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';
|
||||
|
||||
export const NoteCell = ({ value }: ICellRendererParams) => {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
/* eslint-disable import/no-cycle */
|
||||
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';
|
||||
|
||||
export const RatingCell = ({ value, node }: ICellRendererParams) => {
|
||||
export const RatingCell = ({ node, value }: ICellRendererParams) => {
|
||||
const updateRatingMutation = useSetRating({});
|
||||
|
||||
const handleUpdateRating = (rating: number) => {
|
||||
|
|
@ -27,9 +28,9 @@ export const RatingCell = ({ value, node }: ICellRendererParams) => {
|
|||
return (
|
||||
<CellContainer $position="center">
|
||||
<Rating
|
||||
onChange={handleUpdateRating}
|
||||
size="xs"
|
||||
value={value?.userRating}
|
||||
onChange={handleUpdateRating}
|
||||
/>
|
||||
</CellContainer>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { ICellRendererParams } from '@ag-grid-community/core';
|
||||
|
||||
import { Text } from '/@/renderer/components/text';
|
||||
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';
|
||||
|
||||
|
|
@ -132,7 +133,7 @@ const StaticSvg = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export const RowIndexCell = ({ value, eGridCell }: ICellRendererParams) => {
|
||||
export const RowIndexCell = ({ eGridCell, value }: ICellRendererParams) => {
|
||||
const classList = eGridCell.classList;
|
||||
// const isFocused = classList.contains('focused');
|
||||
const isPlaying = classList.contains('playing');
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
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';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { IHeaderParams } from '@ag-grid-community/core';
|
||||
|
||||
import { FiClock } from 'react-icons/fi';
|
||||
|
||||
export interface ICustomHeaderParams extends IHeaderParams {
|
||||
|
|
|
|||
|
|
@ -1,19 +1,21 @@
|
|||
import type { ReactNode } from 'react';
|
||||
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 { _Text } from '/@/renderer/components/text';
|
||||
|
||||
type Presets = 'duration' | 'rowIndex' | 'userFavorite' | 'userRating' | 'actions';
|
||||
import { _Text } from '/@/renderer/components/text';
|
||||
|
||||
type Options = {
|
||||
children?: ReactNode;
|
||||
position?: 'left' | 'center' | 'right';
|
||||
position?: 'center' | 'left' | 'right';
|
||||
preset?: Presets;
|
||||
};
|
||||
|
||||
type Presets = 'actions' | 'duration' | 'rowIndex' | 'userFavorite' | 'userRating';
|
||||
|
||||
export const HeaderWrapper = styled.div<{ $position: Options['position'] }>`
|
||||
display: flex;
|
||||
justify-content: ${(props) =>
|
||||
|
|
@ -77,7 +79,7 @@ const headerPresets = {
|
|||
|
||||
export const GenericTableHeader = (
|
||||
{ displayName }: IHeaderParams,
|
||||
{ preset, children, position }: Options,
|
||||
{ children, position, preset }: Options,
|
||||
) => {
|
||||
if (preset) {
|
||||
return <HeaderWrapper $position={position}>{headerPresets[preset]}</HeaderWrapper>;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { MutableRefObject } from 'react';
|
||||
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 = () => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { MutableRefObject, useEffect, useMemo, useRef } from 'react';
|
||||
import { RowClassRules, RowNode } from '@ag-grid-community/core';
|
||||
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 { Song } from '/@/renderer/api/types';
|
||||
import { useAppFocus } from '/@/renderer/hooks';
|
||||
import { useCurrentSong, usePlayerStore } from '/@/renderer/store';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useInView } from 'framer-motion';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
import { useWindowSettings } from '/@/renderer/store/settings.store';
|
||||
import { Platform } from '/@/renderer/types';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { MutableRefObject, useCallback, useMemo } from 'react';
|
||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||
|
||||
import {
|
||||
BodyScrollEvent,
|
||||
ColDef,
|
||||
|
|
@ -8,13 +9,17 @@ import {
|
|||
PaginationChangedEvent,
|
||||
RowDoubleClickedEvent,
|
||||
} from '@ag-grid-community/core';
|
||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||
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-dom';
|
||||
|
||||
import { ListKey, useListStoreByKey } from '../../../store/list.store';
|
||||
|
||||
import { api } from '/@/renderer/api';
|
||||
import { QueryPagination, queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { queryKeys, QueryPagination } from '/@/renderer/api/query-keys';
|
||||
import {
|
||||
BasePaginatedResponse,
|
||||
BaseQuery,
|
||||
|
|
@ -26,8 +31,6 @@ import { SetContextMenuItems, useHandleTableContextMenu } from '/@/renderer/feat
|
|||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { useListStoreActions } from '/@/renderer/store';
|
||||
import { ListDisplayType, TablePagination } from '/@/renderer/types';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { ListKey, useListStoreByKey } from '../../../store/list.store';
|
||||
|
||||
export type AgGridFetchFn<TResponse, TFilter> = (
|
||||
args: { filter: TFilter; limit: number; startIndex: number },
|
||||
|
|
@ -44,24 +47,24 @@ interface UseAgGridProps<TFilter> {
|
|||
itemCount?: number;
|
||||
itemType: LibraryItem;
|
||||
pageKey: string;
|
||||
server: ServerListItem | null;
|
||||
server: null | ServerListItem;
|
||||
tableRef: MutableRefObject<AgGridReactType | null>;
|
||||
}
|
||||
|
||||
const BLOCK_SIZE = 500;
|
||||
|
||||
export const useVirtualTable = <TFilter extends BaseQuery<any>>({
|
||||
server,
|
||||
tableRef,
|
||||
pageKey,
|
||||
itemType,
|
||||
columnType,
|
||||
contextMenu,
|
||||
itemCount,
|
||||
customFilters,
|
||||
isSearchParams,
|
||||
isClientSide,
|
||||
isClientSideSort,
|
||||
columnType,
|
||||
isSearchParams,
|
||||
itemCount,
|
||||
itemType,
|
||||
pageKey,
|
||||
server,
|
||||
tableRef,
|
||||
}: UseAgGridProps<TFilter>) => {
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
|
|
|
|||
|
|
@ -1,30 +1,42 @@
|
|||
import { Ref, forwardRef, useRef, useEffect, useCallback, useMemo } from 'react';
|
||||
import type {
|
||||
ICellRendererParams,
|
||||
ValueGetterParams,
|
||||
IHeaderParams,
|
||||
ValueFormatterParams,
|
||||
ColDef,
|
||||
ColumnMovedEvent,
|
||||
NewColumnsLoadedEvent,
|
||||
GridReadyEvent,
|
||||
GridSizeChangedEvent,
|
||||
ICellRendererParams,
|
||||
IHeaderParams,
|
||||
ModelUpdatedEvent,
|
||||
NewColumnsLoadedEvent,
|
||||
ValueFormatterParams,
|
||||
ValueGetterParams,
|
||||
} from '@ag-grid-community/core';
|
||||
import type { AgGridReactProps } from '@ag-grid-community/react';
|
||||
import { AgGridReact } 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 formatDuration from 'format-duration';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import { forwardRef, Ref, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { generatePath } from 'react-router';
|
||||
import styled from 'styled-components';
|
||||
|
||||
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 {
|
||||
|
|
@ -32,27 +44,17 @@ import {
|
|||
TableColumn,
|
||||
TablePagination as TablePaginationType,
|
||||
} from '/@/renderer/types';
|
||||
import { FavoriteCell } from '/@/renderer/components/virtual-table/cells/favorite-cell';
|
||||
import { RatingCell } from '/@/renderer/components/virtual-table/cells/rating-cell';
|
||||
import { TablePagination } from '/@/renderer/components/virtual-table/table-pagination';
|
||||
import { ActionsCell } from '/@/renderer/components/virtual-table/cells/actions-cell';
|
||||
import { TitleCell } from '/@/renderer/components/virtual-table/cells/title-cell';
|
||||
import { useFixedTableHeader } from '/@/renderer/components/virtual-table/hooks/use-fixed-table-header';
|
||||
import { NoteCell } from '/@/renderer/components/virtual-table/cells/note-cell';
|
||||
import { RowIndexCell } from '/@/renderer/components/virtual-table/cells/row-index-cell';
|
||||
import i18n from '/@/i18n/i18n';
|
||||
import {
|
||||
formatDateAbsolute,
|
||||
formatDateAbsoluteUTC,
|
||||
formatDateRelative,
|
||||
formatSizeString,
|
||||
} from '/@/renderer/utils/format';
|
||||
import { useTableChange } from '/@/renderer/hooks/use-song-change';
|
||||
|
||||
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 './hooks/use-fixed-table-header';
|
||||
export * from './hooks/use-click-outside-deselect';
|
||||
export * from './utils';
|
||||
|
||||
const TableWrapper = styled.div`
|
||||
|
|
@ -491,16 +493,16 @@ export const VirtualTable = forwardRef(
|
|||
(
|
||||
{
|
||||
autoFitColumns,
|
||||
deselectOnClickOutside,
|
||||
autoHeight,
|
||||
stickyHeader,
|
||||
transparentHeader,
|
||||
deselectOnClickOutside,
|
||||
onColumnMoved,
|
||||
onNewColumnsLoaded,
|
||||
onGridReady,
|
||||
onGridSizeChanged,
|
||||
onNewColumnsLoaded,
|
||||
paginationProps,
|
||||
shouldUpdateSong,
|
||||
stickyHeader,
|
||||
transparentHeader,
|
||||
...rest
|
||||
}: VirtualTableProps,
|
||||
ref: Ref<AgGridReactType | null>,
|
||||
|
|
@ -584,7 +586,7 @@ export const VirtualTable = forwardRef(
|
|||
[autoFitColumns],
|
||||
);
|
||||
|
||||
const { tableHeaderRef, tableContainerRef } = useFixedTableHeader({
|
||||
const { tableContainerRef, tableHeaderRef } = useFixedTableHeader({
|
||||
enabled: stickyHeader || false,
|
||||
});
|
||||
|
||||
|
|
@ -592,32 +594,32 @@ export const VirtualTable = forwardRef(
|
|||
|
||||
return (
|
||||
<TableWrapper
|
||||
ref={mergedWrapperRef}
|
||||
className={
|
||||
transparentHeader
|
||||
? 'ag-theme-alpine-dark ag-header-transparent'
|
||||
: 'ag-theme-alpine-dark'
|
||||
}
|
||||
ref={mergedWrapperRef}
|
||||
>
|
||||
<DummyHeader ref={tableHeaderRef} />
|
||||
<AgGridReact
|
||||
ref={mergedRef}
|
||||
animateRows
|
||||
maintainColumnOrder
|
||||
suppressAsyncEvents
|
||||
suppressContextMenu
|
||||
suppressCopyRowsToClipboard
|
||||
suppressMoveWhenRowDragging
|
||||
suppressPaginationPanel
|
||||
suppressScrollOnNewData
|
||||
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}
|
||||
|
|
@ -627,9 +629,9 @@ export const VirtualTable = forwardRef(
|
|||
/>
|
||||
{paginationProps && (
|
||||
<AnimatePresence
|
||||
presenceAffectsLayout
|
||||
initial={false}
|
||||
mode="wait"
|
||||
presenceAffectsLayout
|
||||
>
|
||||
<TablePagination
|
||||
{...paginationProps}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
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 { useSettingsStoreActions, useSettingsStore } from '/@/renderer/store/settings.store';
|
||||
import { useSettingsStore, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
||||
import { TableColumn, TableType } from '/@/renderer/types';
|
||||
import { Option } from '/@/renderer/components/option';
|
||||
import i18n from '/@/i18n/i18n';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const SONG_TABLE_COLUMNS = [
|
||||
{
|
||||
|
|
@ -408,8 +410,8 @@ export const TableConfigDropdown = ({ type }: TableConfigDropdownProps) => {
|
|||
label={(value) => `Item size: ${value}`}
|
||||
max={100}
|
||||
min={25}
|
||||
w="100%"
|
||||
onChangeEnd={handleUpdateRowHeight}
|
||||
w="100%"
|
||||
/>
|
||||
</Option.Control>
|
||||
</Option>
|
||||
|
|
@ -420,8 +422,8 @@ export const TableConfigDropdown = ({ type }: TableConfigDropdownProps) => {
|
|||
data={SONG_TABLE_COLUMNS}
|
||||
defaultValue={tableConfig[type]?.columns.map((column) => column.column)}
|
||||
dropdownPosition="bottom"
|
||||
width={300}
|
||||
onChange={handleAddOrRemoveColumns}
|
||||
width={300}
|
||||
/>
|
||||
</Option.Control>
|
||||
</Option>
|
||||
|
|
|
|||
|
|
@ -1,19 +1,22 @@
|
|||
import { MutableRefObject } from 'react';
|
||||
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 { MotionFlex } from '../motion';
|
||||
|
||||
import { Button } from '/@/renderer/components/button';
|
||||
import { NumberInput } from '/@/renderer/components/input';
|
||||
import { Pagination } from '/@/renderer/components/pagination';
|
||||
import { Popover } from '/@/renderer/components/popover';
|
||||
import { Text } from '/@/renderer/components/text';
|
||||
import { useContainerQuery } from '/@/renderer/hooks';
|
||||
import { TablePagination as TablePaginationType } from '/@/renderer/types';
|
||||
import { ListKey } from '/@/renderer/store';
|
||||
import { TablePagination as TablePaginationType } from '/@/renderer/types';
|
||||
|
||||
interface TablePaginationProps {
|
||||
pageKey: ListKey;
|
||||
|
|
@ -25,10 +28,10 @@ interface TablePaginationProps {
|
|||
|
||||
export const TablePagination = ({
|
||||
pageKey,
|
||||
tableRef,
|
||||
pagination,
|
||||
setPagination,
|
||||
setIdPagination,
|
||||
setPagination,
|
||||
tableRef,
|
||||
}: TablePaginationProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [isGoToPageOpen, handlers] = useDisclosure(false);
|
||||
|
|
@ -70,14 +73,14 @@ export const TablePagination = ({
|
|||
|
||||
return (
|
||||
<MotionFlex
|
||||
ref={containerQuery.ref}
|
||||
layout
|
||||
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)' }}
|
||||
>
|
||||
<Text
|
||||
|
|
@ -102,18 +105,19 @@ export const TablePagination = ({
|
|||
)}
|
||||
</Text>
|
||||
<Group
|
||||
ref={containerQuery.ref}
|
||||
noWrap
|
||||
ref={containerQuery.ref}
|
||||
spacing="sm"
|
||||
>
|
||||
<Popover
|
||||
trapFocus
|
||||
onClose={() => handlers.close()}
|
||||
opened={isGoToPageOpen}
|
||||
position="bottom-start"
|
||||
onClose={() => handlers.close()}
|
||||
trapFocus
|
||||
>
|
||||
<Popover.Target>
|
||||
<Button
|
||||
onClick={() => handlers.toggle()}
|
||||
radius="sm"
|
||||
size="sm"
|
||||
sx={{ height: '26px', padding: '0', width: '26px' }}
|
||||
|
|
@ -121,7 +125,6 @@ export const TablePagination = ({
|
|||
label: t('action.goToPage', { postProcess: 'sentenceCase' }),
|
||||
}}
|
||||
variant="default"
|
||||
onClick={() => handlers.toggle()}
|
||||
>
|
||||
<RiHashtag size={15} />
|
||||
</Button>
|
||||
|
|
@ -147,14 +150,14 @@ export const TablePagination = ({
|
|||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
<Pagination
|
||||
noWrap
|
||||
$hideDividers={!containerQuery.isSm}
|
||||
boundaries={1}
|
||||
noWrap
|
||||
onChange={handlePagination}
|
||||
radius="sm"
|
||||
siblings={containerQuery.isMd ? 2 : containerQuery.isSm ? 1 : 0}
|
||||
total={pagination.totalPages - 1}
|
||||
value={pagination.currentPage + 1}
|
||||
onChange={handlePagination}
|
||||
/>
|
||||
</Group>
|
||||
</MotionFlex>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { GridApi, RowNode } from '@ag-grid-community/core';
|
|||
export const getNodesByDiscNumber = (args: {
|
||||
api: GridApi;
|
||||
discNumber: number;
|
||||
subtitle: string | null;
|
||||
subtitle: null | string;
|
||||
}) => {
|
||||
const { api, discNumber, subtitle } = args;
|
||||
|
||||
|
|
@ -21,7 +21,7 @@ export const setNodeSelection = (args: {
|
|||
isSelected: boolean;
|
||||
nodes: RowNode<any>[];
|
||||
}) => {
|
||||
const { nodes, isSelected } = args;
|
||||
const { isSelected, nodes } = args;
|
||||
|
||||
nodes.forEach((node) => {
|
||||
node.setSelected(isSelected);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue