diff --git a/package.json b/package.json index 876dd57a..c92c3a18 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "build:remote": "vite build --config remote.vite.config.ts", "build:web": "vite build --config web.vite.config.ts", "dev": "electron-vite dev", + "dev:remote": "vite dev --config remote.vite.config.ts", "dev:watch": "electron-vite dev --watch", "i18next": "i18next -c src/i18n/i18next-parser.config.js", "postinstall": "electron-builder install-app-deps", diff --git a/remote.vite.config.ts b/remote.vite.config.ts index 0c81ca0d..6aa05de5 100644 --- a/remote.vite.config.ts +++ b/remote.vite.config.ts @@ -25,6 +25,12 @@ export default defineConfig({ }, sourcemap: true, }, + css: { + modules: { + generateScopedName: 'fs-[name]-[local]', + localsConvention: 'camelCase', + }, + }, plugins: [ react(), ViteEjsPlugin({ diff --git a/src/remote/app.tsx b/src/remote/app.tsx index 36c98207..cfd6034c 100644 --- a/src/remote/app.tsx +++ b/src/remote/app.tsx @@ -1,10 +1,15 @@ 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 { useIsDark, useReconnect } from '/@/remote/store'; +import { useAppTheme } from '/@/renderer/themes/use-app-theme'; +import { AppTheme } from '/@/shared/themes/app-theme-types'; export const App = () => { const isDark = useIsDark(); @@ -14,58 +19,12 @@ export const App = () => { reconnect(); }, [reconnect]); + const { mode, theme } = useAppTheme(isDark ? AppTheme.DEFAULT_DARK : AppTheme.DEFAULT_LIGHT); + return ( diff --git a/src/remote/components/buttons/image-button.tsx b/src/remote/components/buttons/image-button.tsx index 1d5744b2..c1f232f8 100644 --- a/src/remote/components/buttons/image-button.tsx +++ b/src/remote/components/buttons/image-button.tsx @@ -1,23 +1,21 @@ import { CiImageOff, CiImageOn } from 'react-icons/ci'; -import { RemoteButton } from '/@/remote/components/buttons/remote-button'; import { useShowImage, useToggleShowImage } from '/@/remote/store'; +import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; export const ImageButton = () => { const showImage = useShowImage(); const toggleImage = useToggleShowImage(); return ( - toggleImage()} - size="xl" tooltip={{ label: showImage ? 'Hide Image' : 'Show Image', }} variant="default" > {showImage ? : } - + ); }; diff --git a/src/remote/components/buttons/reconnect-button.tsx b/src/remote/components/buttons/reconnect-button.tsx index 997ce1b9..101f54d5 100644 --- a/src/remote/components/buttons/reconnect-button.tsx +++ b/src/remote/components/buttons/reconnect-button.tsx @@ -1,24 +1,24 @@ import { RiRestartLine } from 'react-icons/ri'; -import { RemoteButton } from '/@/remote/components/buttons/remote-button'; import { useConnected, useReconnect } from '/@/remote/store'; +import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; export const ReconnectButton = () => { const connected = useConnected(); const reconnect = useReconnect(); return ( - reconnect()} - size="xl" tooltip={{ label: connected ? 'Reconnect' : 'Not connected. Reconnect.', }} variant="default" > - - + + ); }; diff --git a/src/remote/components/buttons/remote-button.module.css b/src/remote/components/buttons/remote-button.module.css deleted file mode 100644 index 2e852130..00000000 --- a/src/remote/components/buttons/remote-button.module.css +++ /dev/null @@ -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; - } - } -} diff --git a/src/remote/components/buttons/remote-button.tsx b/src/remote/components/buttons/remote-button.tsx deleted file mode 100644 index 206acf10..00000000 --- a/src/remote/components/buttons/remote-button.tsx +++ /dev/null @@ -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; -} - -export const RemoteButton = forwardRef( - ({ children, isActive, tooltip, ...props }, ref) => { - return ( - - ); - }, -); diff --git a/src/remote/components/buttons/theme-button.tsx b/src/remote/components/buttons/theme-button.tsx index bccdbfdf..2a17dcdc 100644 --- a/src/remote/components/buttons/theme-button.tsx +++ b/src/remote/components/buttons/theme-button.tsx @@ -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 { 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 = () => { const isDark = useIsDark(); const toggleDark = useToggleDark(); - useEffect(() => { - const targetTheme: AppTheme = isDark ? AppTheme.DEFAULT_DARK : AppTheme.DEFAULT_LIGHT; - document.body.setAttribute('data-theme', targetTheme); - }, [isDark]); + const handleToggleTheme = () => { + toggleDark(); + }; return ( - toggleDark()} - size="xl" + - {isDark ? : } - + {isDark ? ( + + ) : ( + + )} + ); }; diff --git a/src/remote/components/player-image.module.css b/src/remote/components/player-image.module.css new file mode 100644 index 00000000..2418d4bb --- /dev/null +++ b/src/remote/components/player-image.module.css @@ -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); +} diff --git a/src/remote/components/player-image.tsx b/src/remote/components/player-image.tsx new file mode 100644 index 00000000..cb73bc1b --- /dev/null +++ b/src/remote/components/player-image.tsx @@ -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 ( + send({ event: 'proxy' })} + src={src?.replaceAll(/&(size|width|height=\d+)/g, '')} + /> + ); +}; diff --git a/src/remote/components/remote-container.module.css b/src/remote/components/remote-container.module.css new file mode 100644 index 00000000..e69de29b diff --git a/src/remote/components/remote-container.tsx b/src/remote/components/remote-container.tsx index d0adab56..e29fe624 100644 --- a/src/remote/components/remote-container.tsx +++ b/src/remote/components/remote-container.tsx @@ -1,24 +1,16 @@ -import { Image, Title } from '@mantine/core'; import formatDuration from 'format-duration'; import debounce from 'lodash/debounce'; import { useCallback } from 'react'; -import { - RiHeartLine, - RiPauseFill, - RiPlayFill, - RiRepeat2Line, - RiRepeatOneLine, - RiShuffleFill, - RiSkipBackFill, - RiSkipForwardFill, - RiVolumeUpFill, -} from 'react-icons/ri'; +import { RiPauseFill, RiPlayFill, RiVolumeUpFill } from 'react-icons/ri'; -import { RemoteButton } from '/@/remote/components/buttons/remote-button'; -import { WrapperSlider } from '/@/remote/components/wrapped-slider'; +import { PlayerImage } from '/@/remote/components/player-image'; +import { WrappedSlider } from '/@/remote/components/wrapped-slider'; 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 { Rating } from '/@/shared/components/rating/rating'; +import { Stack } from '/@/shared/components/stack/stack'; import { Text } from '/@/shared/components/text/text'; import { Tooltip } from '/@/shared/components/tooltip/tooltip'; import { PlayerRepeat, PlayerStatus } from '/@/shared/types/types'; @@ -40,40 +32,115 @@ export const RemoteContainer = () => { const debouncedSetRating = debounce(setRating, 400); return ( - <> + + {showImage && ( + + + + )} {id && ( - <> - {song.name} - - Album: {song.album} - Artist: {song.artistName} - + + + {song.name} + + + {song.album} + + + {song.artistName} + - Duration: {formatDuration(song.duration)} {song.releaseDate && ( - - Released: {new Date(song.releaseDate).toLocaleDateString()} - + {new Date(song.releaseDate).toLocaleDateString()} )} - Plays: {song.playCount} + Plays: {song.playCount} - + )} - { + if (!id) return; + + send({ event: 'favorite', favorite: !song.userFavorite, id }); + }} + tooltip={{ + label: song?.userFavorite ? 'Unfavorite' : 'Favorite', + }} + variant="transparent" + /> + {(song?.serverType === 'navidrome' || song?.serverType === 'subsonic') && ( +
+ + debouncedSetRating(0)} + style={{ margin: 'auto' }} + value={song.userRating ?? 0} + /> + +
+ )} +
+ + send({ event: 'previous' })} tooltip={{ label: 'Previous track', }} variant="default" - > - - - + { if (status === PlayerStatus.PLAYING) { @@ -92,34 +159,50 @@ export const RemoteContainer = () => { ) : ( )} - - + send({ event: 'next' })} tooltip={{ label: 'Next track', }} variant="default" - > - - + /> - send({ event: 'shuffle' })} tooltip={{ label: shuffle ? 'Shuffle tracks' : 'Shuffle disabled', }} variant="default" - > - - - + send({ event: 'repeat' })} tooltip={{ label: `Repeat ${ @@ -131,74 +214,34 @@ export const RemoteContainer = () => { }`, }} variant="default" - > - {repeat === undefined || repeat === PlayerRepeat.ONE ? ( - - ) : ( - - )} - - { - if (!id) return; - - send({ event: 'favorite', favorite: !song.userFavorite, id }); - }} - tooltip={{ - label: song?.userFavorite ? 'Unfavorite' : 'Favorite', - }} - variant="default" - > - - - {(song?.serverType === 'navidrome' || song?.serverType === 'subsonic') && ( -
- - debouncedSetRating(0)} - style={{ margin: 'auto' }} - value={song.userRating ?? 0} - /> - -
- )} + />
- {id && position !== undefined && ( - formatDuration(value * 1e3)} - leftLabel={formatDuration(position * 1e3)} - max={song.duration / 1e3} - onChangeEnd={(e) => send({ event: 'position', position: e })} - rightLabel={formatDuration(song.duration)} - value={position} + + {id && position !== undefined && ( + formatDuration(value * 1e3)} + leftLabel={formatDuration(position * 1e3)} + max={song.duration / 1e3} + onChangeEnd={(e) => send({ event: 'position', position: e })} + rightLabel={formatDuration(song.duration)} + value={position} + /> + )} + } + max={100} + onChangeEnd={(e) => send({ event: 'volume', volume: e })} + rightLabel={ + + {volume ?? 0} + + } + value={volume ?? 0} /> - )} - } - max={100} - onChangeEnd={(e) => send({ event: 'volume', volume: e })} - rightLabel={ - - {volume ?? 0} - - } - value={volume ?? 0} - /> - {showImage && ( - send({ event: 'proxy' })} - src={song?.imageUrl?.replaceAll(/&(size|width|height=\d+)/g, '')} - /> - )} - + +
); }; diff --git a/src/remote/components/shell.tsx b/src/remote/components/shell.tsx index dd80367b..83f3d58c 100644 --- a/src/remote/components/shell.tsx +++ b/src/remote/components/shell.tsx @@ -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 { ReconnectButton } from '/@/remote/components/buttons/reconnect-button'; import { ThemeButton } from '/@/remote/components/buttons/theme-button'; import { RemoteContainer } from '/@/remote/components/remote-container'; 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 = () => { const connected = useConnected(); return ( - - - - -
+ + + + + -
+
- - Feishin Remote - - - - + - +
- + {connected ? ( ) : ( - +
+ +
)} -
+
); }; diff --git a/src/remote/components/wrapped-slider.module.css b/src/remote/components/wrapped-slider.module.css deleted file mode 100644 index b3807cd0..00000000 --- a/src/remote/components/wrapped-slider.module.css +++ /dev/null @@ -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; -} diff --git a/src/remote/components/wrapped-slider.tsx b/src/remote/components/wrapped-slider.tsx index b17bf572..cf6d6e88 100644 --- a/src/remote/components/wrapped-slider.tsx +++ b/src/remote/components/wrapped-slider.tsx @@ -1,7 +1,8 @@ import { rem, Slider, SliderProps } from '@mantine/core'; 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) => { return ( @@ -37,6 +38,7 @@ const PlayerbarSlider = ({ ...props }: SliderProps) => { '&::before': { right: 'calc(0.1rem * -1)', }, + height: '1rem', }, }} {...props} @@ -54,31 +56,32 @@ export interface WrappedProps extends Omit { value: number; } -export const WrapperSlider = ({ leftLabel, rightLabel, value, ...props }: WrappedProps) => { +export const WrappedSlider = ({ leftLabel, rightLabel, value, ...props }: WrappedProps) => { const [isSeeking, setIsSeeking] = useState(false); const [seek, setSeek] = useState(0); return ( -
- {leftLabel &&
{leftLabel}
} -
- { - setIsSeeking(true); - setSeek(e); - }} - onChangeEnd={(e) => { - props.onChangeEnd(e); - setIsSeeking(false); - }} - size={6} - value={!isSeeking ? (value ?? 0) : seek} - w="100%" - /> -
- {rightLabel &&
{rightLabel}
} -
+ + {leftLabel && {leftLabel}} + { + setIsSeeking(true); + setSeek(e); + }} + onChangeEnd={(e) => { + props.onChangeEnd(e); + setIsSeeking(false); + }} + size={6} + value={!isSeeking ? (value ?? 0) : seek} + w="100%" + /> + {rightLabel && {rightLabel}} + ); }; diff --git a/src/remote/index.html b/src/remote/index.html index e7f977ce..9bbf5d61 100644 --- a/src/remote/index.html +++ b/src/remote/index.html @@ -5,6 +5,9 @@ + + + Feishin Remote