pstream is dead; long live pstream taciturnaxolotl.github.io/pstream-ng/
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

add keyboard shortcut modal

Pas 3a31d172 99d3e11b

+292 -4
+37
src/assets/locales/en.json
··· 237 237 "settings": "Settings", 238 238 "migration": "Migrate Account", 239 239 "jip": "Jip" 240 + }, 241 + "keyboardShortcuts": { 242 + "title": "Keyboard Shortcuts", 243 + "subtitle": "Hold ` to show this help anytime", 244 + "groups": { 245 + "videoPlayback": "Video Playback", 246 + "jumpToPosition": "Jump to Position", 247 + "audioVideo": "Audio & Video", 248 + "subtitlesAccessibility": "Subtitles & Accessibility", 249 + "interface": "Interface" 250 + }, 251 + "shortcuts": { 252 + "playPause": "Play/pause (or hold for 2x speed when enabled)", 253 + "playPauseAlt": "Play/pause", 254 + "skipForward5": "Skip forward 5 seconds", 255 + "skipBackward5": "Skip backward 5 seconds", 256 + "skipBackward10": "Skip backward 10 seconds", 257 + "skipForward10": "Skip forward 10 seconds", 258 + "skipForward1": "Skip forward 1 second (when paused)", 259 + "skipBackward1": "Skip backward 1 second (when paused)", 260 + "jumpTo0": "Jump to 0% (beginning)", 261 + "jumpTo9": "Jump to 90%", 262 + "increaseVolume": "Increase volume", 263 + "decreaseVolume": "Decrease volume", 264 + "mute": "Mute/unmute", 265 + "changeSpeed": "Increase/decrease playback speed", 266 + "toggleFullscreen": "Toggle fullscreen", 267 + "toggleCaptions": "Toggle captions", 268 + "syncSubtitlesEarlier": "Sync subtitles earlier (-0.5s)", 269 + "syncSubtitlesLater": "Sync subtitles later (+0.5s)", 270 + "barrelRoll": "Do a barrel roll! 🌀", 271 + "closeOverlay": "Close overlay/modal" 272 + }, 273 + "conditions": { 274 + "notInWatchParty": "Not in watch party", 275 + "whenPaused": "When paused" 276 + } 240 277 } 241 278 }, 242 279 "home": {
+200
src/components/overlays/KeyboardCommandsModal.tsx
··· 1 + import { ReactNode } from "react"; 2 + import { useTranslation } from "react-i18next"; 3 + 4 + import { Modal, ModalCard } from "@/components/overlays/Modal"; 5 + import { Heading2 } from "@/components/utils/Text"; 6 + 7 + interface KeyboardShortcut { 8 + key: string; 9 + description: string; 10 + condition?: string; 11 + } 12 + 13 + interface ShortcutGroup { 14 + title: string; 15 + shortcuts: KeyboardShortcut[]; 16 + } 17 + 18 + const getShortcutGroups = (t: (key: string) => string): ShortcutGroup[] => [ 19 + { 20 + title: t("global.keyboardShortcuts.groups.videoPlayback"), 21 + shortcuts: [ 22 + { 23 + key: "Space", 24 + description: t("global.keyboardShortcuts.shortcuts.playPause"), 25 + }, 26 + { 27 + key: "K", 28 + description: t("global.keyboardShortcuts.shortcuts.playPauseAlt"), 29 + }, 30 + { 31 + key: "→", 32 + description: t("global.keyboardShortcuts.shortcuts.skipForward5"), 33 + }, 34 + { 35 + key: "←", 36 + description: t("global.keyboardShortcuts.shortcuts.skipBackward5"), 37 + }, 38 + { 39 + key: "J", 40 + description: t("global.keyboardShortcuts.shortcuts.skipBackward10"), 41 + }, 42 + { 43 + key: "L", 44 + description: t("global.keyboardShortcuts.shortcuts.skipForward10"), 45 + }, 46 + { 47 + key: ".", 48 + description: t("global.keyboardShortcuts.shortcuts.skipForward1"), 49 + condition: t("global.keyboardShortcuts.conditions.whenPaused"), 50 + }, 51 + { 52 + key: ",", 53 + description: t("global.keyboardShortcuts.shortcuts.skipBackward1"), 54 + condition: t("global.keyboardShortcuts.conditions.whenPaused"), 55 + }, 56 + ], 57 + }, 58 + { 59 + title: t("global.keyboardShortcuts.groups.jumpToPosition"), 60 + shortcuts: [ 61 + { 62 + key: "0", 63 + description: t("global.keyboardShortcuts.shortcuts.jumpTo0"), 64 + }, 65 + { 66 + key: "9", 67 + description: t("global.keyboardShortcuts.shortcuts.jumpTo9"), 68 + }, 69 + ], 70 + }, 71 + { 72 + title: t("global.keyboardShortcuts.groups.audioVideo"), 73 + shortcuts: [ 74 + { 75 + key: "↑", 76 + description: t("global.keyboardShortcuts.shortcuts.increaseVolume"), 77 + }, 78 + { 79 + key: "↓", 80 + description: t("global.keyboardShortcuts.shortcuts.decreaseVolume"), 81 + }, 82 + { key: "M", description: t("global.keyboardShortcuts.shortcuts.mute") }, 83 + { 84 + key: ">/", 85 + description: t("global.keyboardShortcuts.shortcuts.changeSpeed"), 86 + condition: t("global.keyboardShortcuts.conditions.notInWatchParty"), 87 + }, 88 + { 89 + key: "F", 90 + description: t("global.keyboardShortcuts.shortcuts.toggleFullscreen"), 91 + }, 92 + ], 93 + }, 94 + { 95 + title: t("global.keyboardShortcuts.groups.subtitlesAccessibility"), 96 + shortcuts: [ 97 + { 98 + key: "C", 99 + description: t("global.keyboardShortcuts.shortcuts.toggleCaptions"), 100 + }, 101 + { 102 + key: "[", 103 + description: t( 104 + "global.keyboardShortcuts.shortcuts.syncSubtitlesEarlier", 105 + ), 106 + }, 107 + { 108 + key: "]", 109 + description: t("global.keyboardShortcuts.shortcuts.syncSubtitlesLater"), 110 + }, 111 + ], 112 + }, 113 + { 114 + title: t("global.keyboardShortcuts.groups.interface"), 115 + shortcuts: [ 116 + { 117 + key: "R", 118 + description: t("global.keyboardShortcuts.shortcuts.barrelRoll"), 119 + }, 120 + { 121 + key: "Escape", 122 + description: t("global.keyboardShortcuts.shortcuts.closeOverlay"), 123 + }, 124 + ], 125 + }, 126 + ]; 127 + 128 + function KeyBadge({ children }: { children: ReactNode }) { 129 + return ( 130 + <kbd className="inline-flex items-center justify-center min-w-[2rem] h-8 px-2 text-sm font-mono bg-gray-800 text-gray-200 rounded border border-gray-600 shadow-sm"> 131 + {children} 132 + </kbd> 133 + ); 134 + } 135 + 136 + interface KeyboardCommandsModalProps { 137 + id: string; 138 + } 139 + 140 + export function KeyboardCommandsModal({ id }: KeyboardCommandsModalProps) { 141 + const { t } = useTranslation(); 142 + const shortcutGroups = getShortcutGroups(t); 143 + 144 + return ( 145 + <Modal id={id}> 146 + <ModalCard> 147 + <div className="space-y-6"> 148 + <div className="text-center"> 149 + <Heading2 className="!mt-0 !mb-2"> 150 + {t("global.keyboardShortcuts.title")} 151 + </Heading2> 152 + <p className="text-type-secondary text-lg"> 153 + {(() => { 154 + const subtitle = t("global.keyboardShortcuts.subtitle"); 155 + const [before, after] = subtitle.split("`"); 156 + return ( 157 + <> 158 + {before} 159 + <KeyBadge>`</KeyBadge> 160 + {after} 161 + </> 162 + ); 163 + })()} 164 + </p> 165 + </div> 166 + 167 + <div className="space-y-6 max-h-[60vh] overflow-y-auto"> 168 + {shortcutGroups.map((group) => ( 169 + <div key={group.title} className="space-y-3"> 170 + <h3 className="text-lg font-semibold text-white border-b border-gray-700 pb-2"> 171 + {group.title} 172 + </h3> 173 + <div className="space-y-2"> 174 + {group.shortcuts.map((shortcut) => ( 175 + <div 176 + key={shortcut.key} 177 + className="flex items-center justify-between py-1" 178 + > 179 + <div className="flex items-center gap-3"> 180 + <KeyBadge>{shortcut.key}</KeyBadge> 181 + <span className="text-type-secondary"> 182 + {shortcut.description} 183 + </span> 184 + </div> 185 + {shortcut.condition && ( 186 + <span className="text-xs text-gray-400 italic"> 187 + {shortcut.condition} 188 + </span> 189 + )} 190 + </div> 191 + ))} 192 + </div> 193 + </div> 194 + ))} 195 + </div> 196 + </div> 197 + </ModalCard> 198 + </Modal> 199 + ); 200 + }
+53 -4
src/hooks/useGlobalKeyboardEvents.ts
··· 1 - import { useEffect } from "react"; 1 + import { useCallback, useEffect, useRef } from "react"; 2 2 3 3 import { useOverlayStack } from "@/stores/interface/overlayStack"; 4 4 ··· 7 7 * Handles Escape key to close modals and other global shortcuts. 8 8 */ 9 9 export function useGlobalKeyboardEvents() { 10 - const { getTopModal, hideModal } = useOverlayStack(); 10 + const { getTopModal, hideModal, showModal } = useOverlayStack(); 11 + const holdTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(); 12 + const isKeyHeldRef = useRef<boolean>(false); 13 + 14 + const showKeyboardCommands = useCallback(() => { 15 + showModal("keyboard-commands"); 16 + }, [showModal]); 17 + 18 + const hideKeyboardCommands = useCallback(() => { 19 + hideModal("keyboard-commands"); 20 + }, [hideModal]); 11 21 12 22 useEffect(() => { 13 23 const handleKeyDown = (event: KeyboardEvent) => { ··· 17 27 (event.target as HTMLInputElement).nodeName === "INPUT" 18 28 ) { 19 29 return; 30 + } 31 + 32 + // Handle backtick (`) key hold for keyboard commands 33 + if (event.key === "`") { 34 + // Prevent default browser behavior (console opening in some browsers) 35 + event.preventDefault(); 36 + 37 + if (!isKeyHeldRef.current) { 38 + isKeyHeldRef.current = true; 39 + 40 + // Show modal after 500ms hold 41 + holdTimeoutRef.current = setTimeout(() => { 42 + showKeyboardCommands(); 43 + }, 500); 44 + } 20 45 } 21 46 22 47 // Handle Escape key to close modals ··· 28 53 } 29 54 }; 30 55 31 - // Add event listener to document for global coverage 56 + const handleKeyUp = (event: KeyboardEvent) => { 57 + if (event.key === "`") { 58 + // Clear the hold timeout if key is released before modal shows 59 + if (holdTimeoutRef.current) { 60 + clearTimeout(holdTimeoutRef.current); 61 + holdTimeoutRef.current = undefined; 62 + } 63 + 64 + // Hide modal if it was shown 65 + if (isKeyHeldRef.current) { 66 + hideKeyboardCommands(); 67 + } 68 + 69 + isKeyHeldRef.current = false; 70 + } 71 + }; 72 + 73 + // Add event listeners to document for global coverage 32 74 document.addEventListener("keydown", handleKeyDown); 75 + document.addEventListener("keyup", handleKeyUp); 33 76 34 77 return () => { 35 78 document.removeEventListener("keydown", handleKeyDown); 79 + document.removeEventListener("keyup", handleKeyUp); 80 + 81 + // Clean up any pending timeouts 82 + if (holdTimeoutRef.current) { 83 + clearTimeout(holdTimeoutRef.current); 84 + } 36 85 }; 37 - }, [getTopModal, hideModal]); 86 + }, [getTopModal, hideModal, showKeyboardCommands, hideKeyboardCommands]); 38 87 }
+2
src/setup/App.tsx
··· 11 11 12 12 import { convertLegacyUrl, isLegacyUrl } from "@/backend/metadata/getmeta"; 13 13 import { generateQuickSearchMediaUrl } from "@/backend/metadata/tmdb"; 14 + import { KeyboardCommandsModal } from "@/components/overlays/KeyboardCommandsModal"; 14 15 import { NotificationModal } from "@/components/overlays/notificationsModal"; 15 16 import { useGlobalKeyboardEvents } from "@/hooks/useGlobalKeyboardEvents"; 16 17 import { useOnlineListener } from "@/hooks/usePing"; ··· 121 122 <Layout> 122 123 <LanguageProvider /> 123 124 <NotificationModal id="notifications" /> 125 + <KeyboardCommandsModal id="keyboard-commands" /> 124 126 {!showDowntime && ( 125 127 <Routes> 126 128 {/* functional routes */}