fix and update remote design

This commit is contained in:
jeffvli 2025-06-24 14:36:14 -07:00
parent ad533a1d9c
commit 6689e84f67
24 changed files with 326 additions and 453 deletions

View file

@ -25,6 +25,7 @@
"build:remote": "vite build --config remote.vite.config.ts", "build:remote": "vite build --config remote.vite.config.ts",
"build:web": "vite build --config web.vite.config.ts", "build:web": "vite build --config web.vite.config.ts",
"dev": "electron-vite dev", "dev": "electron-vite dev",
"dev:remote": "vite dev --config remote.vite.config.ts",
"dev:watch": "electron-vite dev --watch", "dev:watch": "electron-vite dev --watch",
"i18next": "i18next -c src/i18n/i18next-parser.config.js", "i18next": "i18next -c src/i18n/i18next-parser.config.js",
"postinstall": "electron-builder install-app-deps", "postinstall": "electron-builder install-app-deps",

View file

@ -25,6 +25,12 @@ export default defineConfig({
}, },
sourcemap: true, sourcemap: true,
}, },
css: {
modules: {
generateScopedName: 'fs-[name]-[local]',
localsConvention: 'camelCase',
},
},
plugins: [ plugins: [
react(), react(),
ViteEjsPlugin({ ViteEjsPlugin({

View file

@ -1,10 +1,15 @@
import { MantineProvider } from '@mantine/core'; import { MantineProvider } from '@mantine/core';
import { useEffect } from 'react'; import '@mantine/core/styles.css';
import '@mantine/notifications/styles.css';
import './styles/global.css'; import '/@/shared/styles/global.css';
import { useEffect } from 'react';
import { Shell } from '/@/remote/components/shell'; import { Shell } from '/@/remote/components/shell';
import { useIsDark, useReconnect } from '/@/remote/store'; import { useIsDark, useReconnect } from '/@/remote/store';
import { useAppTheme } from '/@/renderer/themes/use-app-theme';
import { AppTheme } from '/@/shared/themes/app-theme-types';
export const App = () => { export const App = () => {
const isDark = useIsDark(); const isDark = useIsDark();
@ -14,58 +19,12 @@ export const App = () => {
reconnect(); reconnect();
}, [reconnect]); }, [reconnect]);
const { mode, theme } = useAppTheme(isDark ? AppTheme.DEFAULT_DARK : AppTheme.DEFAULT_LIGHT);
return ( return (
<MantineProvider <MantineProvider
defaultColorScheme={isDark ? 'dark' : 'light'} defaultColorScheme={mode}
theme={{ theme={theme}
components: {
AppShell: {
styles: {
body: {
height: '100vh',
overflow: 'scroll',
},
},
},
Modal: {
styles: {
body: {
background: 'var(--theme-modal-bg)',
height: '100vh',
},
close: { marginRight: '0.5rem' },
content: { borderRadius: '5px' },
header: {
background: 'var(--theme-modal-header-bg)',
paddingBottom: '1rem',
},
title: { fontSize: 'medium', fontWeight: 500 },
},
},
},
defaultRadius: 'xs',
focusRing: 'auto',
fontFamily: 'var(--theme-content-font-family)',
fontSizes: {
lg: '1.1rem',
md: '1rem',
sm: '0.9rem',
xl: '1.5rem',
xs: '0.8rem',
},
headings: {
fontFamily: 'var(--theme-content-font-family)',
fontWeight: '700',
},
other: {},
spacing: {
lg: '2rem',
md: '1rem',
sm: '0.5rem',
xl: '4rem',
xs: '0rem',
},
}}
> >
<Shell /> <Shell />
</MantineProvider> </MantineProvider>

View file

@ -1,23 +1,21 @@
import { CiImageOff, CiImageOn } from 'react-icons/ci'; import { CiImageOff, CiImageOn } from 'react-icons/ci';
import { RemoteButton } from '/@/remote/components/buttons/remote-button';
import { useShowImage, useToggleShowImage } from '/@/remote/store'; import { useShowImage, useToggleShowImage } from '/@/remote/store';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
export const ImageButton = () => { export const ImageButton = () => {
const showImage = useShowImage(); const showImage = useShowImage();
const toggleImage = useToggleShowImage(); const toggleImage = useToggleShowImage();
return ( return (
<RemoteButton <ActionIcon
mr={5}
onClick={() => toggleImage()} onClick={() => toggleImage()}
size="xl"
tooltip={{ tooltip={{
label: showImage ? 'Hide Image' : 'Show Image', label: showImage ? 'Hide Image' : 'Show Image',
}} }}
variant="default" variant="default"
> >
{showImage ? <CiImageOff size={30} /> : <CiImageOn size={30} />} {showImage ? <CiImageOff size={30} /> : <CiImageOn size={30} />}
</RemoteButton> </ActionIcon>
); );
}; };

View file

@ -1,24 +1,24 @@
import { RiRestartLine } from 'react-icons/ri'; import { RiRestartLine } from 'react-icons/ri';
import { RemoteButton } from '/@/remote/components/buttons/remote-button';
import { useConnected, useReconnect } from '/@/remote/store'; import { useConnected, useReconnect } from '/@/remote/store';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
export const ReconnectButton = () => { export const ReconnectButton = () => {
const connected = useConnected(); const connected = useConnected();
const reconnect = useReconnect(); const reconnect = useReconnect();
return ( return (
<RemoteButton <ActionIcon
isActive={!connected}
mr={5}
onClick={() => reconnect()} onClick={() => reconnect()}
size="xl"
tooltip={{ tooltip={{
label: connected ? 'Reconnect' : 'Not connected. Reconnect.', label: connected ? 'Reconnect' : 'Not connected. Reconnect.',
}} }}
variant="default" variant="default"
> >
<RiRestartLine size={30} /> <RiRestartLine
</RemoteButton> color={connected ? 'var(--theme-colors-primary)' : 'var(--theme-colors-foreground)'}
size={30}
/>
</ActionIcon>
); );
}; };

View file

@ -1,24 +0,0 @@
.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,29 +0,0 @@
import clsx from 'clsx';
import { forwardRef, ReactNode, Ref } from 'react';
import styles from './remote-button.module.css';
import { Button, ButtonProps } from '/@/shared/components/button/button';
interface RemoteButtonProps extends ButtonProps {
children: ReactNode;
isActive?: boolean;
ref: Ref<HTMLButtonElement>;
}
export const RemoteButton = forwardRef<HTMLButtonElement, RemoteButtonProps>(
({ children, isActive, tooltip, ...props }, ref) => {
return (
<Button
className={clsx(styles.button, {
[styles.active]: isActive,
})}
tooltip={tooltip}
{...props}
ref={ref}
>
{children}
</Button>
);
},
);

View file

@ -1,30 +1,34 @@
import { useEffect } from 'react';
import { RiMoonLine, RiSunLine } from 'react-icons/ri';
import { RemoteButton } from '/@/remote/components/buttons/remote-button';
import { useIsDark, useToggleDark } from '/@/remote/store'; import { useIsDark, useToggleDark } from '/@/remote/store';
import { AppTheme } from '/@/shared/themes/app-theme-types'; import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { Icon } from '/@/shared/components/icon/icon';
export const ThemeButton = () => { export const ThemeButton = () => {
const isDark = useIsDark(); const isDark = useIsDark();
const toggleDark = useToggleDark(); const toggleDark = useToggleDark();
useEffect(() => { const handleToggleTheme = () => {
const targetTheme: AppTheme = isDark ? AppTheme.DEFAULT_DARK : AppTheme.DEFAULT_LIGHT; toggleDark();
document.body.setAttribute('data-theme', targetTheme); };
}, [isDark]);
return ( return (
<RemoteButton <ActionIcon
mr={5} onClick={handleToggleTheme}
onClick={() => toggleDark()}
size="xl"
tooltip={{ tooltip={{
label: 'Toggle Theme', label: 'Toggle Theme',
}} }}
variant="default" variant="default"
> >
{isDark ? <RiSunLine size={30} /> : <RiMoonLine size={30} />} {isDark ? (
</RemoteButton> <Icon
icon="themeLight"
size={30}
/>
) : (
<Icon
icon="themeDark"
size={30}
/>
)}
</ActionIcon>
); );
}; };

View file

@ -0,0 +1,7 @@
.container {
width: 100%;
height: 40vh;
aspect-ratio: 1/1;
object-fit: var(--theme-image-fit);
border-radius: var(--theme-radius-md);
}

View file

@ -0,0 +1,18 @@
import styles from './player-image.module.css';
import { useSend } from '/@/remote/store';
interface PlayerImageProps {
src?: null | string;
}
export const PlayerImage = ({ src }: PlayerImageProps) => {
const send = useSend();
return (
<img
className={styles.container}
onError={() => send({ event: 'proxy' })}
src={src?.replaceAll(/&(size|width|height=\d+)/g, '')}
/>
);
};

View file

@ -1,24 +1,16 @@
import { Image, Title } from '@mantine/core';
import formatDuration from 'format-duration'; import formatDuration from 'format-duration';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { import { RiPauseFill, RiPlayFill, RiVolumeUpFill } from 'react-icons/ri';
RiHeartLine,
RiPauseFill,
RiPlayFill,
RiRepeat2Line,
RiRepeatOneLine,
RiShuffleFill,
RiSkipBackFill,
RiSkipForwardFill,
RiVolumeUpFill,
} from 'react-icons/ri';
import { RemoteButton } from '/@/remote/components/buttons/remote-button'; import { PlayerImage } from '/@/remote/components/player-image';
import { WrapperSlider } from '/@/remote/components/wrapped-slider'; import { WrappedSlider } from '/@/remote/components/wrapped-slider';
import { useInfo, useSend, useShowImage } from '/@/remote/store'; import { useInfo, useSend, useShowImage } from '/@/remote/store';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { Flex } from '/@/shared/components/flex/flex';
import { Group } from '/@/shared/components/group/group'; import { Group } from '/@/shared/components/group/group';
import { Rating } from '/@/shared/components/rating/rating'; import { Rating } from '/@/shared/components/rating/rating';
import { Stack } from '/@/shared/components/stack/stack';
import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
import { Tooltip } from '/@/shared/components/tooltip/tooltip'; import { Tooltip } from '/@/shared/components/tooltip/tooltip';
import { PlayerRepeat, PlayerStatus } from '/@/shared/types/types'; import { PlayerRepeat, PlayerStatus } from '/@/shared/types/types';
@ -40,40 +32,115 @@ export const RemoteContainer = () => {
const debouncedSetRating = debounce(setRating, 400); const debouncedSetRating = debounce(setRating, 400);
return ( return (
<> <Stack
gap="md"
h="100dvh"
w="100%"
>
{showImage && (
<Flex
align="center"
justify="center"
w="100%"
>
<PlayerImage src={song?.imageUrl} />
</Flex>
)}
{id && ( {id && (
<> <Stack gap="xs">
<Title order={1}>{song.name}</Title> <Text
<Group align="flex-end"> fw={700}
<Title order={2}>Album: {song.album}</Title> size="xl"
<Title order={2}>Artist: {song.artistName}</Title> style={{
</Group> overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{song.name}
</Text>
<Text
isMuted
style={{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{song.album}
</Text>
<Text
isMuted
style={{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{song.artistName}
</Text>
<Group justify="space-between"> <Group justify="space-between">
<Title order={3}>Duration: {formatDuration(song.duration)}</Title>
{song.releaseDate && ( {song.releaseDate && (
<Title order={3}> <Text isMuted>{new Date(song.releaseDate).toLocaleDateString()}</Text>
Released: {new Date(song.releaseDate).toLocaleDateString()}
</Title>
)} )}
<Title order={3}>Plays: {song.playCount}</Title> <Text isMuted>Plays: {song.playCount}</Text>
</Group> </Group>
</> </Stack>
)} )}
<Group <Group
gap={0} gap={0}
grow grow
> >
<RemoteButton <ActionIcon
disabled={!id} disabled={!id}
icon="favorite"
iconProps={{
fill: song?.userFavorite ? 'primary' : 'default',
}}
onClick={() => {
if (!id) return;
send({ event: 'favorite', favorite: !song.userFavorite, id });
}}
tooltip={{
label: song?.userFavorite ? 'Unfavorite' : 'Favorite',
}}
variant="transparent"
/>
{(song?.serverType === 'navidrome' || song?.serverType === 'subsonic') && (
<div style={{ margin: 'auto' }}>
<Tooltip
label="Double click to clear"
openDelay={1000}
>
<Rating
onChange={debouncedSetRating}
onDoubleClick={() => debouncedSetRating(0)}
style={{ margin: 'auto' }}
value={song.userRating ?? 0}
/>
</Tooltip>
</div>
)}
</Group>
<Group
gap="xs"
grow
>
<ActionIcon
disabled={!id}
icon="mediaPrevious"
iconProps={{
fill: 'default',
size: 'lg',
}}
onClick={() => send({ event: 'previous' })} onClick={() => send({ event: 'previous' })}
tooltip={{ tooltip={{
label: 'Previous track', label: 'Previous track',
}} }}
variant="default" variant="default"
> />
<RiSkipBackFill size={25} /> <ActionIcon
</RemoteButton>
<RemoteButton
disabled={!id} disabled={!id}
onClick={() => { onClick={() => {
if (status === PlayerStatus.PLAYING) { if (status === PlayerStatus.PLAYING) {
@ -92,34 +159,50 @@ export const RemoteContainer = () => {
) : ( ) : (
<RiPlayFill size={25} /> <RiPlayFill size={25} />
)} )}
</RemoteButton> </ActionIcon>
<RemoteButton <ActionIcon
disabled={!id} disabled={!id}
icon="mediaNext"
iconProps={{
fill: 'default',
size: 'lg',
}}
onClick={() => send({ event: 'next' })} onClick={() => send({ event: 'next' })}
tooltip={{ tooltip={{
label: 'Next track', label: 'Next track',
}} }}
variant="default" variant="default"
> />
<RiSkipForwardFill size={25} />
</RemoteButton>
</Group> </Group>
<Group <Group
gap={0} gap="xs"
grow grow
> >
<RemoteButton <ActionIcon
isActive={shuffle || false} icon="mediaShuffle"
iconProps={{
fill: shuffle ? 'primary' : 'default',
size: 'lg',
}}
onClick={() => send({ event: 'shuffle' })} onClick={() => send({ event: 'shuffle' })}
tooltip={{ tooltip={{
label: shuffle ? 'Shuffle tracks' : 'Shuffle disabled', label: shuffle ? 'Shuffle tracks' : 'Shuffle disabled',
}} }}
variant="default" variant="default"
> />
<RiShuffleFill size={25} /> <ActionIcon
</RemoteButton> icon={
<RemoteButton repeat === undefined || repeat === PlayerRepeat.ONE
isActive={repeat !== undefined && repeat !== PlayerRepeat.NONE} ? 'mediaRepeatOne'
: 'mediaRepeat'
}
iconProps={{
fill:
repeat !== undefined && repeat !== PlayerRepeat.NONE
? 'primary'
: 'default',
size: 'lg',
}}
onClick={() => send({ event: 'repeat' })} onClick={() => send({ event: 'repeat' })}
tooltip={{ tooltip={{
label: `Repeat ${ label: `Repeat ${
@ -131,74 +214,34 @@ export const RemoteContainer = () => {
}`, }`,
}} }}
variant="default" variant="default"
> />
{repeat === undefined || repeat === PlayerRepeat.ONE ? (
<RiRepeatOneLine size={25} />
) : (
<RiRepeat2Line size={25} />
)}
</RemoteButton>
<RemoteButton
disabled={!id}
isActive={song?.userFavorite}
onClick={() => {
if (!id) return;
send({ event: 'favorite', favorite: !song.userFavorite, id });
}}
tooltip={{
label: song?.userFavorite ? 'Unfavorite' : 'Favorite',
}}
variant="default"
>
<RiHeartLine size={25} />
</RemoteButton>
{(song?.serverType === 'navidrome' || song?.serverType === 'subsonic') && (
<div style={{ margin: 'auto' }}>
<Tooltip
label="Double click to clear"
openDelay={1000}
>
<Rating
onChange={debouncedSetRating}
onDoubleClick={() => debouncedSetRating(0)}
style={{ margin: 'auto' }}
value={song.userRating ?? 0}
/>
</Tooltip>
</div>
)}
</Group> </Group>
{id && position !== undefined && ( <Stack gap="lg">
<WrapperSlider {id && position !== undefined && (
label={(value) => formatDuration(value * 1e3)} <WrappedSlider
leftLabel={formatDuration(position * 1e3)} label={(value) => formatDuration(value * 1e3)}
max={song.duration / 1e3} leftLabel={formatDuration(position * 1e3)}
onChangeEnd={(e) => send({ event: 'position', position: e })} max={song.duration / 1e3}
rightLabel={formatDuration(song.duration)} onChangeEnd={(e) => send({ event: 'position', position: e })}
value={position} rightLabel={formatDuration(song.duration)}
value={position}
/>
)}
<WrappedSlider
leftLabel={<RiVolumeUpFill size={20} />}
max={100}
onChangeEnd={(e) => send({ event: 'volume', volume: e })}
rightLabel={
<Text
fw={600}
size="xs"
>
{volume ?? 0}
</Text>
}
value={volume ?? 0}
/> />
)} </Stack>
<WrapperSlider </Stack>
leftLabel={<RiVolumeUpFill size={20} />}
max={100}
onChangeEnd={(e) => send({ event: 'volume', volume: e })}
rightLabel={
<Text
fw={600}
size="xs"
>
{volume ?? 0}
</Text>
}
value={volume ?? 0}
/>
{showImage && (
<Image
onError={() => send({ event: 'proxy' })}
src={song?.imageUrl?.replaceAll(/&(size|width|height=\d+)/g, '')}
/>
)}
</>
); );
}; };

View file

@ -1,54 +1,71 @@
import { AppShell, Container, Flex, Grid, Image, Skeleton, Title } from '@mantine/core'; import { AppShell, Flex, Grid, Image } from '@mantine/core';
import { ImageButton } from '/@/remote/components/buttons/image-button'; import { ImageButton } from '/@/remote/components/buttons/image-button';
import { ReconnectButton } from '/@/remote/components/buttons/reconnect-button'; import { ReconnectButton } from '/@/remote/components/buttons/reconnect-button';
import { ThemeButton } from '/@/remote/components/buttons/theme-button'; import { ThemeButton } from '/@/remote/components/buttons/theme-button';
import { RemoteContainer } from '/@/remote/components/remote-container'; import { RemoteContainer } from '/@/remote/components/remote-container';
import { useConnected } from '/@/remote/store'; import { useConnected } from '/@/remote/store';
import { Center } from '/@/shared/components/center/center';
import { Group } from '/@/shared/components/group/group';
import { Spinner } from '/@/shared/components/spinner/spinner';
export const Shell = () => { export const Shell = () => {
const connected = useConnected(); const connected = useConnected();
return ( return (
<AppShell padding="md"> <AppShell
<AppShell.Header> h="100vh"
<Grid> padding="md"
<Grid.Col span="auto"> w="100vw"
<div> >
<AppShell.Header style={{ background: 'var(--theme-colors-surface)' }}>
<Grid
px="md"
py="sm"
>
<Grid.Col span={4}>
<Flex
align="center"
direction="row"
h="100%"
justify="flex-start"
style={{
justifySelf: 'flex-start',
}}
>
<Image <Image
fit="contain" fit="contain"
height={60} height={32}
src="/favicon.ico" src="/favicon.ico"
width={60} width={32}
/> />
</div> </Flex>
</Grid.Col> </Grid.Col>
<Grid.Col hiddenFrom="md"> <Grid.Col span={8}>
<Title ta="center">Feishin Remote</Title> <Group
</Grid.Col> gap="sm"
justify="flex-end"
<Grid.Col span="auto"> wrap="nowrap"
<Flex
direction="row"
justify="right"
> >
<ReconnectButton /> <ReconnectButton />
<ImageButton /> <ImageButton />
<ThemeButton /> <ThemeButton />
</Flex> </Group>
</Grid.Col> </Grid.Col>
</Grid> </Grid>
</AppShell.Header> </AppShell.Header>
<Container> <AppShell.Main pt="60px">
{connected ? ( {connected ? (
<RemoteContainer /> <RemoteContainer />
) : ( ) : (
<Skeleton <Center
height={300} h="100vh"
width="100%" w="100vw"
/> >
<Spinner />
</Center>
)} )}
</Container> </AppShell.Main>
</AppShell> </AppShell>
); );
}; };

View file

@ -1,21 +0,0 @@
.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,7 +1,8 @@
import { rem, Slider, SliderProps } from '@mantine/core'; import { rem, Slider, SliderProps } from '@mantine/core';
import { ReactNode, useState } from 'react'; import { ReactNode, useState } from 'react';
import styles from './wrapped-slider.module.css'; import { Group } from '/@/shared/components/group/group';
import { Text } from '/@/shared/components/text/text';
const PlayerbarSlider = ({ ...props }: SliderProps) => { const PlayerbarSlider = ({ ...props }: SliderProps) => {
return ( return (
@ -37,6 +38,7 @@ const PlayerbarSlider = ({ ...props }: SliderProps) => {
'&::before': { '&::before': {
right: 'calc(0.1rem * -1)', right: 'calc(0.1rem * -1)',
}, },
height: '1rem',
}, },
}} }}
{...props} {...props}
@ -54,31 +56,32 @@ export interface WrappedProps extends Omit<SliderProps, 'onChangeEnd'> {
value: number; value: number;
} }
export const WrapperSlider = ({ leftLabel, rightLabel, value, ...props }: WrappedProps) => { export const WrappedSlider = ({ leftLabel, rightLabel, value, ...props }: WrappedProps) => {
const [isSeeking, setIsSeeking] = useState(false); const [isSeeking, setIsSeeking] = useState(false);
const [seek, setSeek] = useState(0); const [seek, setSeek] = useState(0);
return ( return (
<div className={styles.container}> <Group
{leftLabel && <div className={styles.valueWrapper}>{leftLabel}</div>} align="center"
<div className={styles.wrapper}> wrap="nowrap"
<PlayerbarSlider >
{...props} {leftLabel && <Text size="sm">{leftLabel}</Text>}
min={0} <PlayerbarSlider
onChange={(e) => { {...props}
setIsSeeking(true); min={0}
setSeek(e); onChange={(e) => {
}} setIsSeeking(true);
onChangeEnd={(e) => { setSeek(e);
props.onChangeEnd(e); }}
setIsSeeking(false); onChangeEnd={(e) => {
}} props.onChangeEnd(e);
size={6} setIsSeeking(false);
value={!isSeeking ? (value ?? 0) : seek} }}
w="100%" size={6}
/> value={!isSeeking ? (value ?? 0) : seek}
</div> w="100%"
{rightLabel && <div className={styles.valueWrapper}>{rightLabel}</div>} />
</div> {rightLabel && <Text size="sm">{rightLabel}</Text>}
</Group>
); );
}; };

View file

@ -5,6 +5,9 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta http-equiv="Content-Security-Policy" /> <meta http-equiv="Content-Security-Policy" />
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<title>Feishin Remote</title> <title>Feishin Remote</title>
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
<script> <script>

View file

@ -1,4 +1,3 @@
import { Notifications } from '@mantine/notifications';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { App } from '/@/remote/app'; import { App } from '/@/remote/app';
@ -6,12 +5,4 @@ import { App } from '/@/remote/app';
const container = document.getElementById('root')! as HTMLElement; const container = document.getElementById('root')! as HTMLElement;
const root = createRoot(container); const root = createRoot(container);
root.render( root.render(<App />);
<>
<Notifications
containerWidth="300px"
position="bottom-center"
/>
<App />
</>,
);

View file

@ -1,112 +0,0 @@
@import url('../../renderer/styles/ag-grid.css');
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
body,
html {
position: absolute;
display: block;
width: 100%;
height: 100%;
overflow: hidden;
font-family: var(--theme-content-font-family);
font-size: var(--theme-root-font-size);
color: var(--theme-content-text-color);
user-select: none;
background: var(--theme-content-bg);
}
@media only screen and (width < 640px) {
body,
html {
overflow-x: auto;
}
}
#app {
height: inherit;
}
*,
*::before,
*::after {
box-sizing: border-box;
text-rendering: optimizelegibility;
-webkit-tap-highlight-color: rgb(0 0 0 / 0%);
text-size-adjust: none;
outline: none;
}
::-webkit-scrollbar {
width: 12px;
height: 12px;
}
/* ::-webkit-scrollbar-corner {
background: var(--theme-scrollbar-track-background);
}
::-webkit-scrollbar-track {
background: var(--theme-scrollbar-track-background);
}
::-webkit-scrollbar-thumb {
background: var(--theme-scrollbar-handle-background);
}
::-webkit-scrollbar-thumb:hover {
background: var(--theme-scrollbar-handle-hover-background);
} */
a {
text-decoration: none;
}
button {
-webkit-app-region: no-drag;
}
.overlay-scrollbar {
overflow: auto !important;
}
.hide-scrollbar {
scrollbar-color: transparent transparent;
scrollbar-width: thin;
&::-webkit-scrollbar {
width: 1px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background-color: transparent;
}
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}

View file

@ -9,13 +9,12 @@ import '@mantine/core/styles.css';
import '@mantine/notifications/styles.css'; import '@mantine/notifications/styles.css';
import '@mantine/dates/styles.css'; import '@mantine/dates/styles.css';
import './styles/global.css'; import '/@/shared/styles/global.css';
import '@ag-grid-community/styles/ag-grid.css'; import '@ag-grid-community/styles/ag-grid.css';
import 'overlayscrollbars/overlayscrollbars.css'; import 'overlayscrollbars/overlayscrollbars.css';
import './styles/overlayscrollbars.css'; import '/styles/overlayscrollbars.css';
import i18n from '/@/i18n/i18n'; import i18n from '/@/i18n/i18n';
import { ContextMenuProvider } from '/@/renderer/features/context-menu'; import { ContextMenuProvider } from '/@/renderer/features/context-menu';
import { useDiscordRpc } from '/@/renderer/features/discord-rpc/use-discord-rpc'; import { useDiscordRpc } from '/@/renderer/features/discord-rpc/use-discord-rpc';

View file

@ -12,7 +12,7 @@ export const THEME_DATA = [
{ label: 'Default Light', type: 'light', value: AppTheme.DEFAULT_LIGHT }, { label: 'Default Light', type: 'light', value: AppTheme.DEFAULT_LIGHT },
]; ];
export const useAppTheme = () => { export const useAppTheme = (overrideTheme?: AppTheme) => {
const accent = useSettingsStore((store) => store.general.accent); const accent = useSettingsStore((store) => store.general.accent);
const nativeImageAspect = useSettingsStore((store) => store.general.nativeAspectRatio); const nativeImageAspect = useSettingsStore((store) => store.general.nativeAspectRatio);
const { builtIn, custom, system, type } = useSettingsStore((state) => state.font); const { builtIn, custom, system, type } = useSettingsStore((state) => state.font);
@ -28,6 +28,10 @@ export const useAppTheme = () => {
}; };
const getSelectedTheme = () => { const getSelectedTheme = () => {
if (overrideTheme) {
return overrideTheme;
}
if (followSystemTheme) { if (followSystemTheme) {
return isDarkTheme ? themeDark : themeLight; return isDarkTheme ? themeDark : themeLight;
} }

View file

@ -63,6 +63,7 @@ import {
LuLogOut, LuLogOut,
LuMenu, LuMenu,
LuMinus, LuMinus,
LuMoon,
LuMusic, LuMusic,
LuMusic2, LuMusic2,
LuPanelRightClose, LuPanelRightClose,
@ -88,6 +89,7 @@ import {
LuStar, LuStar,
LuStepBack, LuStepBack,
LuStepForward, LuStepForward,
LuSun,
LuTable, LuTable,
LuTriangleAlert, LuTriangleAlert,
LuUser, LuUser,
@ -204,6 +206,8 @@ export const AppIcon = {
squareCheck: LuSquareCheck, squareCheck: LuSquareCheck,
star: LuStar, star: LuStar,
success: LuCircleCheck, success: LuCircleCheck,
themeDark: LuMoon,
themeLight: LuSun,
track: LuMusic2, track: LuMusic2,
unfavorite: LuHeartCrack, unfavorite: LuHeartCrack,
user: LuUser, user: LuUser,

View file

@ -122,56 +122,59 @@ button {
@font-face { @font-face {
font-family: Archivo; font-family: Archivo;
font-weight: 100 1000; font-weight: 100 1000;
src: url('../fonts/Archivo-VariableFont_wdth,wght.ttf') format('truetype-variations'); src: url('../../renderer/fonts/Archivo-VariableFont_wdth,wght.ttf')
format('truetype-variations');
} }
@font-face { @font-face {
font-family: Raleway; font-family: Raleway;
font-weight: 100 1000; font-weight: 100 1000;
src: url('../fonts/Raleway-VariableFont_wght.ttf') format('truetype-variations'); src: url('../../renderer/fonts/Raleway-VariableFont_wght.ttf') format('truetype-variations');
} }
@font-face { @font-face {
font-family: Fredoka; font-family: Fredoka;
font-weight: 100 1000; font-weight: 100 1000;
src: url('../fonts/Fredoka-VariableFont_wdth,wght.ttf') format('truetype-variations'); src: url('../../renderer/fonts/Fredoka-VariableFont_wdth,wght.ttf')
format('truetype-variations');
} }
@font-face { @font-face {
font-family: 'League Spartan'; font-family: 'League Spartan';
font-weight: 100 1000; font-weight: 100 1000;
src: url('../fonts/LeagueSpartan-VariableFont_wght.ttf') format('truetype-variations'); src: url('../../renderer/fonts/LeagueSpartan-VariableFont_wght.ttf')
format('truetype-variations');
} }
@font-face { @font-face {
font-family: Lexend; font-family: Lexend;
font-weight: 100 1000; font-weight: 100 1000;
src: url('../fonts/Lexend-VariableFont_wght.ttf') format('truetype-variations'); src: url('../../renderer/fonts/Lexend-VariableFont_wght.ttf') format('truetype-variations');
} }
@font-face { @font-face {
font-family: Inter; font-family: Inter;
font-weight: 100 1000; font-weight: 100 1000;
src: url('../fonts/Inter-VariableFont_slnt,wght.ttf') format('truetype-variations'); src: url('../../renderer/fonts/Inter-VariableFont_slnt,wght.ttf') format('truetype-variations');
} }
@font-face { @font-face {
font-family: Sora; font-family: Sora;
font-weight: 100 1000; font-weight: 100 1000;
src: url('../fonts/Sora-VariableFont_wght.ttf') format('truetype-variations'); src: url('../../renderer/fonts/Sora-VariableFont_wght.ttf') format('truetype-variations');
} }
@font-face { @font-face {
font-family: 'Work Sans'; font-family: 'Work Sans';
font-weight: 100 1000; font-weight: 100 1000;
src: url('../fonts/WorkSans-VariableFont_wght.ttf') format('truetype-variations'); src: url('../../renderer/fonts/WorkSans-VariableFont_wght.ttf') format('truetype-variations');
} }
@font-face { @font-face {
font-family: Poppins; font-family: Poppins;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: url('../fonts/Poppins-Regular.ttf') format('truetype'); src: url('../../renderer/fonts/Poppins-Regular.ttf') format('truetype');
font-display: swap; font-display: swap;
} }
@ -179,7 +182,7 @@ button {
font-family: Poppins; font-family: Poppins;
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
src: url('../fonts/Poppins-SemiBold.ttf') format('truetype'); src: url('../../renderer/fonts/Poppins-SemiBold.ttf') format('truetype');
font-display: swap; font-display: swap;
} }
@ -187,7 +190,7 @@ button {
font-family: Poppins; font-family: Poppins;
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
src: url('../fonts/Poppins-Bold.ttf') format('truetype'); src: url('../../renderer/fonts/Poppins-Bold.ttf') format('truetype');
font-display: swap; font-display: swap;
} }
@ -195,7 +198,7 @@ button {
font-family: Poppins; font-family: Poppins;
font-style: normal; font-style: normal;
font-weight: 800; font-weight: 800;
src: url('../fonts/Poppins-ExtraBold.ttf') format('truetype'); src: url('../../renderer/fonts/Poppins-ExtraBold.ttf') format('truetype');
font-display: swap; font-display: swap;
} }
@ -203,14 +206,14 @@ button {
font-family: Poppins; font-family: Poppins;
font-style: normal; font-style: normal;
font-weight: 900; font-weight: 900;
src: url('../fonts/Poppins-Black.ttf') format('truetype'); src: url('../../renderer/fonts/Poppins-Black.ttf') format('truetype');
font-display: swap; font-display: swap;
} }
@font-face { @font-face {
font-family: Raleway; font-family: Raleway;
font-weight: 100 1000; font-weight: 100 1000;
src: url('../fonts/Raleway-VariableFont_wght.ttf') format('truetype-variations'); src: url('../../renderer/fonts/Raleway-VariableFont_wght.ttf') format('truetype-variations');
} }
@font-face { @font-face {

View file

@ -21,7 +21,6 @@ export default defineConfig({
}, },
css: { css: {
modules: { modules: {
generateScopedName: '[name]__[local]__[hash:base64:5]',
localsConvention: 'camelCase', localsConvention: 'camelCase',
}, },
}, },