mirror of
https://github.com/antebudimir/feishin.git
synced 2025-12-31 10:03:33 +00:00
fix and update remote design
This commit is contained in:
parent
ad533a1d9c
commit
6689e84f67
24 changed files with 326 additions and 453 deletions
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
7
src/remote/components/player-image.module.css
Normal file
7
src/remote/components/player-image.module.css
Normal 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);
|
||||||
|
}
|
||||||
18
src/remote/components/player-image.tsx
Normal file
18
src/remote/components/player-image.tsx
Normal 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, '')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
0
src/remote/components/remote-container.module.css
Normal file
0
src/remote/components/remote-container.module.css
Normal 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
|
||||||
{id && (
|
gap="md"
|
||||||
<>
|
h="100dvh"
|
||||||
<Title order={1}>{song.name}</Title>
|
w="100%"
|
||||||
<Group align="flex-end">
|
>
|
||||||
<Title order={2}>Album: {song.album}</Title>
|
{showImage && (
|
||||||
<Title order={2}>Artist: {song.artistName}</Title>
|
<Flex
|
||||||
</Group>
|
align="center"
|
||||||
<Group justify="space-between">
|
justify="center"
|
||||||
<Title order={3}>Duration: {formatDuration(song.duration)}</Title>
|
w="100%"
|
||||||
{song.releaseDate && (
|
>
|
||||||
<Title order={3}>
|
<PlayerImage src={song?.imageUrl} />
|
||||||
Released: {new Date(song.releaseDate).toLocaleDateString()}
|
</Flex>
|
||||||
</Title>
|
|
||||||
)}
|
)}
|
||||||
<Title order={3}>Plays: {song.playCount}</Title>
|
{id && (
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Text
|
||||||
|
fw={700}
|
||||||
|
size="xl"
|
||||||
|
style={{
|
||||||
|
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">
|
||||||
|
{song.releaseDate && (
|
||||||
|
<Text isMuted>{new Date(song.releaseDate).toLocaleDateString()}</Text>
|
||||||
|
)}
|
||||||
|
<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,46 +214,11 @@ 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>
|
||||||
|
<Stack gap="lg">
|
||||||
{id && position !== undefined && (
|
{id && position !== undefined && (
|
||||||
<WrapperSlider
|
<WrappedSlider
|
||||||
label={(value) => formatDuration(value * 1e3)}
|
label={(value) => formatDuration(value * 1e3)}
|
||||||
leftLabel={formatDuration(position * 1e3)}
|
leftLabel={formatDuration(position * 1e3)}
|
||||||
max={song.duration / 1e3}
|
max={song.duration / 1e3}
|
||||||
|
|
@ -179,7 +227,7 @@ export const RemoteContainer = () => {
|
||||||
value={position}
|
value={position}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<WrapperSlider
|
<WrappedSlider
|
||||||
leftLabel={<RiVolumeUpFill size={20} />}
|
leftLabel={<RiVolumeUpFill size={20} />}
|
||||||
max={100}
|
max={100}
|
||||||
onChangeEnd={(e) => send({ event: 'volume', volume: e })}
|
onChangeEnd={(e) => send({ event: 'volume', volume: e })}
|
||||||
|
|
@ -193,12 +241,7 @@ export const RemoteContainer = () => {
|
||||||
}
|
}
|
||||||
value={volume ?? 0}
|
value={volume ?? 0}
|
||||||
/>
|
/>
|
||||||
{showImage && (
|
</Stack>
|
||||||
<Image
|
</Stack>
|
||||||
onError={() => send({ event: 'proxy' })}
|
|
||||||
src={song?.imageUrl?.replaceAll(/&(size|width|height=\d+)/g, '')}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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,14 +56,16 @@ 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"
|
||||||
|
>
|
||||||
|
{leftLabel && <Text size="sm">{leftLabel}</Text>}
|
||||||
<PlayerbarSlider
|
<PlayerbarSlider
|
||||||
{...props}
|
{...props}
|
||||||
min={0}
|
min={0}
|
||||||
|
|
@ -77,8 +81,7 @@ export const WrapperSlider = ({ leftLabel, rightLabel, value, ...props }: Wrappe
|
||||||
value={!isSeeking ? (value ?? 0) : seek}
|
value={!isSeeking ? (value ?? 0) : seek}
|
||||||
w="100%"
|
w="100%"
|
||||||
/>
|
/>
|
||||||
</div>
|
{rightLabel && <Text size="sm">{rightLabel}</Text>}
|
||||||
{rightLabel && <div className={styles.valueWrapper}>{rightLabel}</div>}
|
</Group>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 />
|
|
||||||
</>,
|
|
||||||
);
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
@ -21,7 +21,6 @@ export default defineConfig({
|
||||||
},
|
},
|
||||||
css: {
|
css: {
|
||||||
modules: {
|
modules: {
|
||||||
generateScopedName: '[name]__[local]__[hash:base64:5]',
|
|
||||||
localsConvention: 'camelCase',
|
localsConvention: 'camelCase',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue