From 691c0caa8e164d00b2a647f681c74561c2f7ede6 Mon Sep 17 00:00:00 2001 From: Neko-Life Date: Sun, 10 Dec 2023 15:28:06 +0700 Subject: [PATCH 1/6] initial kbdsrct --- dashboard/kbdsrct.md | 56 ++++++++++++ dashboard/src/components/Modal.tsx | 22 +++++ dashboard/src/components/ModalShortcut.tsx | 35 ++++++++ dashboard/src/interfaces/components/Modal.ts | 10 +++ dashboard/src/interfaces/kbdsrct.ts | 4 + dashboard/src/layouts/AppLayout.tsx | 95 ++++++++++++++++++++ dashboard/src/libs/kbdsrct.ts | 89 ++++++++++++++++++ 7 files changed, 311 insertions(+) create mode 100644 dashboard/kbdsrct.md create mode 100644 dashboard/src/components/Modal.tsx create mode 100644 dashboard/src/components/ModalShortcut.tsx create mode 100644 dashboard/src/interfaces/components/Modal.ts create mode 100644 dashboard/src/interfaces/kbdsrct.ts create mode 100644 dashboard/src/libs/kbdsrct.ts diff --git a/dashboard/kbdsrct.md b/dashboard/kbdsrct.md new file mode 100644 index 000000000..cd47c81fe --- /dev/null +++ b/dashboard/kbdsrct.md @@ -0,0 +1,56 @@ +# Spotify Shortcuts + +For reference, only a few are gonna be implemented: + + +#### Basic + + - Create new playlist Alt+Shift+P + - Create new folder Ctrl+Alt+Shift+P + - Open context menu Alt+J + - Open Quick Search Ctrl+K + - Search in Your Library Shift+Ctrl+Alt+F + - Log out Alt+Shift+F6 + + +#### Playback + + - Play / Pause Space + - Like Alt+Shift+B + - Shuffle Alt+S + - Repeat Alt+R + - Skip to previous Alt+← + - Skip to next Alt+→ + - Seek backward Shift+← + - Seek forward Shift+→ + - Raise volume Alt+↑ + - Lower volume Alt+↓ + + +#### Navigation + + - Home Alt+Shift+H + - Back in history Ctrl+← + - Forward in history Ctrl+→ + - Currently playing Alt+Shift+J + - Search Ctrl+Shift+L + - Liked songs Alt+Shift+S + - Queue Alt+Shift+Q + - Your Library Alt+Shift+0 + - Your playlists Alt+Shift+1 + - Your podcasts Alt+Shift+2 + - Your artists Alt+Shift+3 + - Your albums Alt+Shift+4 + - Made for you Alt+Shift+M + - New Releases Alt+Shift+N + - Charts Alt+Shift+C + + +#### Layout + + - Toggle left sidebar Alt+Shift+L + - Decrease navigation bar width Alt+Shift+← + - Increase navigation bar width Alt+Shift+→ + - Toggle right sidebar Alt+Shift+R + - Decrease activity tab width Alt+Shift+↓ + - Increase activity tab width Alt+Shift+↑ diff --git a/dashboard/src/components/Modal.tsx b/dashboard/src/components/Modal.tsx new file mode 100644 index 000000000..74a88f455 --- /dev/null +++ b/dashboard/src/components/Modal.tsx @@ -0,0 +1,22 @@ +import { IModalProps } from '@/interfaces/components/Modal'; + +const containerStyle: React.HTMLAttributes['style'] = { + position: 'fixed', + width: '100vw', + height: '100vh', + top: 0, + left: 0, + display: 'flex', + justifyContent: 'center', + zIndex: 500, +}; + +export default function Modal({ children, fullHeight, open }: IModalProps) { + if (!open) return null; + + const cpyStyles = { ...containerStyle }; + + if (!fullHeight) cpyStyles.alignItems = 'center'; + + return
{children}
; +} diff --git a/dashboard/src/components/ModalShortcut.tsx b/dashboard/src/components/ModalShortcut.tsx new file mode 100644 index 000000000..8c8b3a41f --- /dev/null +++ b/dashboard/src/components/ModalShortcut.tsx @@ -0,0 +1,35 @@ +import Modal from '@/components/Modal'; +import { IModalShortcutProps } from '@/interfaces/components/Modal'; +import { Container } from '@nextui-org/react'; + +export default function ModalShortcut({ + open, + modalContainerProps = {}, +}: IModalShortcutProps) { + return ( + + +
+

Title

+
+
+

Description

+
+
+
+ ); +} diff --git a/dashboard/src/interfaces/components/Modal.ts b/dashboard/src/interfaces/components/Modal.ts new file mode 100644 index 000000000..c800d9458 --- /dev/null +++ b/dashboard/src/interfaces/components/Modal.ts @@ -0,0 +1,10 @@ +export interface IModalProps { + children?: React.ReactNode; + fullHeight?: boolean; + open?: boolean; +} + +export interface IModalShortcutProps { + open?: IModalProps['open']; + modalContainerProps?: IModalProps; +} diff --git a/dashboard/src/interfaces/kbdsrct.ts b/dashboard/src/interfaces/kbdsrct.ts new file mode 100644 index 000000000..a0ff44675 --- /dev/null +++ b/dashboard/src/interfaces/kbdsrct.ts @@ -0,0 +1,4 @@ +export interface IKbdsrct { + comb: string[]; + cb: () => void; +} diff --git a/dashboard/src/layouts/AppLayout.tsx b/dashboard/src/layouts/AppLayout.tsx index 6ba50cfa5..848dcddd7 100644 --- a/dashboard/src/layouts/AppLayout.tsx +++ b/dashboard/src/layouts/AppLayout.tsx @@ -1,5 +1,12 @@ +import ModalShortcut from '@/components/ModalShortcut'; import { PageLayout } from '@/interfaces/layouts'; +import { + execKbdsrct, + registerKbdsrct, + unregisterKbdsrct, +} from '@/libs/kbdsrct'; import { createTheme, NextUIProvider } from '@nextui-org/react'; +import { useEffect, useRef, useState } from 'react'; const AppLayout: PageLayout = ({ children, @@ -13,6 +20,93 @@ const AppLayout: PageLayout = ({ overflow: 'hidden', }, }) => { + const [modalShortcutOpen, setModalShortcutOpen] = useState(false); + + const pressesRef = useRef([]); + + const removePresses = (key: string) => { + const rmIdx = pressesRef.current.findIndex((v) => key === v); + if (rmIdx < 0) return false; + + pressesRef.current.splice(rmIdx, 1); + return true; + }; + + /** + * Modifier detection may not work depends on user's keyboard configuration, there's at least + * OS level and app level capture, each that can block browser javascript from getting any key press event + * or preventing some modifier to give its current actual state (mostly Alt key for me) + * + * The browser might not even fire keyboard event cuz it's already captured by the OS or other app + */ + const kbdsrctInHandler = (e: KeyboardEvent) => { + let hasMod = false; + + if (e.shiftKey) { + pressesRef.current.push('Shift'); + hasMod = true; + } + + if (e.metaKey) { + hasMod = true; + } + + if (e.ctrlKey) { + pressesRef.current.push('Control'); + hasMod = true; + } + + if (e.altKey) { + pressesRef.current.push('Alt'); + hasMod = true; + } + + // no sane keyboard shortcut without modifier + if (!hasMod) return; + + pressesRef.current.push(e.key); + + if (execKbdsrct(pressesRef.current)) e.preventDefault(); + }; + + const kbdsrctOutHandler = (e: KeyboardEvent) => { + if (removePresses(e.key)) { + e.preventDefault(); + } + + // remove modifier keys too + if (e.shiftKey) { + removePresses('Shift'); + } + + if (e.ctrlKey) { + removePresses('Control'); + } + + if (e.altKey) { + removePresses('Alt'); + } + }; + + const modalShortcutKbdsrct = { + comb: ['Control', '/'], + cb: () => setModalShortcutOpen((v) => !v), + }; + + useEffect(() => { + document.body.addEventListener('keydown', kbdsrctInHandler); + document.body.addEventListener('keyup', kbdsrctOutHandler); + + registerKbdsrct(modalShortcutKbdsrct); + + return () => { + document.body.removeEventListener('keydown', kbdsrctInHandler); + document.body.removeEventListener('keyup', kbdsrctOutHandler); + + unregisterKbdsrct(modalShortcutKbdsrct); + }; + }, []); + return (
{children}
+
); }; diff --git a/dashboard/src/libs/kbdsrct.ts b/dashboard/src/libs/kbdsrct.ts new file mode 100644 index 000000000..876e359de --- /dev/null +++ b/dashboard/src/libs/kbdsrct.ts @@ -0,0 +1,89 @@ +import { IKbdsrct } from '@/interfaces/kbdsrct'; + +const kbdsrcts: IKbdsrct[] = []; + +export function findRegistered(kbdsrct: IKbdsrct) { + for (const v of kbdsrcts) { + const vcl = v.comb.length; + + if (vcl !== kbdsrct.comb.length) continue; + + let hasAll = true; + + for (let i = 0; i < vcl; i++) { + const vc = v.comb[i]; + + if (kbdsrct.comb.includes(vc)) continue; + + hasAll = false; + break; + } + + if (!hasAll) continue; + + return v; + } +} + +export function registerKbdsrct(kbdsrct: IKbdsrct) { + if (!kbdsrct.comb.length) throw new Error("Shortcut key can't be empty"); + + // warn if the same shortcut key already registered + const dup = findRegistered(kbdsrct); + if (dup) { + console.warn( + `Shortcut with key combination '${kbdsrct.comb.join( + '+', + )}' already registered with ${ + dup.cb === kbdsrct.cb ? 'the same' : 'different' + } callback`, + ); + } + + kbdsrcts.push(kbdsrct); +} + +/** + * Returns true if successfully unregistered + */ +export function unregisterKbdsrct(kbdsrct: IKbdsrct) { + const rmIdx = kbdsrcts.findIndex((v) => v === kbdsrct); + if (rmIdx < 0) return false; + + return kbdsrcts.splice(rmIdx, 1).length > 0; +} + +/** + * Check and execute appropriate callback based on current keypresses + * Returns true when shortcut that matches keypresses combination exists + */ +export function execKbdsrct(keypresses: string[]) { + if (!keypresses.length) return false; + + let hasSrct = false; + + for (const v of kbdsrcts) { + let exec = true; + for (const k of v.comb) { + if (!keypresses.includes(k)) { + exec = false; + break; + } + } + + if (!exec) continue; + + v.cb(); + hasSrct = true; + + // remove executed keypresses + for (const k of v.comb) { + // keypresses guaranteed contains comb entries here + // else smt is wrong w ur algorithm! + const rmIdx = keypresses.findIndex((kp) => kp === k); + keypresses.splice(rmIdx, 1); + } + } + + return hasSrct; +} From 52b37254b6dec1d66293a6ba3e104418e7bd570e Mon Sep 17 00:00:00 2001 From: Neko-Life Date: Sun, 10 Dec 2023 17:35:56 +0700 Subject: [PATCH 2/6] wtf is this bs --- dashboard/src/pages/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/dashboard/src/pages/index.tsx b/dashboard/src/pages/index.tsx index 0115ce9b6..0dd74d43b 100644 --- a/dashboard/src/pages/index.tsx +++ b/dashboard/src/pages/index.tsx @@ -17,6 +17,7 @@ const Home: NextPageWithLayout = () => { css={{ overflow: 'auto', }} + responsive={false} > Discord Music Bot From 612b92a33a9feedf4d9a684d8896a85e71260f1c Mon Sep 17 00:00:00 2001 From: Neko-Life Date: Sun, 10 Dec 2023 20:27:25 +0700 Subject: [PATCH 3/6] modal shortcuts --- dashboard/src/assets/icons/modal-close.svg | 1 + dashboard/src/components/ModalShortcut.tsx | 59 +++++++++++++++++-- dashboard/src/components/shortcuts/Key.tsx | 16 +++++ .../components/shortcuts/ShortcutEntry.tsx | 28 +++++++++ dashboard/src/interfaces/components/Modal.ts | 1 + .../src/interfaces/components/Shortcuts.ts | 8 +++ dashboard/src/layouts/AppLayout.tsx | 5 +- 7 files changed, 112 insertions(+), 6 deletions(-) create mode 100644 dashboard/src/assets/icons/modal-close.svg create mode 100644 dashboard/src/components/shortcuts/Key.tsx create mode 100644 dashboard/src/components/shortcuts/ShortcutEntry.tsx create mode 100644 dashboard/src/interfaces/components/Shortcuts.ts diff --git a/dashboard/src/assets/icons/modal-close.svg b/dashboard/src/assets/icons/modal-close.svg new file mode 100644 index 000000000..87aaef7f4 --- /dev/null +++ b/dashboard/src/assets/icons/modal-close.svg @@ -0,0 +1 @@ +Close diff --git a/dashboard/src/components/ModalShortcut.tsx b/dashboard/src/components/ModalShortcut.tsx index 8c8b3a41f..5eaba5732 100644 --- a/dashboard/src/components/ModalShortcut.tsx +++ b/dashboard/src/components/ModalShortcut.tsx @@ -1,9 +1,13 @@ import Modal from '@/components/Modal'; import { IModalShortcutProps } from '@/interfaces/components/Modal'; import { Container } from '@nextui-org/react'; +import ModalCloseIcon from '@/assets/icons/modal-close.svg'; +import Key from '@/components/shortcuts/Key'; +import ShortcutEntry from './shortcuts/ShortcutEntry'; export default function ModalShortcut({ open, + onClose, modalContainerProps = {}, }: IModalShortcutProps) { return ( @@ -12,22 +16,67 @@ export default function ModalShortcut({ css={{ borderRadius: '6px', backgroundColor: '$gray50', + padding: 0, + maxWidth: 600, + boxShadow: '0px 0px 300px 0px rgba(255,255,255,0.1)', }} fluid + responsive={false} > -
-

Title

-
+
+

Keyboard Shortcuts

+ + +
+
+ Press Ctrl + / to toggle this modal. +
+ +
-

Description

+
+

Playback

+ + + +
diff --git a/dashboard/src/components/shortcuts/Key.tsx b/dashboard/src/components/shortcuts/Key.tsx new file mode 100644 index 000000000..396a4a8bc --- /dev/null +++ b/dashboard/src/components/shortcuts/Key.tsx @@ -0,0 +1,16 @@ +import { IKeyProps } from '@/interfaces/components/Shortcuts'; + +export default function Key({ children }: IKeyProps) { + return ( + + {children} + + ); +} diff --git a/dashboard/src/components/shortcuts/ShortcutEntry.tsx b/dashboard/src/components/shortcuts/ShortcutEntry.tsx new file mode 100644 index 000000000..5e1424ced --- /dev/null +++ b/dashboard/src/components/shortcuts/ShortcutEntry.tsx @@ -0,0 +1,28 @@ +import { IShortcutEntryProps } from '@/interfaces/components/Shortcuts'; +import Key from '@/components/shortcuts/Key'; + +export default function ShortcutEntry({ + name, + comb = [], +}: IShortcutEntryProps) { + return ( +
+
{name}
+
+ {comb.map((v, i) => ( + {v} + ))} +
+
+ ); +} diff --git a/dashboard/src/interfaces/components/Modal.ts b/dashboard/src/interfaces/components/Modal.ts index c800d9458..1a1ced70b 100644 --- a/dashboard/src/interfaces/components/Modal.ts +++ b/dashboard/src/interfaces/components/Modal.ts @@ -6,5 +6,6 @@ export interface IModalProps { export interface IModalShortcutProps { open?: IModalProps['open']; + onClose?: React.ComponentProps<'svg'>['onClick']; modalContainerProps?: IModalProps; } diff --git a/dashboard/src/interfaces/components/Shortcuts.ts b/dashboard/src/interfaces/components/Shortcuts.ts new file mode 100644 index 000000000..fcff82d7e --- /dev/null +++ b/dashboard/src/interfaces/components/Shortcuts.ts @@ -0,0 +1,8 @@ +export interface IKeyProps { + children?: React.ReactNode; +} + +export interface IShortcutEntryProps { + name?: React.ReactNode; + comb?: IKeyProps['children'][]; +} diff --git a/dashboard/src/layouts/AppLayout.tsx b/dashboard/src/layouts/AppLayout.tsx index 848dcddd7..3f0e1e208 100644 --- a/dashboard/src/layouts/AppLayout.tsx +++ b/dashboard/src/layouts/AppLayout.tsx @@ -120,7 +120,10 @@ const AppLayout: PageLayout = ({ })} >
{children}
- + modalShortcutOpen && setModalShortcutOpen(false)} + /> ); }; From 63707dd33bee5294a90498917db6081aee282f40 Mon Sep 17 00:00:00 2001 From: Neko-Life Date: Sun, 10 Dec 2023 20:40:12 +0700 Subject: [PATCH 4/6] untested player srct --- dashboard/src/layouts/AppLayout.tsx | 12 ---- dashboard/src/pages/servers/[id]/player.tsx | 68 +++++++++++++++------ 2 files changed, 50 insertions(+), 30 deletions(-) diff --git a/dashboard/src/layouts/AppLayout.tsx b/dashboard/src/layouts/AppLayout.tsx index 3f0e1e208..1406df0fc 100644 --- a/dashboard/src/layouts/AppLayout.tsx +++ b/dashboard/src/layouts/AppLayout.tsx @@ -40,30 +40,18 @@ const AppLayout: PageLayout = ({ * The browser might not even fire keyboard event cuz it's already captured by the OS or other app */ const kbdsrctInHandler = (e: KeyboardEvent) => { - let hasMod = false; - if (e.shiftKey) { pressesRef.current.push('Shift'); - hasMod = true; - } - - if (e.metaKey) { - hasMod = true; } if (e.ctrlKey) { pressesRef.current.push('Control'); - hasMod = true; } if (e.altKey) { pressesRef.current.push('Alt'); - hasMod = true; } - // no sane keyboard shortcut without modifier - if (!hasMod) return; - pressesRef.current.push(e.key); if (execKbdsrct(pressesRef.current)) e.preventDefault(); diff --git a/dashboard/src/pages/servers/[id]/player.tsx b/dashboard/src/pages/servers/[id]/player.tsx index 31d9f2788..d5b0c92a6 100644 --- a/dashboard/src/pages/servers/[id]/player.tsx +++ b/dashboard/src/pages/servers/[id]/player.tsx @@ -34,6 +34,7 @@ import { import Image from 'next/image'; import { getImageOnErrorHandler } from '@/utils/image'; import useAbortDelay from '@/hooks/useAbortDelay'; +import { registerKbdsrct, unregisterKbdsrct } from '@/libs/kbdsrct'; const FALLBACK_MAX_PROGRESS_VALUE = 1; const SOCKET_WAIT_RES_TIMEOUT = 3000; @@ -332,24 +333,6 @@ const Player: NextPageWithLayout = () => { }); }; - useEffect(() => { - sharedStateMount(sharedState); - seekerMount(); - - playerSocket.mount(serverId as string, { - mountHandler: { - close: handleSocketClose, - }, - eventHandler: socketEventHandlers, - }); - - return () => { - sharedStateUnmount(sharedState); - seekerUnmount(); - playerSocket.unmount(serverId as string); - }; - }, []); - const handleNavbarToggle = () => { if (!sharedState.setNavbarShow) return; if (!sharedState.navbarAbsolute) { @@ -440,6 +423,55 @@ const Player: NextPageWithLayout = () => { runNextBack(() => {}, SOCKET_WAIT_RES_TIMEOUT); }; + const spacePP = { + comb: ['Space'], + cb: togglePlayPause, + }; + + const arrowLeftPrev = { + comb: ['Alt', 'ArrowLeft'], + cb: handlePrevious, + }; + + const arrowRightNext = { + comb: ['Alt', 'ArrowRight'], + cb: handleNext, + }; + + const registerAllSrct = () => { + registerKbdsrct(spacePP); + registerKbdsrct(arrowLeftPrev); + registerKbdsrct(arrowRightNext); + }; + + const unregisterAllSrct = () => { + unregisterKbdsrct(spacePP); + unregisterKbdsrct(arrowLeftPrev); + unregisterKbdsrct(arrowRightNext); + }; + + useEffect(() => { + sharedStateMount(sharedState); + seekerMount(); + + playerSocket.mount(serverId as string, { + mountHandler: { + close: handleSocketClose, + }, + eventHandler: socketEventHandlers, + }); + + registerAllSrct(); + + return () => { + sharedStateUnmount(sharedState); + seekerUnmount(); + playerSocket.unmount(serverId as string); + + unregisterAllSrct(); + }; + }, []); + const mainImg = !playing?.thumbnail?.length ? SampleThumb.src : playing.thumbnail; From 2b3e311093d7533b330dc6206304fa580a33b4b4 Mon Sep 17 00:00:00 2001 From: Neko-Life Date: Sun, 10 Dec 2023 20:53:00 +0700 Subject: [PATCH 5/6] fix space not registering --- dashboard/src/pages/servers/[id]/player.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dashboard/src/pages/servers/[id]/player.tsx b/dashboard/src/pages/servers/[id]/player.tsx index d5b0c92a6..d3a8a7546 100644 --- a/dashboard/src/pages/servers/[id]/player.tsx +++ b/dashboard/src/pages/servers/[id]/player.tsx @@ -424,7 +424,8 @@ const Player: NextPageWithLayout = () => { }; const spacePP = { - comb: ['Space'], + // literally a space + comb: [' '], cb: togglePlayPause, }; From 713c1ea27e3bbc0c28083d8dda6fe7d0dde27bd0 Mon Sep 17 00:00:00 2001 From: Neko-Life Date: Sun, 10 Dec 2023 21:12:46 +0700 Subject: [PATCH 6/6] actually test and fix bug --- dashboard/src/pages/servers/[id]/player.tsx | 34 ++++++++++++--------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/dashboard/src/pages/servers/[id]/player.tsx b/dashboard/src/pages/servers/[id]/player.tsx index d3a8a7546..f7e353213 100644 --- a/dashboard/src/pages/servers/[id]/player.tsx +++ b/dashboard/src/pages/servers/[id]/player.tsx @@ -333,6 +333,24 @@ const Player: NextPageWithLayout = () => { }); }; + useEffect(() => { + sharedStateMount(sharedState); + seekerMount(); + + playerSocket.mount(serverId as string, { + mountHandler: { + close: handleSocketClose, + }, + eventHandler: socketEventHandlers, + }); + + return () => { + sharedStateUnmount(sharedState); + seekerUnmount(); + playerSocket.unmount(serverId as string); + }; + }, []); + const handleNavbarToggle = () => { if (!sharedState.setNavbarShow) return; if (!sharedState.navbarAbsolute) { @@ -452,26 +470,12 @@ const Player: NextPageWithLayout = () => { }; useEffect(() => { - sharedStateMount(sharedState); - seekerMount(); - - playerSocket.mount(serverId as string, { - mountHandler: { - close: handleSocketClose, - }, - eventHandler: socketEventHandlers, - }); - registerAllSrct(); return () => { - sharedStateUnmount(sharedState); - seekerUnmount(); - playerSocket.unmount(serverId as string); - unregisterAllSrct(); }; - }, []); + }, [registerAllSrct, unregisterAllSrct]); const mainImg = !playing?.thumbnail?.length ? SampleThumb.src