Migrate to Mantine v8 and Design Changes (#961)

* mantine v8 migration

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

View file

@ -12,7 +12,9 @@ export const ImageButton = () => {
mr={5}
onClick={() => toggleImage()}
size="xl"
tooltip={showImage ? 'Hide Image' : 'Show Image'}
tooltip={{
label: showImage ? 'Hide Image' : 'Show Image',
}}
variant="default"
>
{showImage ? <CiImageOff size={30} /> : <CiImageOn size={30} />}

View file

@ -9,11 +9,13 @@ export const ReconnectButton = () => {
return (
<RemoteButton
$active={!connected}
isActive={!connected}
mr={5}
onClick={() => reconnect()}
size="xl"
tooltip={connected ? 'Reconnect' : 'Not connected. Reconnect.'}
tooltip={{
label: connected ? 'Reconnect' : 'Not connected. Reconnect.',
}}
variant="default"
>
<RiRestartLine size={30} />

View file

@ -0,0 +1,24 @@
.button {
svg {
display: flex;
fill: var(--theme-colors-foreground);
}
&:hover {
svg {
fill: var(--theme-colors-foreground);
}
}
}
.button.active {
svg {
fill: var(--primary-color);
}
&:hover {
svg {
fill: var(--primary-color) !important;
}
}
}

View file

@ -1,53 +1,29 @@
import { Button, type ButtonProps as MantineButtonProps, Tooltip } from '@mantine/core';
import { forwardRef, MouseEvent, ReactNode, Ref } from 'react';
import styled from 'styled-components';
import clsx from 'clsx';
import { forwardRef, ReactNode, Ref } from 'react';
export interface ButtonProps extends StyledButtonProps {
tooltip: string;
}
import styles from './remote-button.module.css';
interface StyledButtonProps extends MantineButtonProps {
$active?: boolean;
import { Button, ButtonProps } from '/@/shared/components/button/button';
interface RemoteButtonProps extends ButtonProps {
children: ReactNode;
onClick?: (e: MouseEvent<HTMLButtonElement, MouseEvent>) => void;
onMouseDown?: (e: MouseEvent<HTMLButtonElement, MouseEvent>) => void;
isActive?: boolean;
ref: Ref<HTMLButtonElement>;
}
const StyledButton = styled(Button)<StyledButtonProps>`
svg {
display: flex;
fill: ${({ $active: active }) =>
active ? 'var(--primary-color)' : 'var(--playerbar-btn-fg)'};
stroke: var(--playerbar-btn-fg);
}
&:hover {
background: var(--playerbar-btn-bg-hover);
svg {
fill: ${({ $active: active }) =>
active
? 'var(--primary-color) !important'
: 'var(--playerbar-btn-fg-hover) !important'};
}
}
`;
export const RemoteButton = forwardRef<HTMLButtonElement, any>(
({ children, tooltip, ...props }: any, ref) => {
export const RemoteButton = forwardRef<HTMLButtonElement, RemoteButtonProps>(
({ children, isActive, tooltip, ...props }, ref) => {
return (
<Tooltip
label={tooltip}
withinPortal
<Button
className={clsx(styles.button, {
[styles.active]: isActive,
})}
tooltip={tooltip}
{...props}
ref={ref}
>
<StyledButton
{...props}
ref={ref}
>
{children}
</StyledButton>
</Tooltip>
{children}
</Button>
);
},
);

View file

@ -3,7 +3,7 @@ import { RiMoonLine, RiSunLine } from 'react-icons/ri';
import { RemoteButton } from '/@/remote/components/buttons/remote-button';
import { useIsDark, useToggleDark } from '/@/remote/store';
import { AppTheme } from '/@/shared/types/domain-types';
import { AppTheme } from '/@/shared/themes/app-theme-types';
export const ThemeButton = () => {
const isDark = useIsDark();
@ -19,7 +19,9 @@ export const ThemeButton = () => {
mr={5}
onClick={() => toggleDark()}
size="xl"
tooltip="Toggle Theme"
tooltip={{
label: 'Toggle Theme',
}}
variant="default"
>
{isDark ? <RiSunLine size={30} /> : <RiMoonLine size={30} />}

View file

@ -1,4 +1,4 @@
import { Group, Image, Rating, Text, Title, Tooltip } from '@mantine/core';
import { Image, Title } from '@mantine/core';
import formatDuration from 'format-duration';
import debounce from 'lodash/debounce';
import { useCallback } from 'react';
@ -17,6 +17,10 @@ import {
import { RemoteButton } from '/@/remote/components/buttons/remote-button';
import { WrapperSlider } from '/@/remote/components/wrapped-slider';
import { useInfo, useSend, useShowImage } from '/@/remote/store';
import { Group } from '/@/shared/components/group/group';
import { Rating } from '/@/shared/components/rating/rating';
import { Text } from '/@/shared/components/text/text';
import { Tooltip } from '/@/shared/components/tooltip/tooltip';
import { PlayerRepeat, PlayerStatus } from '/@/shared/types/types';
export const RemoteContainer = () => {
@ -44,7 +48,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}>
@ -56,13 +60,15 @@ export const RemoteContainer = () => {
</>
)}
<Group
gap={0}
grow
spacing={0}
>
<RemoteButton
disabled={!id}
onClick={() => send({ event: 'previous' })}
tooltip="Previous track"
tooltip={{
label: 'Previous track',
}}
variant="default"
>
<RiSkipBackFill size={25} />
@ -76,7 +82,9 @@ export const RemoteContainer = () => {
send({ event: 'play' });
}
}}
tooltip={id && status === PlayerStatus.PLAYING ? 'Pause' : 'Play'}
tooltip={{
label: id && status === PlayerStatus.PLAYING ? 'Pause' : 'Play',
}}
variant="default"
>
{id && status === PlayerStatus.PLAYING ? (
@ -88,34 +96,40 @@ export const RemoteContainer = () => {
<RemoteButton
disabled={!id}
onClick={() => send({ event: 'next' })}
tooltip="Next track"
tooltip={{
label: 'Next track',
}}
variant="default"
>
<RiSkipForwardFill size={25} />
</RemoteButton>
</Group>
<Group
gap={0}
grow
spacing={0}
>
<RemoteButton
$active={shuffle || false}
isActive={shuffle || false}
onClick={() => send({ event: 'shuffle' })}
tooltip={shuffle ? 'Shuffle tracks' : 'Shuffle disabled'}
tooltip={{
label: shuffle ? 'Shuffle tracks' : 'Shuffle disabled',
}}
variant="default"
>
<RiShuffleFill size={25} />
</RemoteButton>
<RemoteButton
$active={repeat !== undefined && repeat !== PlayerRepeat.NONE}
isActive={repeat !== undefined && repeat !== PlayerRepeat.NONE}
onClick={() => send({ event: 'repeat' })}
tooltip={`Repeat ${
repeat === PlayerRepeat.ONE
? 'One'
: repeat === PlayerRepeat.ALL
? 'all'
: 'none'
}`}
tooltip={{
label: `Repeat ${
repeat === PlayerRepeat.ONE
? 'One'
: repeat === PlayerRepeat.ALL
? 'all'
: 'none'
}`,
}}
variant="default"
>
{repeat === undefined || repeat === PlayerRepeat.ONE ? (
@ -125,14 +139,16 @@ export const RemoteContainer = () => {
)}
</RemoteButton>
<RemoteButton
$active={song?.userFavorite}
disabled={!id}
isActive={song?.userFavorite}
onClick={() => {
if (!id) return;
send({ event: 'favorite', favorite: !song.userFavorite, id });
}}
tooltip={song?.userFavorite ? 'Unfavorite' : 'Favorite'}
tooltip={{
label: song?.userFavorite ? 'Unfavorite' : 'Favorite',
}}
variant="default"
>
<RiHeartLine size={25} />
@ -146,7 +162,7 @@ export const RemoteContainer = () => {
<Rating
onChange={debouncedSetRating}
onDoubleClick={() => debouncedSetRating(0)}
sx={{ margin: 'auto' }}
style={{ margin: 'auto' }}
value={song.userRating ?? 0}
/>
</Tooltip>
@ -169,8 +185,8 @@ export const RemoteContainer = () => {
onChangeEnd={(e) => send({ event: 'volume', volume: e })}
rightLabel={
<Text
fw={600}
size="xs"
weight={600}
>
{volume ?? 0}
</Text>

View file

@ -1,14 +1,4 @@
import {
AppShell,
Container,
Flex,
Grid,
Header,
Image,
MediaQuery,
Skeleton,
Title,
} from '@mantine/core';
import { AppShell, Container, Flex, Grid, Image, Skeleton, Title } from '@mantine/core';
import { ImageButton } from '/@/remote/components/buttons/image-button';
import { ReconnectButton } from '/@/remote/components/buttons/reconnect-button';
@ -20,47 +10,35 @@ 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>
<AppShell padding="md">
<AppShell.Header>
<Grid>
<Grid.Col span="auto">
<div>
<Image
fit="contain"
height={60}
src="/favicon.ico"
width={60}
/>
</div>
</Grid.Col>
<Grid.Col hiddenFrom="md">
<Title ta="center">Feishin Remote</Title>
</Grid.Col>
<Grid.Col span="auto">
<Flex
direction="row"
justify="right"
>
<ReconnectButton />
<ImageButton />
<ThemeButton />
</Flex>
</Grid.Col>
</Grid>
</Header>
}
padding="md"
>
<Grid.Col span="auto">
<Flex
direction="row"
justify="right"
>
<ReconnectButton />
<ImageButton />
<ThemeButton />
</Flex>
</Grid.Col>
</Grid>
</AppShell.Header>
<Container>
{connected ? (
<RemoteContainer />

View file

@ -0,0 +1,21 @@
.container {
display: flex;
width: 95%;
height: 20px;
margin: 10px 0;
}
.wrapper {
display: flex;
flex: 6;
align-items: center;
height: 100%;
}
.value-wrapper {
display: flex;
flex: 1;
align-self: flex-end;
justify-content: center;
max-width: 50px;
}

View file

@ -1,40 +1,16 @@
import { rem, Slider, SliderProps } from '@mantine/core';
import { ReactNode, useState } from 'react';
import styled from 'styled-components';
const SliderContainer = styled.div`
display: flex;
width: 95%;
height: 20px;
margin: 10px 0;
`;
const SliderValueWrapper = styled.div<{ $position: 'left' | 'right' }>`
display: flex;
flex: 1;
align-self: flex-end;
justify-content: center;
max-width: 50px;
`;
const SliderWrapper = styled.div`
display: flex;
flex: 6;
align-items: center;
height: 100%;
`;
import styles from './wrapped-slider.module.css';
const PlayerbarSlider = ({ ...props }: SliderProps) => {
return (
<Slider
styles={{
bar: {
backgroundColor: 'var(--playerbar-slider-track-progress-bg)',
transition: 'background-color 0.2s ease',
},
label: {
backgroundColor: 'var(--tooltip-bg)',
color: 'var(--tooltip-fg)',
fontSize: '1.1rem',
fontWeight: 600,
padding: '0 1rem',
@ -59,7 +35,6 @@ const PlayerbarSlider = ({ ...props }: SliderProps) => {
},
track: {
'&::before': {
backgroundColor: 'var(--playerbar-slider-track-bg)',
right: 'calc(0.1rem * -1)',
},
},
@ -84,9 +59,9 @@ export const WrapperSlider = ({ leftLabel, rightLabel, value, ...props }: Wrappe
const [seek, setSeek] = useState(0);
return (
<SliderContainer>
{leftLabel && <SliderValueWrapper $position="left">{leftLabel}</SliderValueWrapper>}
<SliderWrapper>
<div className={styles.container}>
{leftLabel && <div className={styles.valueWrapper}>{leftLabel}</div>}
<div className={styles.wrapper}>
<PlayerbarSlider
{...props}
min={0}
@ -102,8 +77,8 @@ export const WrapperSlider = ({ leftLabel, rightLabel, value, ...props }: Wrappe
value={!isSeeking ? (value ?? 0) : seek}
w="100%"
/>
</SliderWrapper>
{rightLabel && <SliderValueWrapper $position="right">{rightLabel}</SliderValueWrapper>}
</SliderContainer>
</div>
{rightLabel && <div className={styles.valueWrapper}>{rightLabel}</div>}
</div>
);
};