diff --git a/package-lock.json b/package-lock.json index 18abc222a..b70679163 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,6 +52,7 @@ "nanoid": "^3.3.3", "net": "^1.0.2", "node-mpv": "github:jeffvli/Node-MPV", + "nosleep.js": "^0.12.0", "overlayscrollbars": "^2.2.1", "overlayscrollbars-react": "^0.5.1", "react": "^18.2.0", @@ -15740,6 +15741,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/nosleep.js": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/nosleep.js/-/nosleep.js-0.12.0.tgz", + "integrity": "sha512-9d1HbpKLh3sdWlhXMhU6MMH+wQzKkrgfRkYV0EBdvt99YJfj0ilCJrWRDYG2130Tm4GXbEoTCx5b34JSaP+HhA==" + }, "node_modules/now-and-later": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-2.0.1.tgz", @@ -33866,6 +33872,11 @@ "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", "dev": true }, + "nosleep.js": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/nosleep.js/-/nosleep.js-0.12.0.tgz", + "integrity": "sha512-9d1HbpKLh3sdWlhXMhU6MMH+wQzKkrgfRkYV0EBdvt99YJfj0ilCJrWRDYG2130Tm4GXbEoTCx5b34JSaP+HhA==" + }, "now-and-later": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-2.0.1.tgz", diff --git a/package.json b/package.json index bc0ff1490..ffed37c1e 100644 --- a/package.json +++ b/package.json @@ -332,6 +332,7 @@ "nanoid": "^3.3.3", "net": "^1.0.2", "node-mpv": "github:jeffvli/Node-MPV", + "nosleep.js": "^0.12.0", "overlayscrollbars": "^2.2.1", "overlayscrollbars-react": "^0.5.1", "react": "^18.2.0", diff --git a/src/remote/app.tsx b/src/remote/app.tsx index d55814308..a1de2a57f 100644 --- a/src/remote/app.tsx +++ b/src/remote/app.tsx @@ -3,6 +3,7 @@ import { MantineProvider } from '@mantine/core'; import './styles/global.scss'; import { useIsDark, useReconnect } from '/@/remote/store'; import { Shell } from '/@/remote/components/shell'; +import { AppTheme } from '/@/renderer/themes/types'; export const App = () => { const isDark = useIsDark(); @@ -12,6 +13,11 @@ export const App = () => { reconnect(); }, [reconnect]); + useEffect(() => { + const targetTheme: AppTheme = isDark ? AppTheme.DEFAULT_DARK : AppTheme.DEFAULT_LIGHT; + document.body.setAttribute('data-theme', targetTheme); + }, [isDark]); + return ( { return ( toggleImage()} > {showImage ? : } diff --git a/src/remote/components/buttons/reconnect-button.tsx b/src/remote/components/buttons/reconnect-button.tsx index 41b5ed202..bf83d835e 100644 --- a/src/remote/components/buttons/reconnect-button.tsx +++ b/src/remote/components/buttons/reconnect-button.tsx @@ -9,10 +9,7 @@ export const ReconnectButton = () => { return ( reconnect()} > diff --git a/src/remote/components/buttons/remote-button.tsx b/src/remote/components/buttons/remote-button.tsx index 7e8b52423..226819d2f 100644 --- a/src/remote/components/buttons/remote-button.tsx +++ b/src/remote/components/buttons/remote-button.tsx @@ -12,7 +12,7 @@ interface StyledButtonProps extends MantineButtonProps { } export interface ButtonProps extends StyledButtonProps { - tooltip: string; + tooltip?: string; } const StyledButton = styled(Button)` @@ -37,19 +37,29 @@ const StyledButton = styled(Button)` export const RemoteButton = forwardRef( ({ children, tooltip, ...props }: ButtonProps, ref) => { - return ( - - - {children} - - + {children} + ); + if (tooltip) { + return ( + + {button} + + ); + } + return button; }, ); diff --git a/src/remote/components/buttons/sleep-button.tsx b/src/remote/components/buttons/sleep-button.tsx new file mode 100644 index 000000000..81ae8403a --- /dev/null +++ b/src/remote/components/buttons/sleep-button.tsx @@ -0,0 +1,18 @@ +import { RemoteButton } from '/@/remote/components/buttons/remote-button'; +import { LuMonitor, LuMonitorOff } from 'react-icons/lu'; +import { useNoSleepContext } from '/@/remote/context/nosleep-context'; +import { useToggleNoSleep } from '/@/remote/hooks/use-toggle-no-sleep'; + +export const SleepButton = () => { + const { enabled } = useNoSleepContext(); + const toggleNoSleep = useToggleNoSleep(); + + return ( + + {enabled ? : } + + ); +}; diff --git a/src/remote/components/buttons/theme-button.tsx b/src/remote/components/buttons/theme-button.tsx index 4e6bfc241..65db3a32d 100644 --- a/src/remote/components/buttons/theme-button.tsx +++ b/src/remote/components/buttons/theme-button.tsx @@ -1,24 +1,14 @@ import { useIsDark, useToggleDark } from '/@/remote/store'; import { RiMoonLine, RiSunLine } from 'react-icons/ri'; import { RemoteButton } from '/@/remote/components/buttons/remote-button'; -import { AppTheme } from '/@/renderer/themes/types'; -import { useEffect } from 'react'; 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]); - return ( toggleDark()} > {isDark ? : } diff --git a/src/remote/components/menu.tsx b/src/remote/components/menu.tsx new file mode 100644 index 000000000..b4900af25 --- /dev/null +++ b/src/remote/components/menu.tsx @@ -0,0 +1,49 @@ +import { CiImageOff, CiImageOn } from 'react-icons/ci'; +import { LuMonitor, LuMonitorOff } from 'react-icons/lu'; +import { RiMenuFill, RiMoonLine, RiSunLine } from 'react-icons/ri'; +import { RemoteButton } from '/@/remote/components/buttons/remote-button'; +import { DropdownMenu } from '/@/renderer/components/dropdown-menu'; +import { useIsDark, useShowImage, useToggleDark, useToggleShowImage } from '/@/remote/store'; +import { useNoSleepContext } from '/@/remote/context/nosleep-context'; +import { useToggleNoSleep } from '/@/remote/hooks/use-toggle-no-sleep'; + +export const ResponsiveMenu = () => { + const showImage = useShowImage(); + const toggleImage = useToggleShowImage(); + const isDark = useIsDark(); + const toggleDark = useToggleDark(); + + const { enabled } = useNoSleepContext(); + const toggleNoSleep = useToggleNoSleep(); + + return ( + + + + + + + + + : } + onClick={toggleImage} + > + {showImage ? 'Hide Image' : 'Show Image'} + + : } + onClick={toggleDark} + > + Toggle Theme + + : } + onClick={toggleNoSleep} + > + {enabled ? 'Enable screen lock' : 'Disable screen lock'} + + + + ); +}; diff --git a/src/remote/components/remote-container.tsx b/src/remote/components/remote-container.tsx index 2213fcbb3..e129cf7ef 100644 --- a/src/remote/components/remote-container.tsx +++ b/src/remote/components/remote-container.tsx @@ -1,9 +1,8 @@ import { useCallback } from 'react'; -import { Group, Image, Text, Title } from '@mantine/core'; +import { Center, Grid, Group, Image, MediaQuery, Text, Title } from '@mantine/core'; import { useInfo, useSend, useShowImage } from '/@/remote/store'; import { RemoteButton } from '/@/remote/components/buttons/remote-button'; import formatDuration from 'format-duration'; -import debounce from 'lodash/debounce'; import { RiHeartLine, RiPauseFill, @@ -17,7 +16,6 @@ import { } from 'react-icons/ri'; import { PlayerRepeat, PlayerStatus } from '/@/renderer/types'; import { WrapperSlider } from '/@/remote/components/wrapped-slider'; -import { Tooltip } from '/@/renderer/components/tooltip'; import { Rating } from '/@/renderer/components'; export const RemoteContainer = () => { @@ -34,8 +32,6 @@ export const RemoteContainer = () => { [send, id], ); - const debouncedSetRating = debounce(setRating, 400); - return ( <> {song && ( @@ -56,101 +52,125 @@ export const RemoteContainer = () => { )} - - send({ event: 'previous' })} - > - - - { - if (status === PlayerStatus.PLAYING) { - send({ event: 'pause' }); - } else if (status === PlayerStatus.PAUSED) { - send({ event: 'play' }); - } - }} - > - {status === PlayerStatus.PLAYING ? ( - - ) : ( - - )} - - send({ event: 'next' })} - > - - - - + send({ event: 'previous' })} + > + + + + + { + if (status === PlayerStatus.PLAYING) { + send({ event: 'pause' }); + } else if (status === PlayerStatus.PAUSED) { + send({ event: 'play' }); + } + }} + > + {status === PlayerStatus.PLAYING ? ( + + ) : ( + + )} + + + + + send({ event: 'next' })} + > + + + + + - send({ event: 'shuffle' })} + - - - send({ event: 'repeat' })} + send({ event: 'shuffle' })} + > + + + + - {repeat === undefined || repeat === PlayerRepeat.ONE ? ( - - ) : ( - - )} - - { - if (!id) return; + send({ event: 'repeat' })} + > + {repeat === undefined || repeat === PlayerRepeat.ONE ? ( + + ) : ( + + )} + + - send({ event: 'favorite', favorite: !song.userFavorite, id }); - }} + - - + { + if (!id) return; + + send({ event: 'favorite', favorite: !song.userFavorite, id }); + }} + > + + + + {(song?.serverType === 'navidrome' || song?.serverType === 'subsonic') && ( -
- + - debouncedSetRating(0)} - /> - -
+
+ +
+ + )} -
+ } max={100} diff --git a/src/remote/components/shell.tsx b/src/remote/components/shell.tsx index 1244f6c9c..b5782fa7c 100644 --- a/src/remote/components/shell.tsx +++ b/src/remote/components/shell.tsx @@ -14,62 +14,97 @@ import { ImageButton } from '/@/remote/components/buttons/image-button'; import { RemoteContainer } from '/@/remote/components/remote-container'; import { ReconnectButton } from '/@/remote/components/buttons/reconnect-button'; import { useConnected } from '/@/remote/store'; +import { NoSleepContext } from '/@/remote/context/nosleep-context'; +import NoSleep from 'nosleep.js'; +import { useMemo, useState } from 'react'; +import { SleepButton } from '/@/remote/components/buttons/sleep-button'; +import { ResponsiveMenu } from '/@/remote/components/menu'; export const Shell = () => { const connected = useConnected(); + const noSleep = useMemo(() => { + return new NoSleep(); + }, []); + + const [blockSleep, setBlockSleep] = useState(false); + + const noSleepValue = useMemo(() => { + return { enabled: blockSleep, noSleep, setEnabled: setBlockSleep }; + }, [blockSleep, noSleep]); return ( - - - -
- -
-
- - - Feishin Remote + + + + +
+ +
-
- - - + + Feishin Remote + + + + + + + + + + + - - - - - -
- - } - padding="md" - > - - {connected ? ( - - ) : ( - - )} - -
+ + + + + + + + + + + + } + padding="md" + > + + {connected ? ( + + ) : ( + + )} + + + ); }; diff --git a/src/remote/context/nosleep-context.tsx b/src/remote/context/nosleep-context.tsx new file mode 100644 index 000000000..d1f03aedb --- /dev/null +++ b/src/remote/context/nosleep-context.tsx @@ -0,0 +1,15 @@ +import { createContext, useContext } from 'react'; +import NoSleep from 'nosleep.js'; + +export const NoSleepContext = createContext<{ + enabled: boolean; + noSleep?: NoSleep; + setEnabled?: (val: boolean) => void; +}>({ + enabled: false, +}); + +export const useNoSleepContext = () => { + const ctxValue = useContext(NoSleepContext); + return ctxValue; +}; diff --git a/src/remote/hooks/use-toggle-no-sleep.tsx b/src/remote/hooks/use-toggle-no-sleep.tsx new file mode 100644 index 000000000..9f46838a7 --- /dev/null +++ b/src/remote/hooks/use-toggle-no-sleep.tsx @@ -0,0 +1,28 @@ +import { useCallback } from 'react'; +import { useNoSleepContext } from '/@/remote/context/nosleep-context'; +import { toast } from '/@/renderer/components'; + +export const useToggleNoSleep = () => { + const { noSleep, enabled, setEnabled } = useNoSleepContext(); + + const toggle = useCallback(async () => { + if (!noSleep) return; + + if (enabled) { + noSleep.disable(); + setEnabled!(false); + } else { + try { + await noSleep.enable(); + setEnabled!(true); + } catch (error) { + toast.error({ + message: (error as Error).message, + title: 'Failed to disable screen lock', + }); + } + } + }, [enabled, noSleep, setEnabled]); + + return toggle; +};