Begin migration (round 1)

This commit is contained in:
jeffvli 2024-05-24 22:33:38 -07:00
parent 875d302a2f
commit c4378e3f11
140 changed files with 2058 additions and 3537 deletions

2830
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -217,7 +217,7 @@
"@types/react-window": "^1.8.5",
"@types/react-window-infinite-loader": "^1.0.6",
"@types/sanitize-html": "^2.11.0",
"@types/styled-components": "^5.1.26",
"@types/styled-components": "^5.1.34",
"@types/terser-webpack-plugin": "^5.0.4",
"@types/webpack-bundle-analyzer": "^4.4.1",
"@types/webpack-env": "^1.16.3",
@ -342,15 +342,15 @@
"react-i18next": "^11.18.6",
"react-icons": "^4.10.1",
"react-player": "^2.11.0",
"react-router": "^6.16.0",
"react-router-dom": "^6.16.0",
"react-router": "^6.23.0",
"react-router-dom": "^6.23.0",
"react-simple-img": "^3.0.0",
"react-virtualized-auto-sizer": "^1.0.17",
"react-window": "^1.8.9",
"react-window-infinite-loader": "^1.0.9",
"sanitize-html": "^2.13.0",
"semver": "^7.5.4",
"styled-components": "^6.0.8",
"styled-components": "^6.1.0",
"swiper": "^9.3.1",
"zod": "^3.22.3",
"zustand": "^4.3.9"

View File

@ -14,10 +14,8 @@ export const App = () => {
return (
<MantineProvider
withGlobalStyles
withNormalizeCSS
forceColorScheme={isDark ? 'dark' : 'light'}
theme={{
colorScheme: isDark ? 'dark' : 'light',
components: {
AppShell: {
styles: {
@ -44,18 +42,17 @@ export const App = () => {
},
},
defaultRadius: 'xs',
dir: 'ltr',
focusRing: 'auto',
focusRingStyles: {
inputStyles: () => ({
border: '1px solid var(--primary-color)',
}),
resetStyles: () => ({ outline: 'none' }),
styles: () => ({
outline: '1px solid var(--primary-color)',
outlineOffset: '-1px',
}),
},
// focusRingStyles: {
// inputStyles: () => ({
// border: '1px solid var(--primary-color)',
// }),
// resetStyles: () => ({ outline: 'none' }),
// styles: () => ({
// outline: '1px solid var(--primary-color)',
// outlineOffset: '-1px',
// }),
// },
fontFamily: 'var(--content-font-family)',
fontSizes: {
lg: '1.1rem',
@ -66,7 +63,7 @@ export const App = () => {
},
headings: {
fontFamily: 'var(--content-font-family)',
fontWeight: 700,
fontWeight: '700',
},
other: {},
spacing: {

View File

@ -45,7 +45,7 @@ export const RemoteContainer = () => {
<Title order={2}>Album: {song.album}</Title>
<Title order={2}>Artist: {song.artistName}</Title>
</Group>
<Group position="apart">
<Group justify="space-between">
<Title order={3}>Duration: {formatDuration(song.duration)}</Title>
{song.releaseDate && (
<Title order={3}>
@ -58,7 +58,7 @@ export const RemoteContainer = () => {
)}
<Group
grow
spacing={0}
gap={0}
>
<RemoteButton
disabled={!song}
@ -97,7 +97,7 @@ export const RemoteContainer = () => {
</Group>
<Group
grow
spacing={0}
gap={0}
>
<RemoteButton
$active={shuffle || false}
@ -145,7 +145,7 @@ export const RemoteContainer = () => {
openDelay={1000}
>
<Rating
sx={{ margin: 'auto' }}
style={{ margin: 'auto' }}
value={song.userRating ?? 0}
onChange={debouncedSetRating}
onDoubleClick={() => debouncedSetRating(0)}
@ -159,8 +159,8 @@ export const RemoteContainer = () => {
max={100}
rightLabel={
<Text
fw={600}
size="xs"
weight={600}
>
{volume ?? 0}
</Text>

View File

@ -1,65 +1,57 @@
import {
AppShell,
Container,
Flex,
Grid,
Header,
Image,
MediaQuery,
Skeleton,
Title,
} from '@mantine/core';
import { AppShell, Container, Flex, Grid, Image, Skeleton } from '@mantine/core';
import { ThemeButton } from '/@/remote/components/buttons/theme-button';
import { ImageButton } from '/@/remote/components/buttons/image-button';
import { RemoteContainer } from '/@/remote/components/remote-container';
import { ReconnectButton } from '/@/remote/components/buttons/reconnect-button';
import { useConnected } from '/@/remote/store';
// TODO: Fix media query
export const Shell = () => {
const connected = useConnected();
return (
<AppShell
header={
<Header height={60}>
<Grid>
<Grid.Col span="auto">
<div>
<Image
fit="contain"
height={60}
src="/favicon.ico"
width={60}
/>
</div>
</Grid.Col>
<MediaQuery
smallerThan="sm"
styles={{ display: 'none' }}
>
<Grid.Col
sm={6}
xs={0}
>
<Title ta="center">Feishin Remote</Title>
</Grid.Col>
</MediaQuery>
<Grid.Col span="auto">
<Flex
direction="row"
justify="right"
>
<ReconnectButton />
<ImageButton />
<ThemeButton />
</Flex>
</Grid.Col>
</Grid>
</Header>
}
header={{ height: 60 }}
padding="md"
>
<AppShell.Header>
<Grid>
<Grid.Col span="auto">
<div>
<Image
fit="contain"
height={60}
src="/favicon.ico"
width={60}
/>
</div>
</Grid.Col>
{/* <MediaQuery
smallerThan="sm"
styles={{ display: 'none' }}
>
<Grid.Col
span={{
sm: 6,
xs: 0,
}}
>
<Title ta="center">Feishin Remote</Title>
</Grid.Col>
</MediaQuery> */}
<Grid.Col span="auto">
<Flex
direction="row"
justify="right"
>
<ReconnectButton />
<ImageButton />
<ThemeButton />
</Flex>
</Grid.Col>
</Grid>
</AppShell.Header>
<Container>
{connected ? (
<RemoteContainer />

View File

@ -1,5 +1,5 @@
import { hideNotification, showNotification } from '@mantine/notifications';
import type { NotificationProps as MantineNotificationProps } from '@mantine/notifications';
import type { NotificationData as MantineNotificationProps } from '@mantine/notifications';
import merge from 'lodash/merge';
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';

View File

@ -1,30 +1,32 @@
import { useEffect, useMemo, useRef } from 'react';
import { ClientSideRowModelModule } from '@ag-grid-community/client-side-row-model';
import { ModuleRegistry } from '@ag-grid-community/core';
import { InfiniteRowModelModule } from '@ag-grid-community/infinite-row-model';
import '@ag-grid-community/styles/ag-grid.css';
import { MantineProvider } from '@mantine/core';
import isElectron from 'is-electron';
import { initSimpleImg } from 'react-simple-img';
import { toast } from './components';
import { useTheme } from './hooks';
import { IsUpdatedDialog } from './is-updated-dialog';
import { AppRouter } from './router/app-router';
import {
useHotkeySettings,
usePlaybackSettings,
useRemoteSettings,
useSettingsStore,
} from './store/settings.store';
import './styles/global.scss';
import { ContextMenuProvider } from '/@/renderer/features/context-menu';
import { useHandlePlayQueueAdd } from '/@/renderer/features/player/hooks/use-handle-playqueue-add';
import { PlayQueueHandlerContext } from '/@/renderer/features/player';
import { getMpvProperties } from '/@/renderer/features/settings/components/playback/mpv-settings';
import { PlayerState, usePlayerStore, useQueueControls } from '/@/renderer/store';
import { FontType, PlaybackType, PlayerStatus } from '/@/renderer/types';
import '@ag-grid-community/styles/ag-grid.css';
import { useDiscordRpc } from '/@/renderer/features/discord-rpc/use-discord-rpc';
import { useTheme } from '/@/renderer/hooks';
import { AppRouter } from '/@/renderer/router/app-router';
import { useRef, useEffect, useMemo } from 'react';
import i18n from '/@/i18n/i18n';
import { toast } from '/@/renderer/components';
import { useDiscordRpc } from '/@/renderer/features/discord-rpc/use-discord-rpc';
import { useHandlePlayQueueAdd } from '/@/renderer/features/player/hooks/use-handle-playqueue-add';
import { getMpvProperties } from '/@/renderer/features/settings/components/playback/mpv-settings';
import { useServerVersion } from '/@/renderer/hooks/use-server-version';
import {
useSettingsStore,
usePlaybackSettings,
useHotkeySettings,
useQueueControls,
useRemoteSettings,
usePlayerStore,
PlayerState,
} from '/@/renderer/store';
import { FontType, PlaybackType, PlayerStatus } from '/@/renderer/types';
import { PlayQueueHandlerContext } from '/@/renderer/features/player';
import { ContextMenuProvider } from '/@/renderer/features/context-menu';
import { IsUpdatedDialog } from '/@/renderer/is-updated-dialog';
ModuleRegistry.registerModules([ClientSideRowModelModule, InfiniteRowModelModule]);
@ -189,13 +191,13 @@ export const App = () => {
}, [language]);
return (
<>
<MantineProvider forceColorScheme={theme as 'light' | 'dark'}>
<PlayQueueHandlerContext.Provider value={providerValue}>
<ContextMenuProvider>
<AppRouter />
</ContextMenuProvider>
</PlayQueueHandlerContext.Provider>
<IsUpdatedDialog />
</>
</MantineProvider>
);
};

View File

@ -0,0 +1,64 @@
import type { ActionIconProps as MantineActionIconProps } from '@mantine/core';
import { createPolymorphicComponent, ActionIcon as MantineActionIcon } from '@mantine/core';
import styled from 'styled-components';
import { BadgeProps } from '/@/renderer/components/badge';
export type ActionIconProps = MantineActionIconProps;
const StyledActionIcon = styled(MantineActionIcon)<ActionIconProps>`
color: ${(props) => `var(--btn-${props.variant}-fg)`};
background: ${(props) => `var(--btn-${props.variant}-bg)`};
border: ${(props) => `var(--btn-${props.variant}-border)`};
border-radius: ${(props) => `var(--btn-${props.variant}-radius)`};
transition: background 0.2s ease-in-out, color 0.2s ease-in-out, border 0.2s ease-in-out;
svg {
fill: ${(props) => `var(--btn-${props.variant}-fg)`};
transition: fill 0.2s ease-in-out;
}
&:disabled {
color: ${(props) => `var(--btn-${props.variant}-fg)`};
background: ${(props) => `var(--btn-${props.variant}-bg)`};
opacity: 0.6;
}
&:not([data-disabled])&:hover {
color: ${(props) => `var(--btn-${props.variant}-fg) !important`};
background: ${(props) => `var(--btn-${props.variant}-bg)`};
filter: brightness(85%);
border: ${(props) => `var(--btn-${props.variant}-border-hover)`};
svg {
fill: ${(props) => `var(--btn-${props.variant}-fg-hover)`};
}
}
&:not([data-disabled])&:focus-visible {
color: ${(props) => `var(--btn-${props.variant}-fg-hover)`};
background: ${(props) => `var(--btn-${props.variant}-bg)`};
filter: brightness(85%);
}
& .mantine-ActionIcon-centerLoader {
display: none;
}
`;
const _ActionIcon = ({ children, ...props }: ActionIconProps) => {
return (
<StyledActionIcon
radius="md"
size="sm"
styles={{
root: { background: 'var(--badge-bg)' },
}}
{...props}
>
{children}
</StyledActionIcon>
);
};
export const ActionIcon = createPolymorphicComponent<'button', BadgeProps>(_ActionIcon);

View File

@ -0,0 +1,136 @@
import { forwardRef, HTMLAttributes, ReactNode } from 'react';
import styled, { CSSProperties } from 'styled-components';
import { StyledComponentsStyleProps, StyleProps } from '/@/renderer/components/shared';
interface BoxProps extends HTMLAttributes<HTMLDivElement>, StyleProps {
align?: CSSProperties['alignItems'];
children?: ReactNode;
columnGap?: CSSProperties['columnGap'];
direction?: CSSProperties['flexDirection'];
gap?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | CSSProperties['gap'];
justify?: CSSProperties['justifyContent'];
rowGap?: CSSProperties['rowGap'];
wrap?: CSSProperties['flexWrap'];
}
export const Box = forwardRef<HTMLDivElement, BoxProps>(({ children, ...props }: BoxProps, ref) => {
const {
bg,
bottom,
display,
fw,
h,
left,
lts,
m,
mah,
maw,
mb,
mih,
miw,
ml,
mr,
mt,
mx,
my,
p,
pb,
pl,
pos,
pr,
pt,
px,
py,
right,
ta,
td,
top,
tt,
w,
...htmlProps
} = props;
const styleProps = {
$bg: bg,
$bottom: bottom,
$display: display,
$fw: fw,
$h: h,
$left: left,
$lts: lts,
$m: m,
$mah: mah,
$maw: maw,
$mb: mb,
$mih: mih,
$miw: miw,
$ml: ml,
$mr: mr,
$mt: mt,
$mx: mx,
$my: my,
$p: p,
$pb: pb,
$pl: pl,
$pos: pos,
$pr: pr,
$pt: pt,
$px: px,
$py: py,
$right: right,
$ta: ta,
$td: td,
$top: top,
$tt: tt,
$w: w,
};
return (
<StyledBox
ref={ref}
{...styleProps}
{...htmlProps}
>
{children}
</StyledBox>
);
});
const StyledBox = styled.div<StyledComponentsStyleProps>`
${(props) => props.$bg && `background-color: ${props.$bg};`}
${(props) => props.$bottom && `bottom: ${props.$bottom};`}
${(props) => props.$display && `display: ${props.$display};`}
${(props) => props.$fw && `font-weight: ${props.$fw};`}
${(props) => props.$h && `height: ${props.$h};`}
${(props) => props.$left && `left: ${props.$left};`}
${(props) => props.$lts && `letter-spacing: ${props.$lts};`}
${(props) => props.$m && `margin: ${props.$m};`}
${(props) => props.$mah && `max-height: ${props.$mah};`}
${(props) => props.$maw && `max-width: ${props.$maw};`}
${(props) => props.$mb && `margin-bottom: ${props.$mb};`}
${(props) => props.$mih && `min-height: ${props.$mih};`}
${(props) => props.$miw && `min-width: ${props.$miw};`}
${(props) => props.$ml && `margin-left: ${props.$ml};`}
${(props) => props.$mr && `margin-right: ${props.$mr};`}
${(props) => props.$mt && `margin-top: ${props.$mt};`}
${(props) => props.$mx && `margin-left: ${props.$mx};`}
${(props) => props.$mx && `margin-right: ${props.$mx};`}
${(props) => props.$my && `margin-top: ${props.$my};`}
${(props) => props.$my && `margin-bottom: ${props.$my};`}
${(props) => props.$p && `padding: ${props.$p};`}
${(props) => props.$pb && `padding-bottom: ${props.$pb};`}
${(props) => props.$pl && `padding-left: ${props.$pl};`}
${(props) => props.$pos && `position: ${props.$pos};`}
${(props) => props.$pr && `padding-right: ${props.$pr};`}
${(props) => props.$pt && `padding-top: ${props.$pt};`}
${(props) => props.$px && `padding-left: ${props.$px};`}
${(props) => props.$px && `padding-right: ${props.$px};`}
${(props) => props.$py && `padding-top: ${props.$py};`}
${(props) => props.$py && `padding-bottom: ${props.$py};`}
${(props) => props.$right && `right: ${props.$right};`}
${(props) => props.$ta && `text-align: ${props.$ta};`}
${(props) => props.$td && `text-decoration: ${props.$td};`}
${(props) => props.$top && `top: ${props.$top};`}
${(props) => props.$tt && `text-transform: ${props.$tt};`}
${(props) => props.$w && `width: ${props.$w};`}
`;

View File

@ -92,7 +92,6 @@ export const _Button = forwardRef<HTMLButtonElement, ButtonProps>(
>
<StyledButton
ref={ref}
loaderPosition="center"
{...props}
>
<ButtonChildWrapper $loading={props.loading}>{children}</ButtonChildWrapper>
@ -109,7 +108,6 @@ export const _Button = forwardRef<HTMLButtonElement, ButtonProps>(
return (
<StyledButton
ref={ref}
loaderPosition="center"
{...props}
>
<ButtonChildWrapper $loading={props.loading}>{children}</ButtonChildWrapper>
@ -171,7 +169,7 @@ export const TimeoutButton = ({ timeoutProps, ...props }: HoldButtonProps) => {
return (
<Button
sx={{ color: 'var(--danger-color)' }}
style={{ color: 'var(--danger-color)' }}
onClick={startTimeout}
{...props}
>

View File

@ -156,7 +156,7 @@ export const AlbumCard = ({
/>
) : (
<Center
sx={{
style={{
background: 'var(--placeholder-bg)',
borderRadius: 'var(--card-default-radius)',
height: `${size}px`,

View File

@ -1,7 +1,6 @@
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';
@ -14,6 +13,7 @@ import {
ALBUM_CONTEXT_MENU_ITEMS,
ARTIST_CONTEXT_MENU_ITEMS,
} from '/@/renderer/features/context-menu/context-menu-items';
import { Group } from '/@/renderer/components/group';
type PlayButtonType = UnstyledButtonProps & React.ComponentPropsWithoutRef<'button'>;
@ -137,11 +137,11 @@ export const CardControls = ({
<PlayButton onClick={handlePlay}>
<RiPlayFill size={25} />
</PlayButton>
<Group spacing="xs">
<Group gap="xs">
<SecondaryButton
disabled
p={5}
sx={{ svg: { fill: 'white !important' } }}
style={{ svg: { fill: 'white !important' } }}
variant="subtle"
>
<FavoriteWrapper isFavorite={itemData?.isFavorite}>
@ -157,7 +157,7 @@ export const CardControls = ({
</SecondaryButton>
<SecondaryButton
p={5}
sx={{ svg: { fill: 'white !important' } }}
style={{ svg: { fill: 'white !important' } }}
variant="subtle"
onClick={(e) => {
e.preventDefault();

View File

@ -40,7 +40,7 @@ export const CardRows = ({ data, rows }: CardRowsProps) => {
<Text
$noSelect
$secondary
sx={{
style={{
display: 'inline-block',
padding: '0 2px 0 1px',
}}

View File

@ -151,7 +151,7 @@ export const PosterCard = ({
/>
) : (
<Center
sx={{
style={{
background: 'var(--placeholder-bg)',
borderRadius: 'var(--card-default-radius)',
height: '100%',
@ -190,7 +190,7 @@ export const PosterCard = ({
<ImageContainerSkeleton />
</Skeleton>
<DetailContainer>
<Stack spacing="sm">
<Stack gap="sm">
{controls.cardRows.map((row, index) => (
<Skeleton
key={`${index}-${row.arrayProperty}`}

View File

@ -0,0 +1,134 @@
import { forwardRef, ReactNode, HTMLAttributes } from 'react';
import { Center as MantineCenter } from '@mantine/core';
import styled from 'styled-components';
import { StyleProps, StyledComponentsStyleProps } from '/@/renderer/components/shared';
interface CenterProps extends HTMLAttributes<HTMLDivElement>, StyleProps {
children?: ReactNode;
inline?: boolean;
}
export const Center = forwardRef<HTMLDivElement, CenterProps>(
({ inline, children, ...props }: CenterProps, ref) => {
const {
bg,
bottom,
display,
fw,
h,
left,
lts,
m,
mah,
maw,
mb,
mih,
miw,
ml,
mr,
mt,
mx,
my,
p,
pb,
pl,
pos,
pr,
pt,
px,
py,
right,
ta,
td,
top,
tt,
w,
...htmlProps
} = props;
const styleProps = {
$bg: bg,
$bottom: bottom,
$display: display,
$fw: fw,
$h: h,
$left: left,
$lts: lts,
$m: m,
$mah: mah,
$maw: maw,
$mb: mb,
$mih: mih,
$miw: miw,
$ml: ml,
$mr: mr,
$mt: mt,
$mx: mx,
$my: my,
$p: p,
$pb: pb,
$pl: pl,
$pos: pos,
$pr: pr,
$pt: pt,
$px: px,
$py: py,
$right: right,
$ta: ta,
$td: td,
$top: top,
$tt: tt,
$w: w,
};
return (
<StyledCenter
ref={ref}
inline={inline}
{...styleProps}
{...htmlProps}
>
{children}
</StyledCenter>
);
},
);
const StyledCenter = styled(MantineCenter)<StyledComponentsStyleProps>`
${(props) => props.$bg && `background-color: ${props.$bg};`}
${(props) => props.$bottom && `bottom: ${props.$bottom};`}
${(props) => props.$display && `display: ${props.$display};`}
${(props) => props.$fw && `font-weight: ${props.$fw};`}
${(props) => props.$h && `height: ${props.$h};`}
${(props) => props.$left && `left: ${props.$left};`}
${(props) => props.$lts && `letter-spacing: ${props.$lts};`}
${(props) => props.$m && `margin: ${props.$m};`}
${(props) => props.$mah && `max-height: ${props.$mah};`}
${(props) => props.$maw && `max-width: ${props.$maw};`}
${(props) => props.$mb && `margin-bottom: ${props.$mb};`}
${(props) => props.$mih && `min-height: ${props.$mih};`}
${(props) => props.$miw && `min-width: ${props.$miw};`}
${(props) => props.$ml && `margin-left: ${props.$ml};`}
${(props) => props.$mr && `margin-right: ${props.$mr};`}
${(props) => props.$mt && `margin-top: ${props.$mt};`}
${(props) => props.$mx && `margin-left: ${props.$mx};`}
${(props) => props.$mx && `margin-right: ${props.$mx};`}
${(props) => props.$my && `margin-top: ${props.$my};`}
${(props) => props.$my && `margin-bottom: ${props.$my};`}
${(props) => props.$p && `padding: ${props.$p};`}
${(props) => props.$pb && `padding-bottom: ${props.$pb};`}
${(props) => props.$pl && `padding-left: ${props.$pl};`}
${(props) => props.$pos && `position: ${props.$pos};`}
${(props) => props.$pr && `padding-right: ${props.$pr};`}
${(props) => props.$pt && `padding-top: ${props.$pt};`}
${(props) => props.$px && `padding-left: ${props.$px};`}
${(props) => props.$px && `padding-right: ${props.$px};`}
${(props) => props.$py && `padding-top: ${props.$py};`}
${(props) => props.$py && `padding-bottom: ${props.$py};`}
${(props) => props.$right && `right: ${props.$right};`}
${(props) => props.$ta && `text-align: ${props.$ta};`}
${(props) => props.$td && `text-decoration: ${props.$td};`}
${(props) => props.$top && `top: ${props.$top};`}
${(props) => props.$tt && `text-transform: ${props.$tt};`}
${(props) => props.$w && `width: ${props.$w};`}
`;

View File

@ -1,7 +1,8 @@
import { ComponentPropsWithoutRef, forwardRef, ReactNode, Ref } from 'react';
import { Box, Group, UnstyledButton, UnstyledButtonProps } from '@mantine/core';
import { UnstyledButton, UnstyledButtonProps } from '@mantine/core';
import { motion, Variants } from 'framer-motion';
import styled from 'styled-components';
import { Group } from '/@/renderer/components/group';
interface ContextMenuProps {
children: ReactNode;
@ -80,12 +81,12 @@ export const ContextMenuButton = forwardRef(
disabled={props.disabled}
onClick={props.onClick}
>
<Group position="apart">
<Group spacing="md">
<Box>{leftIcon}</Box>
<Box mr="2rem">{children}</Box>
<Group justify="space-between">
<Group gap="md">
<div>{leftIcon}</div>
<div style={{ marginRight: '2rem' }}>{children}</div>
</Group>
<Box>{rightIcon}</Box>
<div>{rightIcon}</div>
</Group>
</StyledContextMenuButton>
);

View File

@ -38,7 +38,7 @@ export const DatePicker = ({ width, maxWidth, ...props }: DatePickerProps) => {
return (
<StyledDatePicker
{...props}
sx={{ maxWidth, width }}
style={{ maxWidth, width }}
/>
);
};

View File

@ -0,0 +1,44 @@
import { forwardRef, HTMLAttributes, CSSProperties } from 'react';
import { Divider as MantineDivider } from '@mantine/core';
import styled from 'styled-components';
interface DividerProps extends HTMLAttributes<HTMLDivElement> {
color?: string;
label?: string;
labelPosition?: 'left' | 'center' | 'right';
marginX?: CSSProperties['margin'];
marginY?: CSSProperties['margin'];
orientation?: 'horizontal' | 'vertical';
}
export const Divider = forwardRef<HTMLDivElement, DividerProps>(
(
{ color, label, labelPosition, marginX, marginY, orientation, ...props }: DividerProps,
ref,
) => {
return (
<StyledDivider
ref={ref}
$color={color}
$marginX={marginX}
$marginY={marginY}
label={label}
labelPosition={labelPosition}
orientation={orientation}
{...props}
/>
);
},
);
const StyledDivider = styled(MantineDivider)<{
$color?: DividerProps['color'];
$marginX?: DividerProps['marginX'];
$marginY?: DividerProps['marginY'];
}>`
margin-top: ${(props) => props.$marginY};
margin-bottom: ${(props) => props.$marginY};
margin-left: ${(props) => props.$marginX};
margin-right: ${(props) => props.$marginX};
border-color: ${(props) => props.$color || 'var(--generic-border-color)'} !important;
`;

View File

@ -1,6 +1,6 @@
import type { MouseEvent } from 'react';
import { useState } from 'react';
import { Group, Image, Stack } from '@mantine/core';
import { Image } from '@mantine/core';
import type { Variants } from 'framer-motion';
import { AnimatePresence, motion } from 'framer-motion';
import { useTranslation } from 'react-i18next';
@ -15,6 +15,8 @@ import { AppRoute } from '/@/renderer/router/routes';
import { usePlayQueueAdd } from '/@/renderer/features/player/hooks/use-playqueue-add';
import { Play } from '/@/renderer/types';
import { usePlayButtonBehavior } from '/@/renderer/store';
import { Group } from '/@/renderer/components/group';
import { Stack } from '/@/renderer/components/stack';
const Carousel = styled(motion.div)`
position: relative;
@ -166,21 +168,21 @@ export const FeatureCarousel = ({ data }: FeatureCarouselProps) => {
placeholder="var(--card-default-bg)"
radius="md"
src={data[itemIndex]?.imageUrl}
sx={{ objectFit: 'cover' }}
style={{ objectFit: 'cover' }}
width={225}
/>
</ImageColumn>
<InfoColumn>
<Stack
spacing="md"
sx={{ width: '100%' }}
gap="md"
style={{ width: '100%' }}
>
<TitleWrapper>
<TextTitle
lh="3.5rem"
order={1}
overflow="hidden"
sx={{ fontSize: '3.5rem' }}
style={{ fontSize: '3.5rem' }}
weight={900}
>
{currentItem?.name}
@ -209,7 +211,7 @@ export const FeatureCarousel = ({ data }: FeatureCarouselProps) => {
<Badge size="lg">{currentItem?.releaseYear}</Badge>
<Badge size="lg">{currentItem?.songCount} tracks</Badge>
</Group>
<Group position="apart">
<Group justify="space-between">
<Button
size="lg"
style={{ borderRadius: '5rem' }}
@ -237,7 +239,7 @@ export const FeatureCarousel = ({ data }: FeatureCarouselProps) => {
{ postProcess: 'titleCase' },
)}
</Button>
<Group spacing="sm">
<Group gap="sm">
<Button
radius="lg"
size="sm"

View File

@ -0,0 +1,149 @@
import { forwardRef, HTMLAttributes, ReactNode } from 'react';
import { Flex as MantineFlex } from '@mantine/core';
import styled, { CSSProperties } from 'styled-components';
import { StyleProps, StyledComponentsStyleProps } from '/@/renderer/components/shared';
interface FlexProps extends HTMLAttributes<HTMLDivElement>, StyleProps {
align?: CSSProperties['alignItems'];
children?: ReactNode;
columnGap?: CSSProperties['columnGap'];
direction?: CSSProperties['flexDirection'];
gap?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | CSSProperties['gap'];
justify?: CSSProperties['justifyContent'];
rowGap?: CSSProperties['rowGap'];
wrap?: CSSProperties['flexWrap'];
}
export const Flex = forwardRef<HTMLDivElement, FlexProps>(
(
{ align, columnGap, direction, gap, justify, rowGap, wrap, children, ...props }: FlexProps,
ref,
) => {
const {
bg,
bottom,
display,
fw,
h,
left,
lts,
m,
mah,
maw,
mb,
mih,
miw,
ml,
mr,
mt,
mx,
my,
p,
pb,
pl,
pos,
pr,
pt,
px,
py,
right,
ta,
td,
top,
tt,
w,
...htmlProps
} = props;
const styleProps = {
$bg: bg,
$bottom: bottom,
$display: display,
$fw: fw,
$h: h,
$left: left,
$lts: lts,
$m: m,
$mah: mah,
$maw: maw,
$mb: mb,
$mih: mih,
$miw: miw,
$ml: ml,
$mr: mr,
$mt: mt,
$mx: mx,
$my: my,
$p: p,
$pb: pb,
$pl: pl,
$pos: pos,
$pr: pr,
$pt: pt,
$px: px,
$py: py,
$right: right,
$ta: ta,
$td: td,
$top: top,
$tt: tt,
$w: w,
};
return (
<StyledFlex
ref={ref}
align={align}
columnGap={columnGap}
direction={direction}
gap={gap}
justify={justify}
rowGap={rowGap}
wrap={wrap}
{...styleProps}
{...htmlProps}
>
{children}
</StyledFlex>
);
},
);
const StyledFlex = styled(MantineFlex)<StyledComponentsStyleProps>`
${(props) => props.$bg && `background-color: ${props.$bg};`}
${(props) => props.$bottom && `bottom: ${props.$bottom};`}
${(props) => props.$display && `display: ${props.$display};`}
${(props) => props.$fw && `font-weight: ${props.$fw};`}
${(props) => props.$h && `height: ${props.$h};`}
${(props) => props.$left && `left: ${props.$left};`}
${(props) => props.$lts && `letter-spacing: ${props.$lts};`}
${(props) => props.$m && `margin: ${props.$m};`}
${(props) => props.$mah && `max-height: ${props.$mah};`}
${(props) => props.$maw && `max-width: ${props.$maw};`}
${(props) => props.$mb && `margin-bottom: ${props.$mb};`}
${(props) => props.$mih && `min-height: ${props.$mih};`}
${(props) => props.$miw && `min-width: ${props.$miw};`}
${(props) => props.$ml && `margin-left: ${props.$ml};`}
${(props) => props.$mr && `margin-right: ${props.$mr};`}
${(props) => props.$mt && `margin-top: ${props.$mt};`}
${(props) => props.$mx && `margin-left: ${props.$mx};`}
${(props) => props.$mx && `margin-right: ${props.$mx};`}
${(props) => props.$my && `margin-top: ${props.$my};`}
${(props) => props.$my && `margin-bottom: ${props.$my};`}
${(props) => props.$p && `padding: ${props.$p};`}
${(props) => props.$pb && `padding-bottom: ${props.$pb};`}
${(props) => props.$pl && `padding-left: ${props.$pl};`}
${(props) => props.$pos && `position: ${props.$pos};`}
${(props) => props.$pr && `padding-right: ${props.$pr};`}
${(props) => props.$pt && `padding-top: ${props.$pt};`}
${(props) => props.$px && `padding-left: ${props.$px};`}
${(props) => props.$px && `padding-right: ${props.$px};`}
${(props) => props.$py && `padding-top: ${props.$py};`}
${(props) => props.$py && `padding-bottom: ${props.$py};`}
${(props) => props.$right && `right: ${props.$right};`}
${(props) => props.$ta && `text-align: ${props.$ta};`}
${(props) => props.$td && `text-decoration: ${props.$td};`}
${(props) => props.$top && `top: ${props.$top};`}
${(props) => props.$tt && `text-transform: ${props.$tt};`}
${(props) => props.$w && `width: ${props.$w};`}
`;

View File

@ -9,7 +9,6 @@ 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';
@ -25,6 +24,8 @@ import { usePlayQueueAdd } from '/@/renderer/features/player';
import { useCreateFavorite, useDeleteFavorite } from '/@/renderer/features/shared';
import { usePlayButtonBehavior } from '/@/renderer/store';
import { CardRoute, CardRow } from '/@/renderer/types';
import { Group } from '/@/renderer/components/group';
import { Stack } from '/@/renderer/components/stack';
const getSlidesPerView = (windowWidth: number) => {
if (windowWidth < 400) return 2;
@ -53,7 +54,7 @@ interface TitleProps {
const Title = ({ label, handleNext, handlePrev, pagination }: TitleProps) => {
return (
<Group position="apart">
<Group justify="space-between">
{isValidElement(label) ? (
label
) : (
@ -65,20 +66,18 @@ const Title = ({ label, handleNext, handlePrev, pagination }: TitleProps) => {
</TextTitle>
)}
<Group spacing="sm">
<Group gap="sm">
<Button
compact
disabled={!pagination.hasPreviousPage}
size="lg"
size="compact-lg"
variant="default"
onClick={handlePrev}
>
<RiArrowLeftSLine />
</Button>
<Button
compact
disabled={!pagination.hasNextPage}
size="lg"
size="compact-lg"
variant="default"
onClick={handleNext}
>
@ -280,8 +279,7 @@ export const SwiperGridCarousel = ({
return (
<CarouselContainer
ref={containerRef}
className="grid-carousel"
spacing="md"
gap="md"
>
{title ? (
<Title

View File

@ -0,0 +1,147 @@
import { forwardRef, HTMLAttributes, ReactNode } from 'react';
import { Group as MantineGroup } from '@mantine/core';
import styled, { CSSProperties } from 'styled-components';
import { StyledComponentsStyleProps, StyleProps } from '/@/renderer/components/shared';
interface GroupProps extends HTMLAttributes<HTMLDivElement>, StyleProps {
align?: CSSProperties['alignItems'];
children?: ReactNode;
gap?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | CSSProperties['gap'];
grow?: boolean;
justify?: CSSProperties['justifyContent'];
preventGrowOverflow?: boolean;
wrap?: CSSProperties['flexWrap'];
}
export const Group = forwardRef<HTMLDivElement, GroupProps>(
(
{ align, gap, grow, justify, preventGrowOverflow, wrap, children, ...props }: GroupProps,
ref,
) => {
const {
bg,
bottom,
display,
fw,
h,
left,
lts,
m,
mah,
maw,
mb,
mih,
miw,
ml,
mr,
mt,
mx,
my,
p,
pb,
pl,
pos,
pr,
pt,
px,
py,
right,
ta,
td,
top,
tt,
w,
...htmlProps
} = props;
const styleProps = {
$bg: bg,
$bottom: bottom,
$display: display,
$fw: fw,
$h: h,
$left: left,
$lts: lts,
$m: m,
$mah: mah,
$maw: maw,
$mb: mb,
$mih: mih,
$miw: miw,
$ml: ml,
$mr: mr,
$mt: mt,
$mx: mx,
$my: my,
$p: p,
$pb: pb,
$pl: pl,
$pos: pos,
$pr: pr,
$pt: pt,
$px: px,
$py: py,
$right: right,
$ta: ta,
$td: td,
$top: top,
$tt: tt,
$w: w,
};
return (
<StyledGroup
ref={ref}
align={align}
gap={gap}
grow={grow}
justify={justify}
preventGrowOverflow={preventGrowOverflow}
wrap={wrap}
{...styleProps}
{...htmlProps}
>
{children}
</StyledGroup>
);
},
);
const StyledGroup = styled(MantineGroup)<StyledComponentsStyleProps>`
${(props) => props.$bg && `background-color: ${props.$bg};`}
${(props) => props.$bottom && `bottom: ${props.$bottom};`}
${(props) => props.$display && `display: ${props.$display};`}
${(props) => props.$fw && `font-weight: ${props.$fw};`}
${(props) => props.$h && `height: ${props.$h};`}
${(props) => props.$left && `left: ${props.$left};`}
${(props) => props.$lts && `letter-spacing: ${props.$lts};`}
${(props) => props.$m && `margin: ${props.$m};`}
${(props) => props.$mah && `max-height: ${props.$mah};`}
${(props) => props.$maw && `max-width: ${props.$maw};`}
${(props) => props.$mb && `margin-bottom: ${props.$mb};`}
${(props) => props.$mih && `min-height: ${props.$mih};`}
${(props) => props.$miw && `min-width: ${props.$miw};`}
${(props) => props.$ml && `margin-left: ${props.$ml};`}
${(props) => props.$mr && `margin-right: ${props.$mr};`}
${(props) => props.$mt && `margin-top: ${props.$mt};`}
${(props) => props.$mx && `margin-left: ${props.$mx};`}
${(props) => props.$mx && `margin-right: ${props.$mx};`}
${(props) => props.$my && `margin-top: ${props.$my};`}
${(props) => props.$my && `margin-bottom: ${props.$my};`}
${(props) => props.$p && `padding: ${props.$p};`}
${(props) => props.$pb && `padding-bottom: ${props.$pb};`}
${(props) => props.$pl && `padding-left: ${props.$pl};`}
${(props) => props.$pos && `position: ${props.$pos};`}
${(props) => props.$pr && `padding-right: ${props.$pr};`}
${(props) => props.$pt && `padding-top: ${props.$pt};`}
${(props) => props.$px && `padding-left: ${props.$px};`}
${(props) => props.$px && `padding-right: ${props.$px};`}
${(props) => props.$py && `padding-top: ${props.$py};`}
${(props) => props.$py && `padding-bottom: ${props.$py};`}
${(props) => props.$right && `right: ${props.$right};`}
${(props) => props.$ta && `text-align: ${props.$ta};`}
${(props) => props.$td && `text-decoration: ${props.$td};`}
${(props) => props.$top && `top: ${props.$top};`}
${(props) => props.$tt && `text-transform: ${props.$tt};`}
${(props) => props.$w && `width: ${props.$w};`}
`;

View File

@ -34,3 +34,11 @@ export * from './text';
export * from './text-title';
export * from './toast';
export * from './tooltip';
export * from './group';
export * from './stack';
export * from './flex';
export * from './center';
export * from './divider';
export * from './portal';
export * from './box';
export * from './action-icon';

View File

@ -282,7 +282,7 @@ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
ref={ref}
spellCheck={false}
{...props}
sx={{ maxWidth, width }}
style={{ maxWidth, width }}
>
{children}
</StyledTextInput>
@ -298,7 +298,7 @@ export const NumberInput = forwardRef<HTMLInputElement, NumberInputProps>(
hideControls
spellCheck={false}
{...props}
sx={{ maxWidth, width }}
style={{ maxWidth, width }}
>
{children}
</StyledNumberInput>
@ -312,7 +312,7 @@ export const PasswordInput = forwardRef<HTMLInputElement, PasswordInputProps>(
<StyledPasswordInput
ref={ref}
{...props}
sx={{ maxWidth, width }}
style={{ maxWidth, width }}
>
{children}
</StyledPasswordInput>
@ -326,12 +326,7 @@ export const FileInput = forwardRef<HTMLButtonElement, FileInputProps>(
<StyledFileInput
ref={ref}
{...props}
styles={{
placeholder: {
color: 'var(--input-placeholder-fg)',
},
}}
sx={{ maxWidth, width }}
style={{ maxWidth, width }}
>
{children}
</StyledFileInput>
@ -345,7 +340,7 @@ export const JsonInput = forwardRef<HTMLTextAreaElement, JsonInputProps>(
<StyledJsonInput
ref={ref}
{...props}
sx={{ maxWidth, width }}
style={{ maxWidth, width }}
>
{children}
</StyledJsonInput>
@ -359,7 +354,7 @@ export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
<StyledTextarea
ref={ref}
{...props}
sx={{ maxWidth, width }}
style={{ maxWidth, width }}
>
{children}
</StyledTextarea>

View File

@ -1,13 +1,10 @@
import React, { ReactNode } from 'react';
import {
ModalProps as MantineModalProps,
Stack,
Modal as MantineModal,
Flex,
Group,
} from '@mantine/core';
import { ModalProps as MantineModalProps, Modal as MantineModal } from '@mantine/core';
import { closeAllModals, ContextModalProps } from '@mantine/modals';
import { Button } from '/@/renderer/components/button';
import { Flex } from '/@/renderer/components/flex';
import { Group } from '/@/renderer/components/group';
import { Stack } from '/@/renderer/components/stack';
export interface ModalProps extends Omit<MantineModalProps, 'onClose'> {
children?: ReactNode;
@ -77,7 +74,7 @@ export const ConfirmModal = ({
return (
<Stack>
<Flex>{children}</Flex>
<Group position="right">
<Group align="flex-end">
<Button
data-focus
variant="default"

View File

@ -1,7 +1,7 @@
import { Flex, Group, Stack } from '@mantine/core';
import { motion } from 'framer-motion';
export const MotionFlex = motion(Flex);
export const MotionFlex = motion(Flex as any);
export const MotionGroup = motion(Group);

View File

@ -1,4 +1,4 @@
import { Flex, FlexProps } from '@mantine/core';
import { FlexProps } from '@mantine/core';
import { AnimatePresence, motion, Variants } from 'framer-motion';
import { ReactNode, useRef } from 'react';
import styled from 'styled-components';
@ -6,10 +6,11 @@ import { useShouldPadTitlebar, useTheme } from '/@/renderer/hooks';
import { useWindowSettings } from '/@/renderer/store/settings.store';
import { Platform } from '/@/renderer/types';
const Container = styled(motion(Flex))<{
const Container = styled(motion.div)<{
$height?: string;
$position?: string;
}>`
display: flex;
position: ${(props) => props.$position || 'relative'};
z-index: 200;
width: 100%;
@ -98,7 +99,6 @@ export const PageHeader = ({
backgroundColor,
isHidden,
children,
...props
}: PageHeaderProps) => {
const ref = useRef(null);
const padRight = useShouldPadTitlebar();
@ -111,7 +111,6 @@ export const PageHeader = ({
ref={ref}
$height={height}
$position={position}
{...props}
>
<Header
$isDraggable={windowBarStyle === Platform.WEB}

View File

@ -0,0 +1,10 @@
import { HTMLAttributes, ReactNode } from 'react';
import { Portal as MantinePortal } from '@mantine/core';
interface PortalProps extends HTMLAttributes<HTMLDivElement> {
children: ReactNode;
}
export const Portal = ({ children, ...props }: PortalProps) => {
return <MantinePortal {...props}>{children}</MantinePortal>;
};

View File

@ -1,4 +1,3 @@
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';
@ -7,6 +6,8 @@ import { DropdownMenu } from '/@/renderer/components/dropdown-menu';
import { QueryBuilderOption } from '/@/renderer/components/query-builder/query-builder-option';
import { QueryBuilderGroup, QueryBuilderRule } from '/@/renderer/types';
import i18n from '/@/i18n/i18n';
import { Group } from '/@/renderer/components/group';
import { Stack } from '/@/renderer/components/stack';
const FILTER_GROUP_OPTIONS_DATA = [
{
@ -98,10 +99,10 @@ export const QueryBuilder = ({
return (
<Stack
ml={`${level * 10}px`}
spacing="sm"
gap="sm"
style={{ marginLeft: `${level * 10}px` }}
>
<Group spacing="sm">
<Group gap="sm">
<Select
data={FILTER_GROUP_OPTIONS_DATA}
maxWidth={175}
@ -131,7 +132,7 @@ export const QueryBuilder = ({
</DropdownMenu.Target>
<DropdownMenu.Dropdown>
<DropdownMenu.Item
icon={<RiAddFill />}
leftSection={<RiAddFill />}
onClick={handleAddRuleGroup}
>
Add rule group
@ -139,7 +140,7 @@ export const QueryBuilder = ({
{level > 0 && (
<DropdownMenu.Item
icon={<RiDeleteBinFill />}
leftSection={<RiDeleteBinFill />}
onClick={handleDeleteRuleGroup}
>
Remove rule group
@ -150,14 +151,14 @@ export const QueryBuilder = ({
<DropdownMenu.Divider />
<DropdownMenu.Item
$danger
icon={<RiRestartLine color="var(--danger-color)" />}
leftSection={<RiRestartLine color="var(--danger-color)" />}
onClick={onResetFilters}
>
Reset to default
</DropdownMenu.Item>
<DropdownMenu.Item
$danger
icon={<RiDeleteBinFill color="var(--danger-color)" />}
leftSection={<RiDeleteBinFill color="var(--danger-color)" />}
onClick={onClearFilters}
>
Clear filters

View File

@ -1,10 +1,10 @@
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';
import { QueryBuilderRule } from '/@/renderer/types';
import { Group } from '/@/renderer/components/group';
type DeleteArgs = {
groupIndex: number[];
@ -69,7 +69,7 @@ const QueryValueInput = ({ onChange, type, data, ...props }: any) => {
maxWidth={81}
width="10%"
onChange={(e) => {
const newRange = [e || 0, numberRange[1]];
const newRange = [Number(e) || 0, numberRange[1]];
setNumberRange(newRange);
onChange(newRange);
}}
@ -80,7 +80,7 @@ const QueryValueInput = ({ onChange, type, data, ...props }: any) => {
maxWidth={81}
width="10%"
onChange={(e) => {
const newRange = [numberRange[0], e || 0];
const newRange = [numberRange[0], Number(e) || 0];
setNumberRange(newRange);
onChange(newRange);
}}
@ -188,8 +188,8 @@ export const QueryBuilderOption = ({
return (
<Group
ml={ml}
spacing="sm"
gap="sm"
style={{ marginLeft: `${ml}px` }}
>
<Select
searchable

View File

@ -1,10 +1,11 @@
import { ChangeEvent, KeyboardEvent } from 'react';
import { ActionIcon, TextInputProps } from '@mantine/core';
import { TextInputProps } from '@mantine/core';
import { useFocusWithin, useHotkeys, useMergedRef } from '@mantine/hooks';
import { RiCloseFill, RiSearchLine } from 'react-icons/ri';
import { TextInput } from '/@/renderer/components/input';
import { useSettingsStore } from '/@/renderer/store';
import { shallow } from 'zustand/shallow';
import { ActionIcon } from '/@/renderer/components/action-icon';
interface SearchInputProps extends TextInputProps {
initialWidth?: number;
@ -39,7 +40,7 @@ export const SearchInput = ({
<TextInput
ref={mergedRef}
{...props}
icon={showIcon && <RiSearchLine />}
leftSection={showIcon && <RiSearchLine />}
rightSection={
isOpened ? (
<ActionIcon
@ -55,13 +56,13 @@ export const SearchInput = ({
}
size="md"
styles={{
icon: { svg: { fill: 'var(--titlebar-fg)' } },
input: {
backgroundColor: isOpened ? 'inherit' : 'transparent !important',
border: 'none !important',
cursor: isOpened ? 'text' : 'pointer',
padding: isOpened ? '10px' : 0,
},
section: { svg: { fill: 'var(--titlebar-fg)' }, userSelect: 'none' },
}}
width={isOpened ? openedWidth || 150 : initialWidth || 35}
onChange={onChange}

View File

@ -40,7 +40,11 @@ const StyledSelect = styled(MantineSelect)`
export const Select = ({ width, maxWidth, ...props }: SelectProps) => {
return (
<StyledSelect
withinPortal
comboboxProps={{
transitionProps: { duration: 100, transition: 'fade' },
withinPortal: true,
}}
style={{ maxWidth, width }}
styles={{
dropdown: {
background: 'var(--dropdown-menu-bg)',
@ -50,14 +54,14 @@ export const Select = ({ width, maxWidth, ...props }: SelectProps) => {
background: 'var(--input-bg)',
color: 'var(--input-fg)',
},
item: {
option: {
'&:hover': {
background: 'var(--dropdown-menu-bg-hover)',
},
'&[data-hovered]': {
'&[dataHovered]': {
background: 'var(--dropdown-menu-bg-hover)',
},
'&[data-selected="true"]': {
'&[dataSelected="true"]': {
'&:hover': {
background: 'var(--dropdown-menu-bg-hover)',
},
@ -68,8 +72,6 @@ export const Select = ({ width, maxWidth, ...props }: SelectProps) => {
padding: '.3rem',
},
}}
sx={{ maxWidth, width }}
transitionProps={{ duration: 100, transition: 'fade' }}
{...props}
/>
);
@ -95,7 +97,11 @@ const StyledMultiSelect = styled(MantineMultiSelect)`
export const MultiSelect = ({ width, maxWidth, ...props }: MultiSelectProps) => {
return (
<StyledMultiSelect
withinPortal
comboboxProps={{
transitionProps: { duration: 100, transition: 'fade' },
withinPortal: true,
}}
style={{ maxWidth, width }}
styles={{
dropdown: {
background: 'var(--dropdown-menu-bg)',
@ -105,14 +111,14 @@ export const MultiSelect = ({ width, maxWidth, ...props }: MultiSelectProps) =>
background: 'var(--input-bg)',
color: 'var(--input-fg)',
},
item: {
option: {
'&:hover': {
background: 'var(--dropdown-menu-bg-hover)',
},
'&[data-hovered]': {
'&[dataHovered]': {
background: 'var(--dropdown-menu-bg-hover)',
},
'&[data-selected="true"]': {
'&[dataSelected="true"]': {
'&:hover': {
background: 'var(--dropdown-menu-bg-hover)',
},
@ -122,15 +128,13 @@ export const MultiSelect = ({ width, maxWidth, ...props }: MultiSelectProps) =>
color: 'var(--dropdown-menu-fg)',
padding: '.5rem .1rem',
},
value: {
pill: {
margin: '.2rem',
paddingBottom: '1rem',
paddingLeft: '1rem',
paddingTop: '1rem',
},
}}
sx={{ maxWidth, width }}
transitionProps={{ duration: 100, transition: 'fade' }}
{...props}
/>
);

View File

@ -0,0 +1,114 @@
import { CSSProperties } from 'react';
import styled from 'styled-components';
export type StyleProps = {
bg?: CSSProperties['backgroundColor'];
bottom?: CSSProperties['bottom'];
display?: CSSProperties['display'];
fw?: CSSProperties['fontWeight'];
h?: CSSProperties['height'];
left?: CSSProperties['left'];
lh?: CSSProperties['lineHeight'];
lts?: CSSProperties['letterSpacing'];
m?: CSSProperties['margin'];
mah?: CSSProperties['maxHeight'];
maw?: CSSProperties['maxWidth'];
mb?: CSSProperties['marginBottom'];
mih?: CSSProperties['minHeight'];
miw?: CSSProperties['minWidth'];
ml?: CSSProperties['marginLeft'];
mr?: CSSProperties['marginRight'];
mt?: CSSProperties['marginTop'];
mx?: CSSProperties['marginLeft'] | CSSProperties['marginRight'];
my?: CSSProperties['marginTop'] | CSSProperties['marginBottom'];
p?: CSSProperties['padding'];
pb?: CSSProperties['paddingBottom'];
pl?: CSSProperties['paddingLeft'];
pos?: CSSProperties['position'];
pr?: CSSProperties['paddingRight'];
pt?: CSSProperties['paddingTop'];
px?: CSSProperties['paddingLeft'] | CSSProperties['paddingRight'];
py?: CSSProperties['paddingTop'] | CSSProperties['paddingBottom'];
right?: CSSProperties['right'];
ta?: CSSProperties['textAlign'];
td?: CSSProperties['textDecoration'];
top?: CSSProperties['top'];
tt?: CSSProperties['textTransform'];
w?: CSSProperties['width'];
};
export type StyledComponentsStyleProps = {
$bg?: CSSProperties['backgroundColor'];
$bottom?: CSSProperties['bottom'];
$display?: CSSProperties['display'];
$fw?: CSSProperties['fontWeight'];
$h?: CSSProperties['height'];
$left?: CSSProperties['left'];
$lh?: CSSProperties['lineHeight'];
$lts?: CSSProperties['letterSpacing'];
$m?: CSSProperties['margin'];
$mah?: CSSProperties['maxHeight'];
$maw?: CSSProperties['maxWidth'];
$mb?: CSSProperties['marginBottom'];
$mih?: CSSProperties['minHeight'];
$miw?: CSSProperties['minWidth'];
$ml?: CSSProperties['marginLeft'];
$mr?: CSSProperties['marginRight'];
$mt?: CSSProperties['marginTop'];
$mx?: CSSProperties['marginLeft'] | CSSProperties['marginRight'];
$my?: CSSProperties['marginTop'] | CSSProperties['marginBottom'];
$p?: CSSProperties['padding'];
$pb?: CSSProperties['paddingBottom'];
$pl?: CSSProperties['paddingLeft'];
$pos?: CSSProperties['position'];
$pr?: CSSProperties['paddingRight'];
$pt?: CSSProperties['paddingTop'];
$px?: CSSProperties['paddingLeft'] | CSSProperties['paddingRight'];
$py?: CSSProperties['paddingTop'] | CSSProperties['paddingBottom'];
$right?: CSSProperties['right'];
$ta?: CSSProperties['textAlign'];
$td?: CSSProperties['textDecoration'];
$top?: CSSProperties['top'];
$tt?: CSSProperties['textTransform'];
$w?: CSSProperties['width'];
};
export const DivWithStyleProps = styled.div<StyledComponentsStyleProps>`
${(props) => props.$bg && `background-color: ${props.$bg};`}
${(props) => props.$bottom && `bottom: ${props.$bottom};`}
${(props) => props.$display && `display: ${props.$display};`}
${(props) => props.$fw && `font-weight: ${props.$fw};`}
${(props) => props.$h && `height: ${props.$h};`}
${(props) => props.$left && `left: ${props.$left};`}
${(props) => props.$lh && `line-height: ${props.$lh};`}
${(props) => props.$lts && `letter-spacing: ${props.$lts};`}
${(props) => props.$m && `margin: ${props.$m};`}
${(props) => props.$mah && `max-height: ${props.$mah};`}
${(props) => props.$maw && `max-width: ${props.$maw};`}
${(props) => props.$mb && `margin-bottom: ${props.$mb};`}
${(props) => props.$mih && `min-height: ${props.$mih};`}
${(props) => props.$miw && `min-width: ${props.$miw};`}
${(props) => props.$ml && `margin-left: ${props.$ml};`}
${(props) => props.$mr && `margin-right: ${props.$mr};`}
${(props) => props.$mt && `margin-top: ${props.$mt};`}
${(props) => props.$mx && `margin-left: ${props.$mx};`}
${(props) => props.$mx && `margin-right: ${props.$mx};`}
${(props) => props.$my && `margin-top: ${props.$my};`}
${(props) => props.$my && `margin-bottom: ${props.$my};`}
${(props) => props.$p && `padding: ${props.$p};`}
${(props) => props.$pb && `padding-bottom: ${props.$pb};`}
${(props) => props.$pl && `padding-left: ${props.$pl};`}
${(props) => props.$pos && `position: ${props.$pos};`}
${(props) => props.$pr && `padding-right: ${props.$pr};`}
${(props) => props.$pt && `padding-top: ${props.$pt};`}
${(props) => props.$px && `padding-left: ${props.$px};`}
${(props) => props.$px && `padding-right: ${props.$px};`}
${(props) => props.$py && `padding-top: ${props.$py};`}
${(props) => props.$py && `padding-bottom: ${props.$py};`}
${(props) => props.$right && `right: ${props.$right};`}
${(props) => props.$ta && `text-align: ${props.$ta};`}
${(props) => props.$td && `text-decoration: ${props.$td};`}
${(props) => props.$top && `top: ${props.$top};`}
${(props) => props.$tt && `text-transform: ${props.$tt};`}
${(props) => props.$w && `width: ${props.$w};`}
`;

View File

@ -1,8 +1,8 @@
import { Center } from '@mantine/core';
import type { IconType } from 'react-icons';
import { RiLoader5Fill } from 'react-icons/ri';
import styled from 'styled-components';
import { rotating } from '/@/renderer/styles';
import { Center } from '/@/renderer/components/center';
interface SpinnerProps extends IconType {
color?: string;
@ -18,21 +18,23 @@ export const SpinnerIcon = styled(RiLoader5Fill)`
export const Spinner = ({ ...props }: SpinnerProps) => {
if (props.container) {
return (
<Center
h="100%"
w="100%"
>
<SpinnerContainer>
<SpinnerIcon
color={props.color}
size={props.size}
/>
</Center>
</SpinnerContainer>
);
}
return <SpinnerIcon {...props} />;
};
const SpinnerContainer = styled(Center)`
height: 100%;
width: 100%;
`;
Spinner.defaultProps = {
color: undefined,
size: 15,

View File

@ -0,0 +1,138 @@
import { forwardRef, HTMLAttributes, ReactNode, CSSProperties } from 'react';
import { Stack as MantineStack } from '@mantine/core';
import styled from 'styled-components';
import { StyleProps, StyledComponentsStyleProps } from '../shared';
interface StackProps extends HTMLAttributes<HTMLDivElement>, StyleProps {
align?: CSSProperties['alignItems'];
children?: ReactNode;
gap?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | CSSProperties['gap'];
justify?: CSSProperties['justifyContent'];
}
export const Stack = forwardRef<HTMLDivElement, StackProps>(
({ align, gap, justify, children, ...props }: StackProps, ref) => {
const {
bg,
bottom,
display,
fw,
h,
left,
lts,
m,
mah,
maw,
mb,
mih,
miw,
ml,
mr,
mt,
mx,
my,
p,
pb,
pl,
pos,
pr,
pt,
px,
py,
right,
ta,
td,
top,
tt,
w,
...htmlProps
} = props;
const styleProps = {
$bg: bg,
$bottom: bottom,
$display: display,
$fw: fw,
$h: h,
$left: left,
$lts: lts,
$m: m,
$mah: mah,
$maw: maw,
$mb: mb,
$mih: mih,
$miw: miw,
$ml: ml,
$mr: mr,
$mt: mt,
$mx: mx,
$my: my,
$p: p,
$pb: pb,
$pl: pl,
$pos: pos,
$pr: pr,
$pt: pt,
$px: px,
$py: py,
$right: right,
$ta: ta,
$td: td,
$top: top,
$tt: tt,
$w: w,
};
return (
<StyledStack
ref={ref}
align={align}
gap={gap}
justify={justify}
{...styleProps}
{...htmlProps}
>
{children}
</StyledStack>
);
},
);
const StyledStack = styled(MantineStack)<StyledComponentsStyleProps>`
${(props) => props.$bg && `background-color: ${props.$bg};`}
${(props) => props.$bottom && `bottom: ${props.$bottom};`}
${(props) => props.$display && `display: ${props.$display};`}
${(props) => props.$fw && `font-weight: ${props.$fw};`}
${(props) => props.$h && `height: ${props.$h};`}
${(props) => props.$left && `left: ${props.$left};`}
${(props) => props.$lts && `letter-spacing: ${props.$lts};`}
${(props) => props.$m && `margin: ${props.$m};`}
${(props) => props.$mah && `max-height: ${props.$mah};`}
${(props) => props.$maw && `max-width: ${props.$maw};`}
${(props) => props.$mb && `margin-bottom: ${props.$mb};`}
${(props) => props.$mih && `min-height: ${props.$mih};`}
${(props) => props.$miw && `min-width: ${props.$miw};`}
${(props) => props.$ml && `margin-left: ${props.$ml};`}
${(props) => props.$mr && `margin-right: ${props.$mr};`}
${(props) => props.$mt && `margin-top: ${props.$mt};`}
${(props) => props.$mx && `margin-left: ${props.$mx};`}
${(props) => props.$mx && `margin-right: ${props.$mx};`}
${(props) => props.$my && `margin-top: ${props.$my};`}
${(props) => props.$my && `margin-bottom: ${props.$my};`}
${(props) => props.$p && `padding: ${props.$p};`}
${(props) => props.$pb && `padding-bottom: ${props.$pb};`}
${(props) => props.$pl && `padding-left: ${props.$pl};`}
${(props) => props.$pos && `position: ${props.$pos};`}
${(props) => props.$pr && `padding-right: ${props.$pr};`}
${(props) => props.$pt && `padding-top: ${props.$pt};`}
${(props) => props.$px && `padding-left: ${props.$px};`}
${(props) => props.$px && `padding-right: ${props.$px};`}
${(props) => props.$py && `padding-top: ${props.$py};`}
${(props) => props.$py && `padding-bottom: ${props.$py};`}
${(props) => props.$right && `right: ${props.$right};`}
${(props) => props.$ta && `text-align: ${props.$ta};`}
${(props) => props.$td && `text-decoration: ${props.$td};`}
${(props) => props.$top && `top: ${props.$top};`}
${(props) => props.$tt && `text-transform: ${props.$tt};`}
${(props) => props.$w && `width: ${props.$w};`}
`;

View File

@ -1,22 +1,21 @@
import type { ComponentPropsWithoutRef, ReactNode } from 'react';
import type { TitleProps as MantineTitleProps } from '@mantine/core';
import type { ReactNode } from 'react';
import { createPolymorphicComponent, Title as MantineHeader } from '@mantine/core';
import styled from 'styled-components';
import { textEllipsis } from '/@/renderer/styles';
import { StyleProps, StyledComponentsStyleProps } from '/@/renderer/components/shared';
type MantineTextTitleDivProps = MantineTitleProps & ComponentPropsWithoutRef<'div'>;
interface TextTitleProps extends MantineTextTitleDivProps {
export interface TextTitleProps extends StyleProps {
$link?: boolean;
$noSelect?: boolean;
$secondary?: boolean;
children?: ReactNode;
order?: number;
overflow?: 'hidden' | 'visible';
to?: string;
weight?: number;
}
const StyledTextTitle = styled(MantineHeader)<TextTitleProps>`
const StyledTextTitle = styled(MantineHeader)<TextTitleProps & StyledComponentsStyleProps>`
overflow: ${(props) => props.overflow};
color: ${(props) => (props.$secondary ? 'var(--main-fg-secondary)' : 'var(--main-fg)')};
cursor: ${(props) => props.$link && 'cursor'};
@ -28,15 +27,132 @@ const StyledTextTitle = styled(MantineHeader)<TextTitleProps>`
color: ${(props) => props.$link && 'var(--main-fg)'};
text-decoration: ${(props) => (props.$link ? 'underline' : 'none')};
}
${(props) => props.$bg && `background-color: ${props.$bg};`}
${(props) => props.$bottom && `bottom: ${props.$bottom};`}
${(props) => props.$display && `display: ${props.$display};`}
${(props) => props.$fw && `font-weight: ${props.$fw};`}
${(props) => props.$h && `height: ${props.$h};`}
${(props) => props.$left && `left: ${props.$left};`}
${(props) => props.$lts && `letter-spacing: ${props.$lts};`}
${(props) => props.$m && `margin: ${props.$m};`}
${(props) => props.$mah && `max-height: ${props.$mah};`}
${(props) => props.$maw && `max-width: ${props.$maw};`}
${(props) => props.$mb && `margin-bottom: ${props.$mb};`}
${(props) => props.$mih && `min-height: ${props.$mih};`}
${(props) => props.$miw && `min-width: ${props.$miw};`}
${(props) => props.$ml && `margin-left: ${props.$ml};`}
${(props) => props.$mr && `margin-right: ${props.$mr};`}
${(props) => props.$mt && `margin-top: ${props.$mt};`}
${(props) => props.$mx && `margin-left: ${props.$mx};`}
${(props) => props.$mx && `margin-right: ${props.$mx};`}
${(props) => props.$my && `margin-top: ${props.$my};`}
${(props) => props.$my && `margin-bottom: ${props.$my};`}
${(props) => props.$p && `padding: ${props.$p};`}
${(props) => props.$pb && `padding-bottom: ${props.$pb};`}
${(props) => props.$pl && `padding-left: ${props.$pl};`}
${(props) => props.$pos && `position: ${props.$pos};`}
${(props) => props.$pr && `padding-right: ${props.$pr};`}
${(props) => props.$pt && `padding-top: ${props.$pt};`}
${(props) => props.$px && `padding-left: ${props.$px};`}
${(props) => props.$px && `padding-right: ${props.$px};`}
${(props) => props.$py && `padding-top: ${props.$py};`}
${(props) => props.$py && `padding-bottom: ${props.$py};`}
${(props) => props.$right && `right: ${props.$right};`}
${(props) => props.$ta && `text-align: ${props.$ta};`}
${(props) => props.$td && `text-decoration: ${props.$td};`}
${(props) => props.$top && `top: ${props.$top};`}
${(props) => props.$tt && `text-transform: ${props.$tt};`}
${(props) => props.$w && `width: ${props.$w};`}
`;
const _TextTitle = ({ children, $secondary, overflow, $noSelect, ...rest }: TextTitleProps) => {
const _TextTitle = ({
children,
order,
$secondary,
overflow,
$noSelect,
...props
}: TextTitleProps) => {
const {
bg,
bottom,
display,
fw,
h,
left,
lts,
m,
mah,
maw,
mb,
mih,
miw,
ml,
mr,
mt,
mx,
my,
p,
pb,
pl,
pos,
pr,
pt,
px,
py,
right,
ta,
td,
top,
tt,
w,
...htmlProps
} = props;
const styleProps = {
$bg: bg,
$bottom: bottom,
$display: display,
$fw: fw,
$h: h,
$left: left,
$lts: lts,
$m: m,
$mah: mah,
$maw: maw,
$mb: mb,
$mih: mih,
$miw: miw,
$ml: ml,
$mr: mr,
$mt: mt,
$mx: mx,
$my: my,
$p: p,
$pb: pb,
$pl: pl,
$pos: pos,
$pr: pr,
$pt: pt,
$px: px,
$py: py,
$right: right,
$ta: ta,
$td: td,
$top: top,
$tt: tt,
$w: w,
};
return (
<StyledTextTitle
$noSelect={$noSelect}
$secondary={$secondary}
order={order}
overflow={overflow}
{...rest}
{...styleProps}
{...htmlProps}
>
{children}
</StyledTextTitle>

View File

@ -8,6 +8,7 @@ import { textEllipsis } from '/@/renderer/styles';
type MantineTextDivProps = MantineTextProps & ComponentPropsWithoutRef<'div'>;
interface TextProps extends MantineTextDivProps {
$align?: 'left' | 'center' | 'right' | 'justify';
$link?: boolean;
$noSelect?: boolean;
$secondary?: boolean;
@ -24,6 +25,7 @@ const StyledText = styled(MantineText)<TextProps>`
color: ${(props) => (props.$secondary ? 'var(--main-fg-secondary)' : 'var(--main-fg)')};
cursor: ${(props) => props.$link && 'cursor'};
user-select: ${(props) => (props.$noSelect ? 'none' : 'auto')};
text-align: ${(props) => props.$align};
${(props) => props.overflow === 'hidden' && !props.lineClamp && textEllipsis}
&:hover {
@ -32,9 +34,18 @@ const StyledText = styled(MantineText)<TextProps>`
}
`;
export const _Text = ({ children, $secondary, overflow, font, $noSelect, ...rest }: TextProps) => {
export const _Text = ({
children,
$secondary,
overflow,
font,
$noSelect,
$align,
...rest
}: TextProps) => {
return (
<StyledText
$align={$align}
$noSelect={$noSelect}
$secondary={$secondary}
font={font}

View File

@ -1,4 +1,3 @@
import type { NotificationProps as MantineNotificationProps } from '@mantine/notifications';
import {
showNotification,
updateNotification,
@ -7,7 +6,13 @@ import {
cleanNotificationsQueue,
} from '@mantine/notifications';
interface NotificationProps extends MantineNotificationProps {
interface NotificationProps {
autoClose?: boolean | number;
id?: string;
message: string;
onClick?: (e: any) => void;
onClose?: (e: any) => void;
title?: string;
type?: 'success' | 'error' | 'warning' | 'info';
}

View File

@ -0,0 +1,13 @@
export const Center = ({ children }: { children: React.ReactNode }) => {
return (
<div
style={{
alignItems: 'center',
display: 'flex',
justifyContent: 'center',
}}
>
{children}
</div>
);
};

View File

@ -1,4 +1,3 @@
import { Center, Stack } from '@mantine/core';
import { RiAlbumFill, RiUserVoiceFill, RiPlayListFill } from 'react-icons/ri';
import { generatePath, useNavigate } from 'react-router-dom';
import { SimpleImg } from 'react-simple-img';
@ -9,6 +8,8 @@ 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 { Center } from '/@/renderer/components/center';
import { Stack } from '/@/renderer/components/stack';
interface BaseGridCardProps {
columnIndex: number;
@ -186,7 +187,7 @@ export const DefaultCard = ({
/>
) : (
<Center
sx={{
style={{
background: 'var(--placeholder-bg)',
borderRadius: 'var(--card-default-radius)',
height: '100%',
@ -233,7 +234,7 @@ export const DefaultCard = ({
/>
</ImageContainer>
<DetailContainer>
<Stack spacing="sm">
<Stack gap="sm">
{controls.cardRows.map((row, index) => (
<Skeleton
key={`${index}-${columnIndex}-${row.arrayProperty}`}

View File

@ -1,4 +1,3 @@
import { Center, Stack } from '@mantine/core';
import { RiAlbumFill, RiPlayListFill, RiUserVoiceFill } from 'react-icons/ri';
import { generatePath, useNavigate } from 'react-router-dom';
import { SimpleImg } from 'react-simple-img';
@ -9,6 +8,8 @@ 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 { Center } from '/@/renderer/components/center';
import { Stack } from '/@/renderer/components/stack';
interface BaseGridCardProps {
columnIndex: number;
@ -173,7 +174,7 @@ export const PosterCard = ({
/>
) : (
<Center
sx={{
style={{
background: 'var(--placeholder-bg)',
borderRadius: 'var(--card-default-radius)',
height: '100%',
@ -218,7 +219,7 @@ export const PosterCard = ({
<ImageContainer />
</Skeleton>
<DetailContainer>
<Stack spacing="sm">
<Stack gap="sm">
{controls.cardRows.map((row, index) => (
<Skeleton
key={`${index}-${columnIndex}-${row.arrayProperty}`}

View File

@ -7,7 +7,7 @@ export const ActionsCell = ({ context, api }: ICellRendererParams) => {
return (
<CellContainer $position="center">
<Button
compact
size="compact-md"
variant="subtle"
onClick={(e) => {
e.stopPropagation();

View File

@ -1,6 +1,5 @@
import React, { useMemo } from 'react';
import type { ICellRendererParams } from '@ag-grid-community/core';
import { Center } from '@mantine/core';
import { motion } from 'framer-motion';
import { RiAlbumFill } from 'react-icons/ri';
import { generatePath } from 'react-router';
@ -12,6 +11,7 @@ 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 { Center } from '/@/renderer/components/center';
const CellContainer = styled(motion.div)<{ height: number }>`
display: grid;
@ -89,7 +89,7 @@ export const CombinedTitleCell = ({ value, rowIndex, node }: ICellRendererParams
/>
) : (
<Center
sx={{
style={{
background: 'var(--placeholder-bg)',
borderRadius: 'var(--card-default-radius)',
height: `${(node.rowHeight || 40) - 10}px`,
@ -127,7 +127,7 @@ export const CombinedTitleCell = ({ value, rowIndex, node }: ICellRendererParams
component={Link}
overflow="hidden"
size="md"
sx={{ width: 'fit-content' }}
style={{ width: 'fit-content' }}
to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
albumArtistId: artist.id,
})}
@ -139,7 +139,7 @@ export const CombinedTitleCell = ({ value, rowIndex, node }: ICellRendererParams
$secondary
overflow="hidden"
size="md"
sx={{ width: 'fit-content' }}
style={{ width: 'fit-content' }}
>
{artist.name}
</Text>

View File

@ -48,8 +48,8 @@ export const FavoriteCell = ({ value, data, node }: ICellRendererParams) => {
return (
<CellContainer $position="center">
<Button
compact
sx={{
size="compact-md"
style={{
svg: {
fill: !value
? 'var(--main-fg-secondary) !important'

View File

@ -1,18 +1,11 @@
import { useState } from 'react';
import { ICellRendererParams } from '@ag-grid-community/core';
import { Group } from '@mantine/core';
import { RiCheckboxBlankLine, RiCheckboxLine } from 'react-icons/ri';
import styled from 'styled-components';
import { Button } from '/@/renderer/components/button';
import { Paper } from '/@/renderer/components/paper';
import { getNodesByDiscNumber, setNodeSelection } from '../utils';
const Container = styled(Paper)`
display: flex;
height: 100%;
padding: 0.5rem 1rem;
border: 1px solid transparent;
`;
import { Group } from '/@/renderer/components/group';
export const FullWidthDiscCell = ({ node, data, api }: ICellRendererParams) => {
const [isSelected, setIsSelected] = useState(false);
@ -28,20 +21,27 @@ export const FullWidthDiscCell = ({ node, data, api }: ICellRendererParams) => {
return (
<Container>
<Group
position="apart"
w="100%"
>
<ButtonContainer>
<Button
compact
leftIcon={isSelected ? <RiCheckboxLine /> : <RiCheckboxBlankLine />}
size="md"
leftSection={isSelected ? <RiCheckboxLine /> : <RiCheckboxBlankLine />}
size="compact-md"
variant="subtle"
onClick={handleToggleDiscNodes}
>
{data.name}
</Button>
</Group>
</ButtonContainer>
</Container>
);
};
const Container = styled(Paper)`
display: flex;
height: 100%;
padding: 0.5rem 1rem;
border: 1px solid transparent;
`;
const ButtonContainer = styled(Group)`
width: 100%;
`;

View File

@ -149,7 +149,7 @@ export const RowIndexCell = ({ value, eGridCell }: ICellRendererParams) => {
) : null)}
<Text
$secondary
align="right"
$align="right"
className="current-song-child current-song-index"
overflow="hidden"
size="md"

View File

@ -288,7 +288,7 @@ export const TableConfigDropdown = ({ type }: TableConfigDropdownProps) => {
const { setSettings } = useSettingsStoreActions();
const tableConfig = useSettingsStore((state) => state.tables);
const handleAddOrRemoveColumns = (values: TableColumn[]) => {
const handleAddOrRemoveColumns = (values: TableColumn[] | any[]) => {
const existingColumns = tableConfig[type].columns;
if (values.length === 0) {
@ -409,9 +409,11 @@ export const TableConfigDropdown = ({ type }: TableConfigDropdownProps) => {
<Option.Control>
<MultiSelect
clearable
comboboxProps={{
position: 'bottom',
}}
data={SONG_TABLE_COLUMNS}
defaultValue={tableConfig[type]?.columns.map((column) => column.column)}
dropdownPosition="bottom"
width={300}
onChange={handleAddOrRemoveColumns}
/>

View File

@ -1,6 +1,5 @@
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 { useTranslation } from 'react-i18next';
@ -14,6 +13,7 @@ import { Text } from '/@/renderer/components/text';
import { useContainerQuery } from '/@/renderer/hooks';
import { TablePagination as TablePaginationType } from '/@/renderer/types';
import { ListKey } from '/@/renderer/store';
import { Group } from '/@/renderer/components/group';
interface TablePaginationProps {
pageKey: ListKey;
@ -72,13 +72,15 @@ export const TablePagination = ({
<MotionFlex
ref={containerQuery.ref}
layout
align="center"
animate={{ y: 0 }}
exit={{ y: 50 }}
initial={{ y: 50 }}
justify="space-between"
p="1rem"
sx={{ borderTop: '1px solid var(--generic-border-color)' }}
style={{
alignContent: 'center',
borderTop: '1px solid var(--generic-border-color)',
justifyContent: 'space-between',
padding: '1rem',
}}
>
<Text
$secondary
@ -103,8 +105,8 @@ export const TablePagination = ({
</Text>
<Group
ref={containerQuery.ref}
noWrap
spacing="sm"
gap="sm"
wrap="nowrap"
>
<Popover
trapFocus
@ -116,7 +118,7 @@ export const TablePagination = ({
<Button
radius="sm"
size="sm"
sx={{ height: '26px', padding: '0', width: '26px' }}
style={{ height: '26px', padding: '0', width: '26px' }}
tooltip={{
label: t('action.goToPage', { postProcess: 'sentenceCase' }),
}}
@ -147,7 +149,6 @@ export const TablePagination = ({
</Popover.Dropdown>
</Popover>
<Pagination
noWrap
$hideDividers={!containerQuery.isSm}
boundaries={1}
radius="sm"

View File

@ -1,6 +1,5 @@
import { Stack, Group } from '@mantine/core';
import { RiAlertFill } from 'react-icons/ri';
import { Text } from '/@/renderer/components';
import { Group, Stack, Text } from '/@/renderer/components';
import { ReactNode } from 'react';
interface ActionRequiredContainerProps {
@ -9,7 +8,7 @@ interface ActionRequiredContainerProps {
}
export const ActionRequiredContainer = ({ title, children }: ActionRequiredContainerProps) => (
<Stack sx={{ cursor: 'default', maxWidth: '700px' }}>
<Stack style={{ cursor: 'default', maxWidth: '700px' }}>
<Group>
<RiAlertFill
color="var(--warning-color)"
@ -17,7 +16,7 @@ export const ActionRequiredContainer = ({ title, children }: ActionRequiredConta
/>
<Text
size="xl"
sx={{ textTransform: 'uppercase' }}
style={{ textTransform: 'uppercase' }}
>
{title}
</Text>

View File

@ -1,22 +1,17 @@
import { Box, Center, Group, Stack } from '@mantine/core';
import type { FallbackProps } from 'react-error-boundary';
import { RiErrorWarningLine } from 'react-icons/ri';
import { useRouteError } from 'react-router';
import styled from 'styled-components';
import { Button, Text } from '/@/renderer/components';
const Container = styled(Box)`
background: var(--main-bg);
`;
import { Button, Center, Group, Stack, Text } from '/@/renderer/components';
export const ErrorFallback = ({ resetErrorBoundary }: FallbackProps) => {
const error = useRouteError() as any;
return (
<Container>
<Center sx={{ height: '100vh' }}>
<Stack sx={{ maxWidth: '50%' }}>
<Group spacing="xs">
<Center style={{ height: '100vh' }}>
<Stack style={{ maxWidth: '50%' }}>
<Group gap="xs">
<RiErrorWarningLine
color="var(--danger-color)"
size={30}
@ -35,3 +30,7 @@ export const ErrorFallback = ({ resetErrorBoundary }: FallbackProps) => {
</Container>
);
};
const Container = styled.div`
background: var(--main-bg);
`;

View File

@ -14,7 +14,8 @@ export const MpvRequired = () => {
const [disabled, setDisabled] = useState(false);
const { t } = useTranslation();
const handleSetMpvPath = (e: File) => {
const handleSetMpvPath = (e: File | null) => {
if (!e) return;
localSettings?.set('mpv_path', e.path);
};

View File

@ -1,9 +1,9 @@
import { Center, Stack, Group, Divider, Box } from '@mantine/core';
import { RiArrowLeftSLine, RiErrorWarningLine, RiHome4Line, RiMenuFill } from 'react-icons/ri';
import { useNavigate, useRouteError } from 'react-router';
import { Button, DropdownMenu, Text } from '/@/renderer/components';
import { Button, Center, Divider, DropdownMenu, Group, Stack, Text } from '/@/renderer/components';
import { AppMenu } from '/@/renderer/features/titlebar/components/app-menu';
import { AppRoute } from '/@/renderer/router/routes';
import styled from 'styled-components';
const RouteErrorBoundary = () => {
const navigate = useNavigate();
@ -23,9 +23,9 @@ const RouteErrorBoundary = () => {
};
return (
<Box bg="var(--main-bg)">
<Center sx={{ height: '100vh' }}>
<Stack sx={{ maxWidth: '50%' }}>
<Container>
<Center style={{ height: '100vh' }}>
<Stack style={{ maxWidth: '50%' }}>
<Group>
<Button
px={10}
@ -40,16 +40,16 @@ const RouteErrorBoundary = () => {
/>
<Text size="lg">Something went wrong</Text>
</Group>
<Divider my={5} />
<Divider marginY={5} />
<Text size="sm">{error?.message}</Text>
<Group
grow
spacing="sm"
gap="sm"
>
<Button
leftIcon={<RiHome4Line />}
leftSection={<RiHome4Line />}
size="md"
sx={{ flex: 0.5 }}
style={{ flex: 0.5 }}
variant="default"
onClick={handleHome}
>
@ -58,9 +58,9 @@ const RouteErrorBoundary = () => {
<DropdownMenu position="bottom-start">
<DropdownMenu.Target>
<Button
leftIcon={<RiMenuFill />}
leftSection={<RiMenuFill />}
size="md"
sx={{ flex: 0.5 }}
style={{ flex: 0.5 }}
variant="default"
>
Menu
@ -82,8 +82,12 @@ const RouteErrorBoundary = () => {
</Group>
</Stack>
</Center>
</Box>
</Container>
);
};
export default RouteErrorBoundary;
const Container = styled.div`
background: var(--main-bg);
`;

View File

@ -9,7 +9,7 @@ export const ServerRequired = () => {
<DropdownMenu>
<DropdownMenu.Target>
<Button
leftIcon={<RiMenuFill />}
leftSection={<RiMenuFill />}
variant="filled"
>
Open menu

View File

@ -1,8 +1,7 @@
import { Center, Group, Stack } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { RiCheckFill, RiEdit2Line, RiHome4Line } from 'react-icons/ri';
import { Link } from 'react-router-dom';
import { Button, PageHeader, Text } from '/@/renderer/components';
import { Button, Center, Group, PageHeader, Stack, Text } from '/@/renderer/components';
import { ActionRequiredContainer } from '/@/renderer/features/action-required/components/action-required-container';
import { ServerCredentialRequired } from '/@/renderer/features/action-required/components/server-credential-required';
import { ServerRequired } from '/@/renderer/features/action-required/components/server-required';
@ -44,24 +43,24 @@ const ActionRequiredRoute = () => {
return (
<AnimatedPage>
<PageHeader />
<Center sx={{ height: '100%', width: '100vw' }}>
<Center style={{ height: '100%', width: '100vw' }}>
<Stack
spacing="xl"
sx={{ maxWidth: '50%' }}
gap="xl"
style={{ maxWidth: '50%' }}
>
<Group noWrap>
<Group wrap="nowrap">
{displayedCheck && (
<ActionRequiredContainer title={displayedCheck.title}>
{displayedCheck?.component}
</ActionRequiredContainer>
)}
</Group>
<Stack mt="2rem">
<Stack style={{ marginTop: '2rem' }}>
{canReturnHome && (
<>
<Group
noWrap
position="center"
justify="center"
wrap="nowrap"
>
<RiCheckFill
color="var(--success-color)"
@ -72,7 +71,7 @@ const ActionRequiredRoute = () => {
<Button
component={Link}
disabled={!canReturnHome}
leftIcon={<RiHome4Line />}
leftSection={<RiHome4Line />}
to={AppRoute.HOME}
variant="filled"
>
@ -82,12 +81,12 @@ const ActionRequiredRoute = () => {
)}
{!displayedCheck && (
<Group
noWrap
position="center"
justify="center"
wrap="nowrap"
>
<Button
fullWidth
leftIcon={<RiEdit2Line />}
leftSection={<RiEdit2Line />}
variant="filled"
onClick={handleManageServersModal}
>

View File

@ -1,7 +1,6 @@
import { Center, Group, Stack } from '@mantine/core';
import { RiQuestionLine } from 'react-icons/ri';
import { useLocation, useNavigate } from 'react-router-dom';
import { Button, Text } from '/@/renderer/components';
import { Button, Center, Group, Stack, Text } from '/@/renderer/components';
import { AnimatedPage } from '/@/renderer/features/shared';
const InvalidRoute = () => {
@ -10,11 +9,11 @@ const InvalidRoute = () => {
return (
<AnimatedPage>
<Center sx={{ height: '100%', width: '100%' }}>
<Center style={{ height: '100%', width: '100%' }}>
<Stack>
<Group
noWrap
position="center"
justify="center"
wrap="nowrap"
>
<RiQuestionLine
color="var(--warning-color)"

View File

@ -1,7 +1,6 @@
import { MutableRefObject, useCallback, useMemo } from 'react';
import { RowDoubleClickedEvent, RowHeightParams, RowNode } from '@ag-grid-community/core';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { Box, Group, Stack } from '@mantine/core';
import { useSetState } from '@mantine/hooks';
import { useTranslation } from 'react-i18next';
import { FaLastfmSquare } from 'react-icons/fa';
@ -12,7 +11,7 @@ import { Link } from 'react-router-dom';
import styled from 'styled-components';
import { queryKeys } from '/@/renderer/api/query-keys';
import { AlbumListSort, LibraryItem, QueueSong, SortOrder } from '/@/renderer/api/types';
import { Button, Popover, Spoiler } from '/@/renderer/components';
import { Button, Group, Popover, Spoiler, Stack } from '/@/renderer/components';
import { MemoizedSwiperGridCarousel } from '/@/renderer/components/grid-carousel';
import {
TableConfigDropdown,
@ -328,19 +327,19 @@ export const AlbumDetailContent = ({ tableRef, background }: AlbumDetailContentP
<ContentContainer>
<LibraryBackgroundOverlay $backgroundColor={background} />
<DetailContainer>
<Box component="section">
<section>
<Group
position="apart"
spacing="sm"
gap="sm"
justify="space-between"
>
<Group>
<PlayButton onClick={() => handlePlay(playButtonBehavior)} />
<Button
compact
loading={
createFavoriteMutation.isLoading ||
deleteFavoriteMutation.isLoading
}
size="compact-md"
variant="subtle"
onClick={handleFavorite}
>
@ -354,7 +353,7 @@ export const AlbumDetailContent = ({ tableRef, background }: AlbumDetailContentP
)}
</Button>
<Button
compact
size="compact-md"
variant="subtle"
onClick={(e) => {
if (!detailQuery?.data) return;
@ -368,8 +367,7 @@ export const AlbumDetailContent = ({ tableRef, background }: AlbumDetailContentP
<Popover position="bottom-end">
<Popover.Target>
<Button
compact
size="md"
size="compact-md"
variant="subtle"
>
<RiSettings2Fill size={20} />
@ -380,17 +378,16 @@ export const AlbumDetailContent = ({ tableRef, background }: AlbumDetailContentP
</Popover.Dropdown>
</Popover>
</Group>
</Box>
</section>
{showGenres && (
<Box component="section">
<Group spacing="sm">
<section>
<Group gap="sm">
{detailQuery?.data?.genres?.map((genre) => (
<Button
key={`genre-${genre.id}`}
compact
component={Link}
radius={0}
size="md"
size="compact-md"
to={generatePath(genreRoute, {
genreId: genre.id,
})}
@ -400,20 +397,19 @@ export const AlbumDetailContent = ({ tableRef, background }: AlbumDetailContentP
</Button>
))}
</Group>
</Box>
</section>
)}
{externalLinks ? (
<Box component="section">
<Group spacing="sm">
<section>
<Group gap="sm">
<Button
compact
component="a"
href={`https://www.last.fm/music/${encodeURIComponent(
detailQuery?.data?.albumArtist || '',
)}/${encodeURIComponent(detailQuery.data?.name || '')}`}
radius="md"
rel="noopener noreferrer"
size="md"
size="compact-md"
target="_blank"
tooltip={{
label: t('action.openIn.lastfm'),
@ -424,12 +420,11 @@ export const AlbumDetailContent = ({ tableRef, background }: AlbumDetailContentP
</Button>
{mbzId ? (
<Button
compact
component="a"
href={`https://musicbrainz.org/release/${mbzId}`}
radius="md"
rel="noopener noreferrer"
size="md"
size="compact-md"
target="_blank"
tooltip={{
label: t('action.openIn.musicbrainz'),
@ -440,14 +435,14 @@ export const AlbumDetailContent = ({ tableRef, background }: AlbumDetailContentP
</Button>
) : null}
</Group>
</Box>
</section>
) : null}
{comment && (
<Box component="section">
<section>
<Spoiler maxHeight={75}>{replaceURLWithHTMLLinks(comment)}</Spoiler>
</Box>
</section>
)}
<Box style={{ minHeight: '300px' }}>
<div style={{ minHeight: '300px' }}>
<VirtualTable
key={`table-${tableConfig.rowHeight}`}
ref={tableRef}
@ -482,11 +477,11 @@ export const AlbumDetailContent = ({ tableRef, background }: AlbumDetailContentP
onColumnMoved={onColumnMoved}
onRowDoubleClicked={handleRowDoubleClick}
/>
</Box>
</div>
<Stack
ref={cq.ref}
mt="3rem"
spacing="lg"
gap="lg"
style={{ marginTop: '3rem' }}
>
{cq.height || cq.width ? (
<>

View File

@ -1,9 +1,8 @@
import { Group, Stack } from '@mantine/core';
import { forwardRef, Fragment, Ref } from 'react';
import { generatePath, useParams } from 'react-router';
import { Link } from 'react-router-dom';
import { LibraryItem, ServerType } from '/@/renderer/api/types';
import { Rating, Text } from '/@/renderer/components';
import { Group, Rating, Stack, Text } from '/@/renderer/components';
import { useAlbumDetail } from '/@/renderer/features/albums/queries/album-detail-query';
import { LibraryHeader, useSetRating } from '/@/renderer/features/shared';
import { useContainerQuery } from '/@/renderer/hooks';
@ -66,8 +65,8 @@ export const AlbumDetailHeader = forwardRef(
item={{ route: AppRoute.LIBRARY_ALBUMS, type: LibraryItem.ALBUM }}
title={detailQuery?.data?.name || ''}
>
<Stack spacing="sm">
<Group spacing="sm">
<Stack gap="sm">
<Group gap="sm">
{metadataItems.map((item, index) => (
<Fragment key={`item-${item.id}-${index}`}>
{index > 0 && <Text $noSelect></Text>}
@ -89,11 +88,11 @@ export const AlbumDetailHeader = forwardRef(
)}
</Group>
<Group
mah="4rem"
spacing="md"
sx={{
gap="md"
style={{
WebkitBoxOrient: 'vertical',
WebkitLineClamp: 2,
maxHeight: '4rem',
overflow: 'hidden',
}}
>

View File

@ -1,6 +1,5 @@
import { ChangeEvent, MouseEvent, MutableRefObject, useCallback, useMemo } from 'react';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { Divider, Flex, Group, Stack } from '@mantine/core';
import { openModal } from '@mantine/modals';
import { useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
@ -16,7 +15,18 @@ import {
} from 'react-icons/ri';
import { queryKeys } from '/@/renderer/api/query-keys';
import { AlbumListSort, LibraryItem, ServerType, SortOrder } from '/@/renderer/api/types';
import { Button, DropdownMenu, MultiSelect, Slider, Switch, Text } from '/@/renderer/components';
import {
Button,
Divider,
DropdownMenu,
Flex,
Group,
MultiSelect,
Slider,
Stack,
Switch,
Text,
} from '/@/renderer/components';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
import { ALBUM_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
import { useListContext } from '/@/renderer/context/list-context';
@ -296,7 +306,7 @@ export const AlbumListHeaderFilters = ({ gridRef, tableRef }: AlbumListHeaderFil
[pageKey, setDisplayType],
);
const handleTableColumns = (values: TableColumn[]) => {
const handleTableColumns = (values: TableColumn[] | any[]) => {
const existingColumns = table.columns;
if (values.length === 0) {
@ -352,15 +362,14 @@ export const AlbumListHeaderFilters = ({ gridRef, tableRef }: AlbumListHeaderFil
<Flex justify="space-between">
<Group
ref={cq.ref}
spacing="sm"
w="100%"
gap="sm"
style={{ width: '100%' }}
>
<DropdownMenu position="bottom-start">
<DropdownMenu.Target>
<Button
compact
fw={600}
size="md"
size="compact-md"
variant="subtle"
>
{sortByLabel}
@ -390,10 +399,9 @@ export const AlbumListHeaderFilters = ({ gridRef, tableRef }: AlbumListHeaderFil
<DropdownMenu position="bottom-start">
<DropdownMenu.Target>
<Button
compact
fw={600}
size="md"
sx={{
size="compact-md"
style={{
svg: {
fill: isFolderFilterApplied
? 'var(--primary-color) !important'
@ -422,9 +430,8 @@ export const AlbumListHeaderFilters = ({ gridRef, tableRef }: AlbumListHeaderFil
)}
<Divider orientation="vertical" />
<Button
compact
size="md"
sx={{
size="compact-md"
style={{
svg: {
fill: isFilterApplied ? 'var(--primary-color) !important' : undefined,
},
@ -439,8 +446,7 @@ export const AlbumListHeaderFilters = ({ gridRef, tableRef }: AlbumListHeaderFil
</Button>
<Divider orientation="vertical" />
<Button
compact
size="md"
size="compact-md"
tooltip={{ label: t('common.refresh', { postProcess: 'sentenceCase' }) }}
variant="subtle"
onClick={handleRefresh}
@ -451,8 +457,7 @@ export const AlbumListHeaderFilters = ({ gridRef, tableRef }: AlbumListHeaderFil
<DropdownMenu position="bottom-start">
<DropdownMenu.Target>
<Button
compact
size="md"
size="compact-md"
variant="subtle"
>
<RiMoreFill size={15} />
@ -460,26 +465,26 @@ export const AlbumListHeaderFilters = ({ gridRef, tableRef }: AlbumListHeaderFil
</DropdownMenu.Target>
<DropdownMenu.Dropdown>
<DropdownMenu.Item
icon={<RiPlayFill />}
leftSection={<RiPlayFill />}
onClick={() => handlePlay?.({ playType: Play.NOW })}
>
{t('player.play', { postProcess: 'sentenceCase' })}
</DropdownMenu.Item>
<DropdownMenu.Item
icon={<RiAddBoxFill />}
leftSection={<RiAddBoxFill />}
onClick={() => handlePlay?.({ playType: Play.LAST })}
>
{t('player.addLast', { postProcess: 'sentenceCase' })}
</DropdownMenu.Item>
<DropdownMenu.Item
icon={<RiAddCircleFill />}
leftSection={<RiAddCircleFill />}
onClick={() => handlePlay?.({ playType: Play.NEXT })}
>
{t('player.addNext', { postProcess: 'sentenceCase' })}
</DropdownMenu.Item>
<DropdownMenu.Divider />
<DropdownMenu.Item
icon={<RiRefreshLine />}
leftSection={<RiRefreshLine />}
onClick={handleRefresh}
>
{t('common.refresh', { postProcess: 'sentenceCase' })}
@ -488,8 +493,8 @@ export const AlbumListHeaderFilters = ({ gridRef, tableRef }: AlbumListHeaderFil
</DropdownMenu>
</Group>
<Group
noWrap
spacing="sm"
gap="sm"
wrap="nowrap"
>
<DropdownMenu
position="bottom-end"
@ -497,8 +502,7 @@ export const AlbumListHeaderFilters = ({ gridRef, tableRef }: AlbumListHeaderFil
>
<DropdownMenu.Target>
<Button
compact
size="md"
size="compact-md"
tooltip={{
label: t('common.configure', { postProcess: 'sentenceCase' }),
}}
@ -573,7 +577,7 @@ export const AlbumListHeaderFilters = ({ gridRef, tableRef }: AlbumListHeaderFil
<DropdownMenu.Item
closeMenuOnClick={false}
component="div"
sx={{ cursor: 'default' }}
style={{ cursor: 'default' }}
>
<Stack>
<MultiSelect
@ -585,7 +589,7 @@ export const AlbumListHeaderFilters = ({ gridRef, tableRef }: AlbumListHeaderFil
width={300}
onChange={handleTableColumns}
/>
<Group position="apart">
<Group justify="space-between">
<Text>Auto Fit Columns</Text>
<Switch
defaultChecked={table.autoFit}

View File

@ -1,10 +1,9 @@
import { useEffect, useRef, type ChangeEvent, type MutableRefObject } from 'react';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { Flex, Group, Stack } from '@mantine/core';
import debounce from 'lodash/debounce';
import { useTranslation } from 'react-i18next';
import { LibraryItem } from '/@/renderer/api/types';
import { PageHeader, SearchInput } from '/@/renderer/components';
import { Flex, Group, PageHeader, SearchInput, Stack } from '/@/renderer/components';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
import { AlbumListHeaderFilters } from '/@/renderer/features/albums/components/album-list-header-filters';
import { FilterBar, LibraryHeaderBar } from '/@/renderer/features/shared';
@ -57,7 +56,7 @@ export const AlbumListHeader = ({
return (
<Stack
ref={cq.ref}
spacing={0}
gap={0}
>
<PageHeader backgroundColor="var(--titlebar-bg)">
<Flex

View File

@ -1,10 +1,18 @@
import { ChangeEvent, useMemo, useState } from 'react';
import { Divider, Group, Stack } from '@mantine/core';
import debounce from 'lodash/debounce';
import { useTranslation } from 'react-i18next';
import { useListFilterByKey } from '../../../store/list.store';
import { AlbumArtistListSort, GenreListSort, LibraryItem, SortOrder } from '/@/renderer/api/types';
import { MultiSelect, NumberInput, SpinnerIcon, Switch, Text } from '/@/renderer/components';
import {
Divider,
Group,
MultiSelect,
NumberInput,
SpinnerIcon,
Stack,
Switch,
Text,
} from '/@/renderer/components';
import { useAlbumArtistList } from '/@/renderer/features/artists/queries/album-artist-list-query';
import { useGenreList } from '/@/renderer/features/genres';
import { AlbumListFilter, useListStoreActions } from '/@/renderer/store';
@ -176,11 +184,11 @@ export const JellyfinAlbumFilters = ({
};
return (
<Stack p="0.8rem">
<Stack style={{ padding: '0.8rem' }}>
{toggleFilters.map((filter) => (
<Group
key={`nd-filter-${filter.label}`}
position="apart"
justify="space-between"
>
<Text>{filter.label}</Text>
<Switch
@ -190,7 +198,7 @@ export const JellyfinAlbumFilters = ({
/>
</Group>
))}
<Divider my="0.5rem" />
<Divider marginY="0.5rem" />
<Group grow>
<NumberInput
defaultValue={filter?._custom?.jellyfin?.minYear}

View File

@ -1,6 +1,14 @@
import { ChangeEvent, useMemo, useState } from 'react';
import { Divider, Group, Stack } from '@mantine/core';
import { NumberInput, Switch, Text, Select, SpinnerIcon } from '/@/renderer/components';
import {
NumberInput,
Switch,
Text,
Select,
SpinnerIcon,
Divider,
Group,
Stack,
} from '/@/renderer/components';
import { AlbumListFilter, useListStoreActions, useListStoreByKey } from '/@/renderer/store';
import debounce from 'lodash/debounce';
import { useGenreList } from '/@/renderer/features/genres';
@ -210,11 +218,11 @@ export const NavidromeAlbumFilters = ({
};
return (
<Stack p="0.8rem">
<Stack style={{ padding: '0.8rem' }}>
{toggleFilters.map((filter) => (
<Group
key={`nd-filter-${filter.label}`}
position="apart"
justify="space-between"
>
<Text>{filter.label}</Text>
<Switch
@ -223,7 +231,7 @@ export const NavidromeAlbumFilters = ({
/>
</Group>
))}
<Divider my="0.5rem" />
<Divider marginY="0.5rem" />
<Group grow>
<NumberInput
defaultValue={filter._custom?.navidrome?.year}

View File

@ -1,4 +1,4 @@
import { Button, Spinner, Spoiler, Text } from '/@/renderer/components';
import { Button, Center, Group, Spinner, Spoiler, Stack, Text } from '/@/renderer/components';
import {
AnimatedPage,
LibraryHeader,
@ -13,7 +13,6 @@ import { usePlayQueueAdd } from '/@/renderer/features/player';
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import { LibraryItem, SongDetailResponse } from '/@/renderer/api/types';
import { useCurrentServer } from '/@/renderer/store';
import { Stack, Group, Box, Center } from '@mantine/core';
import { Link } from 'react-router-dom';
import { AppRoute } from '/@/renderer/router/routes';
import { formatDurationString } from '/@/renderer/utils';
@ -139,8 +138,8 @@ const DummyAlbumDetailRoute = () => {
item={{ route: AppRoute.LIBRARY_SONGS, type: LibraryItem.SONG }}
title={detailQuery?.data?.name || ''}
>
<Stack spacing="sm">
<Group spacing="sm">
<Stack gap="sm">
<Group gap="sm">
{metadataItems.map((item, index) => (
<Fragment key={`item-${item.id}-${index}`}>
{index > 0 && <Text $noSelect></Text>}
@ -149,11 +148,11 @@ const DummyAlbumDetailRoute = () => {
))}
</Group>
<Group
mah="4rem"
spacing="md"
sx={{
gap="md"
style={{
WebkitBoxOrient: 'vertical',
WebkitLineClamp: 2,
maxHeight: '4rem',
overflow: 'hidden',
}}
>
@ -177,19 +176,19 @@ const DummyAlbumDetailRoute = () => {
</LibraryHeader>
</Stack>
<DetailContainer>
<Box component="section">
<section>
<Group
position="apart"
spacing="sm"
gap="sm"
justify="space-between"
>
<Group>
<PlayButton onClick={() => handlePlay()} />
<Button
compact
loading={
createFavoriteMutation.isLoading ||
deleteFavoriteMutation.isLoading
}
size="compact-md"
variant="subtle"
onClick={handleFavorite}
>
@ -203,7 +202,7 @@ const DummyAlbumDetailRoute = () => {
)}
</Button>
<Button
compact
size="compact-md"
variant="subtle"
onClick={(e) => {
if (!detailQuery?.data) return;
@ -214,17 +213,16 @@ const DummyAlbumDetailRoute = () => {
</Button>
</Group>
</Group>
</Box>
</section>
{showGenres && (
<Box component="section">
<Group spacing="sm">
<section>
<Group gap="sm">
{detailQuery?.data?.genres?.map((genre) => (
<Button
key={`genre-${genre.id}`}
compact
component={Link}
radius={0}
size="md"
size="compact-md"
to={generatePath(AppRoute.LIBRARY_GENRES_SONGS, {
genreId: genre.id,
})}
@ -234,16 +232,16 @@ const DummyAlbumDetailRoute = () => {
</Button>
))}
</Group>
</Box>
</section>
)}
{comment && (
<Box component="section">
<section>
<Spoiler maxHeight={75}>{replaceURLWithHTMLLinks(comment)}</Spoiler>
</Box>
</section>
)}
<Box component="section">
<section>
<Center>
<Group mr={5}>
<Group style={{ marginRight: '5px' }}>
<RiErrorWarningLine
color="var(--danger-color)"
size={30}
@ -251,7 +249,7 @@ const DummyAlbumDetailRoute = () => {
</Group>
<h2>{t('error.badAlbum', { postProcess: 'sentenceCase' })}</h2>
</Center>
</Box>
</section>
</DetailContainer>
</AnimatedPage>
);

View File

@ -1,6 +1,5 @@
import { useMemo } from 'react';
import { ColDef, RowDoubleClickedEvent } from '@ag-grid-community/core';
import { Box, Group, Stack } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { FaLastfmSquare } from 'react-icons/fa';
import { RiHeartFill, RiHeartLine, RiMoreFill } from 'react-icons/ri';
@ -17,7 +16,7 @@ import {
ServerType,
SortOrder,
} from '/@/renderer/api/types';
import { Button, Spoiler, TextTitle } from '/@/renderer/components';
import { Button, Group, Spoiler, Stack, TextTitle } from '/@/renderer/components';
import { MemoizedSwiperGridCarousel } from '/@/renderer/components/grid-carousel';
import { getColumnDefs, VirtualTable } from '/@/renderer/components/virtual-table';
import { useAlbumList } from '/@/renderer/features/albums/queries/album-list-query';
@ -221,10 +220,10 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
})}
</TextTitle>
<Button
compact
uppercase
component={Link}
size="compact-md"
to={artistDiscographyLink}
tt="uppercase"
variant="subtle"
>
{t('page.albumArtistDetail.viewDiscography')}
@ -354,14 +353,14 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
<ContentContainer ref={cq.ref}>
<LibraryBackgroundOverlay $backgroundColor={background} />
<DetailContainer>
<Group spacing="md">
<Group gap="md">
<PlayButton onClick={() => handlePlay(playButtonBehavior)} />
<Group spacing="xs">
<Group gap="xs">
<Button
compact
loading={
createFavoriteMutation.isLoading || deleteFavoriteMutation.isLoading
}
size="compact-md"
variant="subtle"
onClick={handleFavorite}
>
@ -375,7 +374,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
)}
</Button>
<Button
compact
size="compact-md"
variant="subtle"
onClick={(e) => {
if (!detailQuery?.data) return;
@ -386,36 +385,35 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
</Button>
</Group>
</Group>
<Group spacing="md">
<Group gap="md">
<Button
compact
uppercase
component={Link}
size="compact-md"
to={artistDiscographyLink}
tt="uppercase"
variant="subtle"
>
{t('page.albumArtistDetail.viewDiscography')}
</Button>
<Button
compact
uppercase
component={Link}
size="compact-md"
to={artistSongsLink}
tt="uppercase"
variant="subtle"
>
{t('page.albumArtistDetail.viewAllTracks')}
</Button>
</Group>
{showGenres ? (
<Box component="section">
<Group spacing="sm">
<section>
<Group gap="sm">
{detailQuery?.data?.genres?.map((genre) => (
<Button
key={`genre-${genre.id}`}
compact
component={Link}
radius="md"
size="md"
size="compact-md"
to={generatePath(genrePath, {
genreId: genre.id,
})}
@ -425,20 +423,19 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
</Button>
))}
</Group>
</Box>
</section>
) : null}
{externalLinks ? (
<Box component="section">
<Group spacing="sm">
<section>
<Group gap="sm">
<Button
compact
component="a"
href={`https://www.last.fm/music/${encodeURIComponent(
detailQuery?.data?.name || '',
)}`}
radius="md"
rel="noopener noreferrer"
size="md"
size="compact-md"
target="_blank"
tooltip={{
label: t('action.openIn.lastfm'),
@ -449,12 +446,11 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
</Button>
{mbzId ? (
<Button
compact
component="a"
href={`https://musicbrainz.org/artist/${mbzId}`}
radius="md"
rel="noopener noreferrer"
size="md"
size="compact-md"
target="_blank"
tooltip={{
label: t('action.openIn.musicbrainz'),
@ -465,13 +461,10 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
</Button>
) : null}
</Group>
</Box>
</section>
) : null}
{biography ? (
<Box
component="section"
maw="1280px"
>
<section style={{ maxWidth: '1280px' }}>
<TextTitle
order={2}
weight={700}
@ -481,17 +474,17 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
})}
</TextTitle>
<Spoiler dangerouslySetInnerHTML={{ __html: biography }} />
</Box>
</section>
) : null}
{showTopSongs ? (
<Box component="section">
<section>
<Group
noWrap
position="apart"
justify="space-between"
wrap="nowrap"
>
<Group
noWrap
align="flex-end"
wrap="nowrap"
>
<TextTitle
order={2}
@ -502,15 +495,15 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
})}
</TextTitle>
<Button
compact
uppercase
component={Link}
size="compact-md"
to={generatePath(
AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_TOP_SONGS,
{
albumArtistId,
},
)}
tt="uppercase"
variant="subtle"
>
{t('page.albumArtistDetail.viewAll', {
@ -537,10 +530,10 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
onCellContextMenu={handleContextMenu}
onRowDoubleClicked={handleRowDoubleClick}
/>
</Box>
</section>
) : null}
<Box component="section">
<Stack spacing="xl">
<section>
<Stack gap="xl">
{carousels
.filter((c) => !c.isHidden)
.map((carousel) => (
@ -563,7 +556,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
/>
))}
</Stack>
</Box>
</section>
</DetailContainer>
</ContentContainer>
);

View File

@ -1,8 +1,7 @@
import { forwardRef, Fragment, Ref } from 'react';
import { Group, Rating, Stack } from '@mantine/core';
import { useParams } from 'react-router';
import { LibraryItem, ServerType } from '/@/renderer/api/types';
import { Text } from '/@/renderer/components';
import { Group, Rating, Stack, Text } from '/@/renderer/components';
import { useAlbumArtistDetail } from '/@/renderer/features/artists/queries/album-artist-detail-query';
import { LibraryHeader, useSetRating } from '/@/renderer/features/shared';
import { AppRoute } from '/@/renderer/router/routes';

View File

@ -47,8 +47,8 @@ export const AlbumArtistDetailTopSongsListHeader = ({
<DropdownMenu position="bottom-start">
<DropdownMenu.Target>
<Button
compact
fw="600"
size="compact-md"
variant="subtle"
>
<RiMoreFill size={15} />
@ -56,19 +56,19 @@ export const AlbumArtistDetailTopSongsListHeader = ({
</DropdownMenu.Target>
<DropdownMenu.Dropdown>
<DropdownMenu.Item
icon={<RiPlayFill />}
leftSection={<RiPlayFill />}
onClick={() => handlePlay(Play.NOW)}
>
{t('player.play', { postProcess: 'sentenceCase' })}
</DropdownMenu.Item>
<DropdownMenu.Item
icon={<RiAddBoxFill />}
leftSection={<RiAddBoxFill />}
onClick={() => handlePlay(Play.LAST)}
>
{t('player.addLast', { postProcess: 'sentenceCase' })}
</DropdownMenu.Item>
<DropdownMenu.Item
icon={<RiAddCircleFill />}
leftSection={<RiAddCircleFill />}
onClick={() => handlePlay(Play.NEXT)}
>
{t('player.addNext', { postProcess: 'sentenceCase' })}

View File

@ -1,7 +1,6 @@
import { ChangeEvent, MouseEvent, MutableRefObject, useCallback } from 'react';
import { IDatasource } from '@ag-grid-community/core';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { Divider, Flex, Group, Stack } from '@mantine/core';
import { useQueryClient } from '@tanstack/react-query';
import debounce from 'lodash/debounce';
import { useTranslation } from 'react-i18next';
@ -10,7 +9,18 @@ import { useListContext } from '../../../context/list-context';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { AlbumArtistListSort, LibraryItem, ServerType, SortOrder } from '/@/renderer/api/types';
import { Button, DropdownMenu, MultiSelect, Slider, Switch, Text } from '/@/renderer/components';
import {
Button,
Divider,
DropdownMenu,
Flex,
Group,
MultiSelect,
Slider,
Stack,
Switch,
Text,
} from '/@/renderer/components';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
import { ALBUMARTIST_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
import { OrderToggleButton, useMusicFolders } from '/@/renderer/features/shared';
@ -284,7 +294,7 @@ export const AlbumArtistListHeaderFilters = ({
[pageKey, setDisplayType],
);
const handleTableColumns = (values: TableColumn[]) => {
const handleTableColumns = (values: TableColumn[] | any[]) => {
const existingColumns = table.columns;
if (values.length === 0) {
@ -329,15 +339,14 @@ export const AlbumArtistListHeaderFilters = ({
<Flex justify="space-between">
<Group
ref={cq.ref}
spacing="sm"
w="100%"
gap="sm"
style={{ width: '100%' }}
>
<DropdownMenu position="bottom-start">
<DropdownMenu.Target>
<Button
compact
fw="600"
size="md"
size="compact-md"
variant="subtle"
>
{sortByLabel}
@ -367,9 +376,8 @@ export const AlbumArtistListHeaderFilters = ({
<DropdownMenu position="bottom-start">
<DropdownMenu.Target>
<Button
compact
fw="600"
size="md"
size="compact-md"
variant="subtle"
>
{cq.isMd ? 'Folder' : <RiFolder2Line size={15} />}
@ -392,8 +400,7 @@ export const AlbumArtistListHeaderFilters = ({
)}
<Divider orientation="vertical" />
<Button
compact
size="md"
size="compact-md"
tooltip={{ label: t('common.refresh', { postProcess: 'titleCase' }) }}
variant="subtle"
onClick={handleRefresh}
@ -404,8 +411,7 @@ export const AlbumArtistListHeaderFilters = ({
<DropdownMenu position="bottom-start">
<DropdownMenu.Target>
<Button
compact
size="md"
size="compact-md"
variant="subtle"
>
<RiMoreFill size={15} />
@ -413,7 +419,7 @@ export const AlbumArtistListHeaderFilters = ({
</DropdownMenu.Target>
<DropdownMenu.Dropdown>
<DropdownMenu.Item
icon={<RiRefreshLine />}
leftSection={<RiRefreshLine />}
onClick={handleRefresh}
>
Refresh
@ -428,8 +434,7 @@ export const AlbumArtistListHeaderFilters = ({
>
<DropdownMenu.Target>
<Button
compact
size="md"
size="compact-md"
variant="subtle"
>
<RiSettings3Fill size="1.3rem" />
@ -520,7 +525,7 @@ export const AlbumArtistListHeaderFilters = ({
<DropdownMenu.Item
closeMenuOnClick={false}
component="div"
sx={{ cursor: 'default' }}
style={{ cursor: 'default' }}
>
<Stack>
<MultiSelect
@ -532,7 +537,7 @@ export const AlbumArtistListHeaderFilters = ({
width={300}
onChange={handleTableColumns}
/>
<Group position="apart">
<Group justify="space-between">
<Text>
{t('table.config.general.autoFitColumns', {
postProcess: 'sentenceCase',

View File

@ -1,11 +1,10 @@
import type { ChangeEvent, MutableRefObject } from 'react';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { Flex, Group, Stack } from '@mantine/core';
import debounce from 'lodash/debounce';
import { useTranslation } from 'react-i18next';
import { FilterBar } from '../../shared/components/filter-bar';
import { LibraryItem } from '/@/renderer/api/types';
import { PageHeader, SearchInput } from '/@/renderer/components';
import { Flex, Group, PageHeader, SearchInput, Stack } from '/@/renderer/components';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
import { AlbumArtistListHeaderFilters } from '/@/renderer/features/artists/components/album-artist-list-header-filters';
import { LibraryHeaderBar } from '/@/renderer/features/shared';
@ -43,12 +42,12 @@ export const AlbumArtistListHeader = ({
return (
<Stack
ref={cq.ref}
spacing={0}
gap={0}
>
<PageHeader backgroundColor="var(--titlebar-bg)">
<Flex
justify="space-between"
w="100%"
style={{ width: '100%' }}
>
<LibraryHeaderBar>
<LibraryHeaderBar.Title>

View File

@ -1,6 +1,5 @@
import { createContext, Fragment, ReactNode, useState, useMemo, useCallback } from 'react';
import { RowNode } from '@ag-grid-community/core';
import { Divider, Group, Portal, Stack } from '@mantine/core';
import {
useClickOutside,
useMergedRef,
@ -35,8 +34,12 @@ import {
ConfirmModal,
ContextMenu,
ContextMenuButton,
Divider,
Group,
HoverCard,
Portal,
Rating,
Stack,
Text,
toast,
} from '/@/renderer/components';
@ -861,9 +864,9 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
xPos={ctx.xPos}
yPos={ctx.yPos}
>
<Stack spacing={0}>
<Stack gap={0}>
<Stack
spacing={0}
gap={0}
onClick={closeContextMenu}
>
{ctx.menuItems?.map((item) => {
@ -897,7 +900,7 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
</ContextMenuButton>
</HoverCard.Target>
<HoverCard.Dropdown>
<Stack spacing={0}>
<Stack gap={0}>
{contextMenuItems[
item.id
].children?.map((child) => (
@ -937,7 +940,6 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
<Divider
key={`context-menu-divider-${item.id}`}
color="rgb(62, 62, 62)"
size="sm"
/>
)}
</Fragment>
@ -945,10 +947,7 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
);
})}
</Stack>
<Divider
color="rgb(62, 62, 62)"
size="sm"
/>
<Divider color="rgb(62, 62, 62)" />
<ContextMenuButton disabled>
{t('page.contextMenu.numberSelected', {
count: ctx.data?.length || 0,

View File

@ -1,5 +1,5 @@
import { GridOptions, RowNode } from '@ag-grid-community/core';
import { createUseExternalEvents } from '@mantine/utils';
import { createUseExternalEvents } from '@mantine/core';
import { LibraryItem } from '/@/renderer/api/types';
export type OpenContextMenuProps = {

View File

@ -1,6 +1,5 @@
import { ChangeEvent, MouseEvent, MutableRefObject, useCallback, useMemo } from 'react';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { Divider, Flex, Group, Stack } from '@mantine/core';
import { useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import {
@ -13,7 +12,18 @@ import {
} from 'react-icons/ri';
import { queryKeys } from '/@/renderer/api/query-keys';
import { GenreListSort, LibraryItem, ServerType, SortOrder } from '/@/renderer/api/types';
import { Button, DropdownMenu, MultiSelect, Slider, Switch, Text } from '/@/renderer/components';
import {
Button,
Divider,
DropdownMenu,
Flex,
Group,
MultiSelect,
Slider,
Stack,
Switch,
Text,
} from '/@/renderer/components';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
import { GENRE_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
import { useListContext } from '/@/renderer/context/list-context';
@ -182,7 +192,7 @@ export const GenreListHeaderFilters = ({ gridRef, tableRef }: GenreListHeaderFil
[pageKey, setDisplayType],
);
const handleTableColumns = (values: TableColumn[]) => {
const handleTableColumns = (values: TableColumn[] | any[]) => {
const existingColumns = table.columns;
if (values.length === 0) {
@ -229,15 +239,14 @@ export const GenreListHeaderFilters = ({ gridRef, tableRef }: GenreListHeaderFil
<Flex justify="space-between">
<Group
ref={cq.ref}
spacing="sm"
w="100%"
gap="sm"
style={{ width: '100%' }}
>
<DropdownMenu position="bottom-start">
<DropdownMenu.Target>
<Button
compact
fw={600}
size="md"
size="compact-md"
variant="subtle"
>
{sortByLabel}
@ -267,10 +276,9 @@ export const GenreListHeaderFilters = ({ gridRef, tableRef }: GenreListHeaderFil
<DropdownMenu position="bottom-start">
<DropdownMenu.Target>
<Button
compact
fw={600}
size="md"
sx={{
size="compact-md"
style={{
svg: {
fill: isFolderFilterApplied
? 'var(--primary-color) !important'
@ -299,8 +307,7 @@ export const GenreListHeaderFilters = ({ gridRef, tableRef }: GenreListHeaderFil
)}
<Divider orientation="vertical" />
<Button
compact
size="md"
size="compact-md"
tooltip={{ label: t('common.refresh', { postProcess: 'titleCase' }) }}
variant="subtle"
onClick={handleRefresh}
@ -311,8 +318,7 @@ export const GenreListHeaderFilters = ({ gridRef, tableRef }: GenreListHeaderFil
<DropdownMenu position="bottom-start">
<DropdownMenu.Target>
<Button
compact
size="md"
size="compact-md"
variant="subtle"
>
<RiMoreFill size={15} />
@ -320,7 +326,7 @@ export const GenreListHeaderFilters = ({ gridRef, tableRef }: GenreListHeaderFil
</DropdownMenu.Target>
<DropdownMenu.Dropdown>
<DropdownMenu.Item
icon={<RiRefreshLine />}
leftSection={<RiRefreshLine />}
onClick={handleRefresh}
>
{t('common.refresh', { postProcess: 'titleCase' })}
@ -328,8 +334,7 @@ export const GenreListHeaderFilters = ({ gridRef, tableRef }: GenreListHeaderFil
</DropdownMenu.Dropdown>
<Divider orientation="vertical" />
<Button
compact
size="md"
size="compact-md"
tooltip={{
label: t(
genreTarget === GenreTarget.ALBUM
@ -346,8 +351,8 @@ export const GenreListHeaderFilters = ({ gridRef, tableRef }: GenreListHeaderFil
</DropdownMenu>
</Group>
<Group
noWrap
spacing="sm"
gap="sm"
wrap="nowrap"
>
<DropdownMenu
position="bottom-end"
@ -355,8 +360,7 @@ export const GenreListHeaderFilters = ({ gridRef, tableRef }: GenreListHeaderFil
>
<DropdownMenu.Target>
<Button
compact
size="md"
size="compact-md"
tooltip={{
label: t('common.configure', { postProcess: 'titleCase' }),
}}
@ -426,7 +430,7 @@ export const GenreListHeaderFilters = ({ gridRef, tableRef }: GenreListHeaderFil
<DropdownMenu.Item
closeMenuOnClick={false}
component="div"
sx={{ cursor: 'default' }}
style={{ cursor: 'default' }}
>
<Stack>
<MultiSelect
@ -438,7 +442,7 @@ export const GenreListHeaderFilters = ({ gridRef, tableRef }: GenreListHeaderFil
width={300}
onChange={handleTableColumns}
/>
<Group position="apart">
<Group justify="space-between">
<Text>
{t('table.config.general.autoFitColumns', {
postProcess: 'titleCase',

View File

@ -1,9 +1,8 @@
import { ChangeEvent, MutableRefObject } from 'react';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { Flex, Group, Stack } from '@mantine/core';
import debounce from 'lodash/debounce';
import { LibraryItem } from '/@/renderer/api/types';
import { PageHeader, SearchInput } from '/@/renderer/components';
import { Flex, Group, PageHeader, SearchInput, Stack } from '/@/renderer/components';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
import { GenreListHeaderFilters } from '/@/renderer/features/genres/components/genre-list-header-filters';
import { FilterBar, LibraryHeaderBar } from '/@/renderer/features/shared';
@ -37,12 +36,12 @@ export const GenreListHeader = ({ itemCount, gridRef, tableRef }: GenreListHeade
return (
<Stack
ref={cq.ref}
spacing={0}
gap={0}
>
<PageHeader backgroundColor="var(--titlebar-bg)">
<Flex
justify="space-between"
w="100%"
style={{ width: '100%' }}
>
<LibraryHeaderBar>
<LibraryHeaderBar.Title>

View File

@ -1,5 +1,4 @@
import { useMemo, useRef } from 'react';
import { ActionIcon, Group, Stack } from '@mantine/core';
import {
AlbumListSort,
LibraryItem,
@ -7,7 +6,15 @@ import {
SongListSort,
SortOrder,
} from '/@/renderer/api/types';
import { FeatureCarousel, NativeScrollArea, Spinner, TextTitle } from '/@/renderer/components';
import {
ActionIcon,
FeatureCarousel,
Group,
NativeScrollArea,
Spinner,
Stack,
TextTitle,
} from '/@/renderer/components';
import { useAlbumList } from '/@/renderer/features/albums';
import { useRecentlyPlayed } from '/@/renderer/features/home/queries/recently-played-query';
import { AnimatedPage, LibraryHeaderBar } from '/@/renderer/features/shared';
@ -244,10 +251,10 @@ const HomeRoute = () => {
}}
>
<Stack
gap="lg"
mb="5rem"
pt={windowBarStyle === Platform.WEB ? '5rem' : '3rem'}
px="2rem"
spacing="lg"
>
<FeatureCarousel data={featureItemsWithImage} />
{sortedCarousel.map((carousel) => (

View File

@ -304,7 +304,7 @@ export const ItemDetailsModal = ({ item }: ItemDetailsModalProps) => {
<Table
highlightOnHover
horizontalSpacing="sm"
sx={{ userSelect: 'text' }}
style={{ userSelect: 'text' }}
verticalSpacing="sm"
>
<tbody>{body}</tbody>

View File

@ -1,5 +1,4 @@
import { useMemo } from 'react';
import { Divider, Group, Stack } from '@mantine/core';
import { useForm } from '@mantine/form';
import { useDebouncedValue } from '@mantine/hooks';
import { openModal } from '@mantine/modals';
@ -12,7 +11,15 @@ import {
LyricsOverride,
} from '../../../api/types';
import { useLyricSearch } from '../queries/lyric-search-query';
import { ScrollArea, Spinner, Text, TextInput } from '/@/renderer/components';
import {
Divider,
Group,
ScrollArea,
Spinner,
Stack,
Text,
TextInput,
} from '/@/renderer/components';
import i18n from '/@/i18n/i18n';
const SearchItem = styled.button`
@ -47,12 +54,12 @@ const SearchResult = ({ data, onClick }: SearchResultProps) => {
return (
<SearchItem onClick={onClick}>
<Group
noWrap
position="apart"
justify="space-between"
wrap="nowrap"
>
<Stack
gap={0}
maw="65%"
spacing={0}
>
<Text
size="md"
@ -62,8 +69,8 @@ const SearchResult = ({ data, onClick }: SearchResultProps) => {
</Text>
<Text $secondary>{artist}</Text>
<Group
noWrap
spacing="sm"
gap="sm"
wrap="nowrap"
>
<Text
$secondary
@ -146,7 +153,7 @@ export const LyricsSearchForm = ({ artist, name, onSearchOverride }: LyricSearch
type="auto"
w="100%"
>
<Stack spacing="md">
<Stack gap="md">
{searchResults.map((result) => (
<SearchResult
key={`${result.source}-${result.id}`}

View File

@ -1,6 +1,5 @@
import { ComponentPropsWithoutRef } from 'react';
import { TextTitle } from '/@/renderer/components/text-title';
import { TitleProps } from '@mantine/core';
import { TextTitle, TextTitleProps } from '/@/renderer/components/text-title';
import styled from 'styled-components';
interface LyricLineProps extends ComponentPropsWithoutRef<'div'> {
@ -9,7 +8,7 @@ interface LyricLineProps extends ComponentPropsWithoutRef<'div'> {
text: string;
}
const StyledText = styled(TextTitle)<TitleProps & { $alignment: string; $fontSize: number }>`
const StyledText = styled(TextTitle)<TextTitleProps & { $alignment: string; $fontSize: number }>`
padding: 0 1rem;
font-size: ${(props) => props.$fontSize}px;
font-weight: 600;

View File

@ -1,9 +1,9 @@
import { Box, Center, Group, Select, SelectItem } from '@mantine/core';
import { ComboboxItem } from '@mantine/core';
import isElectron from 'is-electron';
import { useTranslation } from 'react-i18next';
import { RiAddFill, RiSubtractFill } from 'react-icons/ri';
import { LyricsOverride } from '/@/renderer/api/types';
import { Button, NumberInput, Tooltip } from '/@/renderer/components';
import { Box, Button, Center, Group, NumberInput, Select, Tooltip } from '/@/renderer/components';
import { openLyricSearchModal } from '/@/renderer/features/lyrics/components/lyrics-search-form';
import {
useCurrentSong,
@ -14,7 +14,7 @@ import {
interface LyricsActionsProps {
index: number;
languages: SelectItem[];
languages: ComboboxItem[];
onRemoveLyric: () => void;
onResetLyric: () => void;
@ -61,11 +61,11 @@ export const LyricsActions = ({
</Center>
)}
<Group position="center">
<Group justify="center">
{isDesktop && sources.length ? (
<Button
uppercase
disabled={isActionsDisabled}
tt="uppercase"
variant="subtle"
onClick={() =>
openLyricSearchModal({
@ -94,7 +94,7 @@ export const LyricsActions = ({
styles={{ input: { textAlign: 'center' } }}
value={delayMs || 0}
width={55}
onChange={handleLyricOffset}
onChange={(e) => handleLyricOffset(Number(e))}
/>
</Tooltip>
<Button
@ -106,8 +106,8 @@ export const LyricsActions = ({
</Button>
{isDesktop && sources.length ? (
<Button
uppercase
disabled={isActionsDisabled}
tt="uppercase"
variant="subtle"
onClick={onResetLyric}
>
@ -119,9 +119,9 @@ export const LyricsActions = ({
<Box style={{ position: 'absolute', right: 0, top: 0 }}>
{isDesktop && sources.length ? (
<Button
uppercase
color="red"
disabled={isActionsDisabled}
tt="uppercase"
variant="subtle"
onClick={onRemoveLyric}
>

View File

@ -1,12 +1,11 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Center, Group } from '@mantine/core';
import { AnimatePresence, motion } from 'framer-motion';
import { ErrorBoundary } from 'react-error-boundary';
import { RiInformationFill } from 'react-icons/ri';
import styled from 'styled-components';
import { useSongLyricsByRemoteId, useSongLyricsBySong } from './queries/lyric-query';
import { SynchronizedLyrics, SynchronizedLyricsProps } from './synchronized-lyrics';
import { Spinner, TextTitle } from '/@/renderer/components';
import { Center, Group, Spinner, TextTitle } from '/@/renderer/components';
import { ErrorFallback } from '/@/renderer/features/action-required';
import {
UnsynchronizedLyrics,

View File

@ -1,9 +1,9 @@
import { useRef } from 'react';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { Box, Flex } from '@mantine/core';
import { PlayQueueListControls } from './play-queue-list-controls';
import { Song } from '/@/renderer/api/types';
import { PlayQueue } from '/@/renderer/features/now-playing/components/play-queue';
import { Box, Flex } from '/@/renderer/components';
export const DrawerPlayQueue = () => {
const queueRef = useRef<{ grid: AgGridReactType<Song> } | null>(null);
@ -15,7 +15,7 @@ export const DrawerPlayQueue = () => {
>
<Box
bg="var(--main-bg)"
sx={{ borderRadius: '10px' }}
style={{ borderRadius: '10px' }}
>
<PlayQueueListControls
tableRef={queueRef}

View File

@ -1,7 +1,6 @@
import type { MutableRefObject } from 'react';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { Group } from '@mantine/core';
import { Button, Popover } from '/@/renderer/components';
import { Button, Group, Popover } from '/@/renderer/components';
import isElectron from 'is-electron';
import { useTranslation } from 'react-i18next';
import {
@ -107,16 +106,15 @@ export const PlayQueueListControls = ({ type, tableRef }: PlayQueueListOptionsPr
return (
<Group
position="apart"
justify="space-between"
px="1rem"
py="1rem"
sx={{ alignItems: 'center' }}
style={{ alignItems: 'center' }}
w="100%"
>
<Group spacing="sm">
<Group gap="sm">
<Button
compact
size="md"
size="compact-md"
tooltip={{ label: t('player.shuffle', { postProcess: 'sentenceCase' }) }}
variant="default"
onClick={handleShuffleQueue}
@ -124,8 +122,7 @@ export const PlayQueueListControls = ({ type, tableRef }: PlayQueueListOptionsPr
<RiShuffleLine size="1.1rem" />
</Button>
<Button
compact
size="md"
size="compact-md"
tooltip={{ label: t('action.moveToBottom', { postProcess: 'sentenceCase' }) }}
variant="default"
onClick={handleMoveToBottom}
@ -133,8 +130,7 @@ export const PlayQueueListControls = ({ type, tableRef }: PlayQueueListOptionsPr
<RiArrowDownLine size="1.1rem" />
</Button>
<Button
compact
size="md"
size="compact-md"
tooltip={{ label: t('action.moveToTop', { postProcess: 'sentenceCase' }) }}
variant="default"
onClick={handleMoveToTop}
@ -142,8 +138,7 @@ export const PlayQueueListControls = ({ type, tableRef }: PlayQueueListOptionsPr
<RiArrowUpLine size="1.1rem" />
</Button>
<Button
compact
size="md"
size="compact-md"
tooltip={{
label: t('action.removeFromQueue', { postProcess: 'sentenceCase' }),
}}
@ -153,8 +148,7 @@ export const PlayQueueListControls = ({ type, tableRef }: PlayQueueListOptionsPr
<RiEraserLine size="1.1rem" />
</Button>
<Button
compact
size="md"
size="compact-md"
tooltip={{ label: t('action.clearQueue', { postProcess: 'sentenceCase' }) }}
variant="default"
onClick={handleClearQueue}
@ -169,8 +163,7 @@ export const PlayQueueListControls = ({ type, tableRef }: PlayQueueListOptionsPr
>
<Popover.Target>
<Button
compact
size="md"
size="compact-md"
tooltip={{
label: t('common.configure', { postProcess: 'sentenceCase' }),
}}

View File

@ -15,7 +15,7 @@ const NowPlayingRoute = () => {
<AnimatedPage>
<VirtualGridContainer>
<NowPlayingHeader />
<Paper sx={{ borderTop: '1px solid var(--generic-border-color)' }}>
<Paper style={{ borderTop: '1px solid var(--generic-border-color)' }}>
<PlayQueueListControls
tableRef={queueRef}
type="nowPlaying"

View File

@ -1,4 +1,3 @@
import { Flex, Stack, Group, Center } from '@mantine/core';
import { useSetState } from '@mantine/hooks';
import { AnimatePresence, HTMLMotionProps, motion, Variants } from 'framer-motion';
import { useEffect, useRef, useLayoutEffect, useState, useCallback, Fragment } from 'react';
@ -7,7 +6,7 @@ import { generatePath } from 'react-router';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
import { QueueSong } from '/@/renderer/api/types';
import { Badge, Text, TextTitle } from '/@/renderer/components';
import { Badge, Center, Flex, Group, Stack, Text, TextTitle } from '/@/renderer/components';
import { useFastAverageColor } from '/@/renderer/hooks';
import { AppRoute } from '/@/renderer/router/routes';
import {
@ -104,7 +103,7 @@ const ImageWithPlaceholder = ({
if (!props.src) {
return (
<Center
sx={{
style={{
background: 'var(--placeholder-bg)',
borderRadius: 'var(--card-default-radius)',
height: '100%',
@ -246,11 +245,10 @@ export const FullScreenPlayerImage = () => {
</ImageContainer>
<MetadataContainer
className="full-screen-player-image-metadata"
gap="xs"
maw="100%"
spacing="xs"
>
<TextTitle
align="center"
order={1}
overflow="hidden"
style={{
@ -263,7 +261,6 @@ export const FullScreenPlayerImage = () => {
</TextTitle>
<TextTitle
$link
align="center"
component={Link}
order={3}
overflow="hidden"
@ -280,7 +277,6 @@ export const FullScreenPlayerImage = () => {
</TextTitle>
<TextTitle
key="fs-artists"
align="center"
order={3}
style={{
textShadow: 'var(--fullscreen-player-text-shadow)',
@ -291,7 +287,7 @@ export const FullScreenPlayerImage = () => {
{index > 0 && (
<Text
$secondary
sx={{
style={{
display: 'inline-block',
padding: '0 0.5rem',
}}
@ -317,8 +313,8 @@ export const FullScreenPlayerImage = () => {
))}
</TextTitle>
<Group
justify="center"
mt="sm"
position="center"
>
{currentSong?.container && (
<Badge size="lg">

View File

@ -1,10 +1,9 @@
import { Group } from '@mantine/core';
import { motion } from 'framer-motion';
import { useTranslation } from 'react-i18next';
import { HiOutlineQueueList } from 'react-icons/hi2';
import { RiFileMusicLine, RiFileTextLine } from 'react-icons/ri';
import styled from 'styled-components';
import { Button } from '/@/renderer/components';
import { Button, Group } from '/@/renderer/components';
import { PlayQueue } from '/@/renderer/features/now-playing';
import {
useFullScreenPlayerStore,
@ -91,23 +90,23 @@ export const FullScreenPlayerQueue = () => {
<Group
grow
align="center"
position="center"
justify="center"
>
{headerItems.map((item) => (
<HeaderItemWrapper key={`tab-${item.label}`}>
<Button
fullWidth
uppercase
fw="600"
pos="relative"
size="lg"
sx={{
style={{
alignItems: 'center',
color: item.active
? 'var(--main-fg) !important'
: 'var(--main-fg-secondary) !important',
letterSpacing: '1px',
}}
tt="uppercase"
variant="subtle"
onClick={item.onClick}
>

View File

@ -1,5 +1,4 @@
import { useLayoutEffect, useRef } from 'react';
import { Divider, Group } from '@mantine/core';
import { useHotkeys } from '@mantine/hooks';
import { Variants, motion } from 'framer-motion';
import { useTranslation } from 'react-i18next';
@ -8,6 +7,8 @@ import { useLocation } from 'react-router';
import styled from 'styled-components';
import {
Button,
Divider,
Group,
NumberInput,
Option,
Popover,
@ -108,17 +109,16 @@ const Controls = () => {
return (
<Group
gap="sm"
p="1rem"
pos="absolute"
spacing="sm"
sx={{
style={{
left: 0,
top: 10,
}}
>
<Button
compact
size="sm"
size="compact-sm"
tooltip={{ label: t('common.minimize', { postProcess: 'titleCase' }) }}
variant="subtle"
onClick={handleToggleFullScreenPlayer}
@ -128,8 +128,7 @@ const Controls = () => {
<Popover position="bottom-start">
<Popover.Target>
<Button
compact
size="sm"
size="compact-sm"
tooltip={{ label: t('common.configure', { postProcess: 'titleCase' }) }}
variant="subtle"
>
@ -229,7 +228,7 @@ const Controls = () => {
/>
</Option.Control>
</Option>
<Divider my="sm" />
<Divider marginY="sm" />
<Option>
<Option.Label>
{t('page.fullscreenPlayer.config.followCurrentLyric', {
@ -283,8 +282,8 @@ const Controls = () => {
</Option.Label>
<Option.Control>
<Group
noWrap
w="100%"
wrap="nowrap"
>
<Slider
defaultValue={lyricConfig.fontSize}
@ -323,8 +322,8 @@ const Controls = () => {
</Option.Label>
<Option.Control>
<Group
noWrap
w="100%"
wrap="nowrap"
>
<Slider
defaultValue={lyricConfig.gap}
@ -393,7 +392,7 @@ const Controls = () => {
/>
</Option.Control>
</Option>
<Divider my="sm" />
<Divider marginY="sm" />
<TableConfigDropdown type="fullScreen" />
</Popover.Dropdown>
</Popover>

View File

@ -162,7 +162,7 @@ export const LeftControls = () => {
/>
) : (
<Center
sx={{
style={{
background: 'var(--placeholder-bg)',
height: '100%',
}}
@ -177,11 +177,10 @@ export const LeftControls = () => {
{!collapsed && (
<Button
compact
opacity={0.8}
radius={50}
size="md"
sx={{
size="compact-md"
style={{
cursor: 'default',
position: 'absolute',
right: 2,
@ -209,9 +208,9 @@ export const LeftControls = () => {
<MetadataStack layout="position">
<LineItem>
<Group
noWrap
align="flex-start"
spacing="xs"
gap="xs"
wrap="nowrap"
>
<Text
$link
@ -225,7 +224,7 @@ export const LeftControls = () => {
</Text>
{isSongDefined && (
<Button
compact
size="compact-md"
variant="subtle"
onClick={(e) => handleGeneralContextMenu(e, [currentSong!])}
>

View File

@ -1,5 +1,4 @@
import { useEffect } from 'react';
import { Flex, Group } from '@mantine/core';
import { useHotkeys, useMediaQuery } from '@mantine/hooks';
import isElectron from 'is-electron';
import { useTranslation } from 'react-i18next';
@ -26,7 +25,7 @@ import { useRightControls } from '../hooks/use-right-controls';
import { PlayerButton } from './player-button';
import { LibraryItem, QueueSong, ServerType, Song } from '/@/renderer/api/types';
import { useCreateFavorite, useDeleteFavorite, useSetRating } from '/@/renderer/features/shared';
import { DropdownMenu, Rating } from '/@/renderer/components';
import { DropdownMenu, Flex, Group, Rating } from '/@/renderer/components';
import { PlayerbarSlider } from '/@/renderer/features/player/components/playerbar-slider';
import { Slider } from '/@/renderer/components/slider';
@ -213,9 +212,9 @@ export const RightControls = () => {
)}
</Group>
<Group
noWrap
align="center"
spacing="xs"
gap="xs"
wrap="nowrap"
>
<DropdownMenu
withArrow
@ -272,7 +271,7 @@ export const RightControls = () => {
<RiHeartLine size="1.1rem" />
)
}
sx={{
style={{
svg: {
fill: !currentSong?.userFavorite
? undefined
@ -297,8 +296,8 @@ export const RightControls = () => {
/>
) : null}
<Group
noWrap
spacing="xs"
gap="xs"
wrap="nowrap"
>
<PlayerButton
icon={

View File

@ -129,7 +129,7 @@ export const ShuffleAllModal = ({
}, [musicFolders]);
return (
<Stack spacing="md">
<Stack gap="md">
<NumberInput
required
label="How many tracks?"
@ -188,7 +188,7 @@ export const ShuffleAllModal = ({
<Divider />
<Group grow>
<Button
leftIcon={<RiAddBoxFill size="1rem" />}
leftSection={<RiAddBoxFill size="1rem" />}
type="submit"
variant="default"
onClick={() => handlePlay(Play.LAST)}
@ -196,7 +196,7 @@ export const ShuffleAllModal = ({
Add
</Button>
<Button
leftIcon={<RiAddCircleFill size="1rem" />}
leftSection={<RiAddCircleFill size="1rem" />}
type="submit"
variant="default"
onClick={() => handlePlay(Play.NEXT)}
@ -205,7 +205,7 @@ export const ShuffleAllModal = ({
</Button>
</Group>
<Button
leftIcon={<RiPlayFill size="1rem" />}
leftSection={<RiPlayFill size="1rem" />}
type="submit"
variant="filled"
onClick={() => handlePlay(Play.NOW)}

View File

@ -229,7 +229,7 @@ export const AddToPlaylistContextModal = ({
})}
{...form.getInputProps('skipDuplicates', { type: 'checkbox' })}
/>
<Group position="right">
<Group align="flex-end">
<Group>
<Button
disabled={addToPlaylistMutation.isLoading}

View File

@ -144,7 +144,7 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
</Stack>
)}
<Group position="right">
<Group align="flex-end">
<Button
variant="subtle"
onClick={onCancel}

View File

@ -159,15 +159,15 @@ export const PlaylistDetailContent = ({ tableRef }: PlaylistDetailContentProps)
return (
<ContentContainer>
<Group
justify="space-between"
p="1rem"
position="apart"
>
<Group>
<PlayButton onClick={() => handlePlay()} />
<DropdownMenu position="bottom-start">
<DropdownMenu.Target>
<Button
compact
size="compact-md"
variant="subtle"
>
<RiMoreFill size={20} />
@ -199,10 +199,10 @@ export const PlaylistDetailContent = ({ tableRef }: PlaylistDetailContentProps)
</DropdownMenu.Dropdown>
</DropdownMenu>
<Button
compact
uppercase
component={Link}
size="compact-md"
to={generatePath(AppRoute.PLAYLISTS_DETAIL_SONGS, { playlistId })}
tt="uppercase"
variant="subtle"
>
View full playlist
@ -234,15 +234,15 @@ export const PlaylistDetailContent = ({ tableRef }: PlaylistDetailContentProps)
/>
</Box>
<MotionGroup
justify="center"
p="2rem"
position="center"
onViewportEnter={handleLoadMore}
>
<Button
ref={loadMoreRef}
compact
disabled={!playlistSongsQueryInfinite.hasNextPage}
loading={playlistSongsQueryInfinite.isFetchingNextPage}
size="compact-md"
variant="subtle"
onClick={handleLoadMore}
>

View File

@ -51,7 +51,7 @@ export const PlaylistDetailHeader = forwardRef(
title={detailQuery?.data?.name || ''}
>
<Stack>
<Group spacing="sm">
<Group gap="sm">
{metadataItems.map((item, index) => (
<Fragment key={`item-${item.id}-${index}`}>
{index > 0 && <Text $noSelect></Text>}

View File

@ -318,7 +318,7 @@ export const PlaylistDetailSongListHeaderFilters = ({
[page, setPage],
);
const handleTableColumns = (values: TableColumn[]) => {
const handleTableColumns = (values: TableColumn[] | any[]) => {
const existingColumns = page.table.columns;
if (values.length === 0) {
@ -394,15 +394,14 @@ export const PlaylistDetailSongListHeaderFilters = ({
<Flex justify="space-between">
<Group
ref={cq.ref}
spacing="sm"
gap="sm"
w="100%"
>
<DropdownMenu position="bottom-start">
<DropdownMenu.Target>
<Button
compact
fw="600"
size="md"
size="compact-md"
variant="subtle"
>
{sortByLabel}
@ -430,9 +429,8 @@ export const PlaylistDetailSongListHeaderFilters = ({
<DropdownMenu position="bottom-start">
<DropdownMenu.Target>
<Button
compact
fw="600"
size="md"
size="compact-md"
variant="subtle"
>
<RiMoreFill size="1.3rem" />
@ -440,26 +438,26 @@ export const PlaylistDetailSongListHeaderFilters = ({
</DropdownMenu.Target>
<DropdownMenu.Dropdown>
<DropdownMenu.Item
icon={<RiPlayFill />}
leftSection={<RiPlayFill />}
onClick={() => handlePlay(Play.NOW)}
>
{t('player.play', { postProcess: 'sentenceCase' })}
</DropdownMenu.Item>
<DropdownMenu.Item
icon={<RiAddBoxFill />}
leftSection={<RiAddBoxFill />}
onClick={() => handlePlay(Play.LAST)}
>
{t('player.addLast', { postProcess: 'sentenceCase' })}
</DropdownMenu.Item>
<DropdownMenu.Item
icon={<RiAddCircleFill />}
leftSection={<RiAddCircleFill />}
onClick={() => handlePlay(Play.NEXT)}
>
{t('player.addNext', { postProcess: 'sentenceCase' })}
</DropdownMenu.Item>
<DropdownMenu.Divider />
<DropdownMenu.Item
icon={<RiEditFill />}
leftSection={<RiEditFill />}
onClick={() =>
openUpdatePlaylistModal({
playlist: detailQuery.data!,
@ -470,14 +468,14 @@ export const PlaylistDetailSongListHeaderFilters = ({
{t('action.editPlaylist', { postProcess: 'sentenceCase' })}
</DropdownMenu.Item>
<DropdownMenu.Item
icon={<RiDeleteBinFill />}
leftSection={<RiDeleteBinFill />}
onClick={openDeletePlaylistModal}
>
{t('action.deletePlaylist', { postProcess: 'sentenceCase' })}
</DropdownMenu.Item>
<DropdownMenu.Divider />
<DropdownMenu.Item
icon={<RiRefreshLine />}
leftSection={<RiRefreshLine />}
onClick={handleRefresh}
>
{t('action.refresh', { postProcess: 'sentenceCase' })}
@ -505,8 +503,7 @@ export const PlaylistDetailSongListHeaderFilters = ({
>
<DropdownMenu.Target>
<Button
compact
size="md"
size="compact-md"
variant="subtle"
>
<RiSettings3Fill size="1.3rem" />
@ -548,7 +545,7 @@ export const PlaylistDetailSongListHeaderFilters = ({
<DropdownMenu.Item
closeMenuOnClick={false}
component="div"
sx={{ cursor: 'default' }}
style={{ cursor: 'default' }}
>
<Stack>
<MultiSelect
@ -560,7 +557,7 @@ export const PlaylistDetailSongListHeaderFilters = ({
width={300}
onChange={handleTableColumns}
/>
<Group position="apart">
<Group justify="space-between">
<Text>Auto Fit Columns</Text>
<Switch
defaultChecked={page.table.autoFit}

View File

@ -43,7 +43,7 @@ export const PlaylistDetailSongListHeader = ({
const isSmartPlaylist = detailQuery?.data?.rules;
return (
<Stack spacing={0}>
<Stack gap={0}>
<PageHeader backgroundColor="var(--titlebar-bg)">
<LibraryHeaderBar>
<LibraryHeaderBar.PlayButton onClick={() => handlePlay(playButtonBehavior)} />

View File

@ -235,7 +235,7 @@ export const PlaylistListHeaderFilters = ({
[pageKey, setDisplayType],
);
const handleTableColumns = (values: TableColumn[]) => {
const handleTableColumns = (values: TableColumn[] | any[]) => {
const existingColumns = table.columns;
if (values.length === 0) {
@ -288,15 +288,14 @@ export const PlaylistListHeaderFilters = ({
<Flex justify="space-between">
<Group
ref={cq.ref}
spacing="sm"
gap="sm"
w="100%"
>
<DropdownMenu position="bottom-start">
<DropdownMenu.Target>
<Button
compact
fw="600"
size="md"
size="compact-md"
variant="subtle"
>
{sortByLabel}
@ -322,8 +321,7 @@ export const PlaylistListHeaderFilters = ({
/>
<Divider orientation="vertical" />
<Button
compact
size="md"
size="compact-md"
tooltip={{ label: t('common.refresh', { postProcess: 'titleCase' }) }}
variant="subtle"
onClick={handleRefresh}
@ -334,9 +332,8 @@ export const PlaylistListHeaderFilters = ({
<DropdownMenu position="bottom-start">
<DropdownMenu.Target>
<Button
compact
fw="600"
size="md"
size="compact-md"
variant="subtle"
>
<RiMoreFill size="1.3rem" />
@ -344,7 +341,7 @@ export const PlaylistListHeaderFilters = ({
</DropdownMenu.Target>
<DropdownMenu.Dropdown>
<DropdownMenu.Item
icon={<RiRefreshLine />}
leftSection={<RiRefreshLine />}
onClick={handleRefresh}
>
{t('common.refresh', { postProcess: 'titleCase' })}
@ -359,8 +356,7 @@ export const PlaylistListHeaderFilters = ({
>
<DropdownMenu.Target>
<Button
compact
size="md"
size="compact-md"
variant="subtle"
>
<RiSettings3Fill size="1.3rem" />
@ -437,7 +433,7 @@ export const PlaylistListHeaderFilters = ({
<DropdownMenu.Item
closeMenuOnClick={false}
component="div"
sx={{ cursor: 'default' }}
style={{ cursor: 'default' }}
>
<Stack>
<MultiSelect
@ -449,7 +445,7 @@ export const PlaylistListHeaderFilters = ({
width={300}
onChange={handleTableColumns}
/>
<Group position="apart">
<Group justify="space-between">
<Text>
{t('table.config.general.autoFitColumns', {
postProcess: 'titleCase',

View File

@ -52,7 +52,7 @@ export const PlaylistListHeader = ({ itemCount, tableRef, gridRef }: PlaylistLis
return (
<Stack
ref={cq.ref}
spacing={0}
gap={0}
>
<PageHeader backgroundColor="var(--titlebar-bg)">
<Flex

View File

@ -411,9 +411,11 @@ export const PlaylistQueryBuilder = forwardRef(
return (
<MotionFlex
direction="column"
h="calc(100% - 3.5rem)"
justify="space-between"
style={{
flexDirection: 'column',
height: 'calc(100% - 3.5rem)',
justifyContent: 'space-between',
}}
>
<ScrollArea
h="100%"
@ -446,15 +448,15 @@ export const PlaylistQueryBuilder = forwardRef(
/>
</ScrollArea>
<Group
noWrap
align="flex-end"
justify="space-between"
m="1rem"
position="apart"
wrap="nowrap"
>
<Group
noWrap
spacing="sm"
gap="sm"
w="100%"
wrap="nowrap"
>
<Select
searchable
@ -489,8 +491,8 @@ export const PlaylistQueryBuilder = forwardRef(
</Group>
{onSave && onSaveAs && (
<Group
noWrap
spacing="sm"
gap="sm"
wrap="nowrap"
>
<Button
loading={isSaving}
@ -519,7 +521,7 @@ export const PlaylistQueryBuilder = forwardRef(
<DropdownMenu.Dropdown>
<DropdownMenu.Item
$danger
icon={<RiSaveLine color="var(--danger-color)" />}
leftSection={<RiSaveLine color="var(--danger-color)" />}
onClick={handleSave}
>
{t('common.saveAndReplace', { postProcess: 'titleCase' })}

View File

@ -89,7 +89,7 @@ export const SaveAsPlaylistForm = ({
{...form.getInputProps('_custom.navidrome.public', { type: 'checkbox' })}
/>
)}
<Group position="right">
<Group justify="flex-end">
<Button
variant="subtle"
onClick={onCancel}

View File

@ -117,7 +117,7 @@ export const UpdatePlaylistForm = ({ users, query, body, onCancel }: UpdatePlayl
{...form.getInputProps('_custom.navidrome.public', { type: 'checkbox' })}
/>
)}
<Group position="right">
<Group justify="flex-end">
<Button
variant="subtle"
onClick={onCancel}

View File

@ -179,7 +179,7 @@ const PlaylistDetailSongListRoute = () => {
>
<Group p="1rem">
<Button
compact
size="compact-md"
variant="default"
onClick={handleToggleExpand}
>

View File

@ -93,15 +93,15 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
size="lg"
>
<Group
gap="sm"
mb="1rem"
spacing="sm"
>
{pages.map((page, index) => (
<Fragment key={page}>
{index > 0 && ' > '}
<Button
compact
disabled
size="compact-md"
variant="default"
>
{page?.toLocaleUpperCase()}
@ -122,7 +122,7 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
<TextInput
ref={searchInputRef}
data-autofocus
icon={<RiSearchLine />}
leftSection={<RiSearchLine />}
rightSection={
<ActionIcon
onClick={() => {
@ -261,11 +261,11 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
mt="0.5rem"
p="0.5rem"
>
<Group position="apart">
<Group justify="space-between">
<Command.Loading>
{isHome && isLoading && query !== '' && <Spinner />}
</Command.Loading>
<Group spacing="sm">
<Group gap="sm">
<Kbd size="md">ESC</Kbd>
<Kbd size="md"></Kbd>
<Kbd size="md"></Kbd>

Some files were not shown because too many files have changed in this diff Show More