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.

Merge branch 'pr/46' into production

Pas ba6b4414 2884c0a6

+204 -16
+3
src/assets/locales/en.json
··· 1050 1050 "holdToBoost": "Hold to boost speed", 1051 1051 "holdToBoostDescription": "Hold spacebar or touch and hold the screen to temporarily increase playback speed to 2x. Release to return to previous speed.", 1052 1052 "holdToBoostLabel": "Enable hold to boost", 1053 + "doubleClickToSeek": "Double tap to seek", 1054 + "doubleClickToSeekDescription": "Double tap on the left or right side of the player to seek 10 seconds forward or backward.", 1055 + "doubleClickToSeekLabel": "Enable double tap to seek", 1053 1056 "sourceOrder": "Reordering sources", 1054 1057 "sourceOrderDescription": "Drag and drop to reorder sources. This will determine the order in which sources are checked for the media you are trying to watch. If a source is greyed out, it means the <bold>extension</bold> is required for that source. <br><br> <strong>(The default order is best for most users)</strong>", 1055 1058 "sourceOrderEnableLabel": "Custom source order",
+2
src/backend/accounts/settings.ts
··· 27 27 enableLowPerformanceMode?: boolean; 28 28 enableNativeSubtitles?: boolean; 29 29 enableHoldToBoost?: boolean; 30 + enableDoubleClickToSeek?: boolean; 30 31 manualSourceSelection?: boolean; 31 32 } 32 33 ··· 53 54 enableLowPerformanceMode?: boolean; 54 55 enableNativeSubtitles?: boolean; 55 56 enableHoldToBoost?: boolean; 57 + enableDoubleClickToSeek?: boolean; 56 58 manualSourceSelection?: boolean; 57 59 } 58 60
+20
src/components/player/atoms/Seek.tsx
··· 1 + import { Icon, Icons } from "@/components/Icon"; 2 + 3 + export type SeekDirection = "backward" | "forward"; 4 + 5 + export function Seek(props: { direction: SeekDirection }) { 6 + return ( 7 + <div 8 + className={`pointer-events-none flex h-20 w-20 items-center justify-center rounded-full bg-black bg-opacity-50 text-white ${ 9 + props.direction === "backward" ? "animate-seek-left" : "animate-seek-right" 10 + }`} 11 + > 12 + <Icon 13 + icon={ 14 + props.direction === "backward" ? Icons.SKIP_BACKWARD : Icons.SKIP_FORWARD 15 + } 16 + className="text-3xl" 17 + /> 18 + </div> 19 + ); 20 + }
+9 -2
src/components/player/atoms/Skips.tsx
··· 3 3 import { Icons } from "@/components/Icon"; 4 4 import { VideoPlayerButton } from "@/components/player/internals/Button"; 5 5 import { usePlayerStore } from "@/stores/player/store"; 6 + import { usePreferencesStore } from "@/stores/preferences"; 6 7 7 8 export function SkipForward(props: { 8 9 iconSizeClass?: string; ··· 10 11 }) { 11 12 const display = usePlayerStore((s) => s.display); 12 13 const time = usePlayerStore((s) => s.progress.time); 14 + const enableDoubleClickToSeek = usePreferencesStore( 15 + (s) => s.enableDoubleClickToSeek, 16 + ); 13 17 const commit = useCallback(() => { 14 18 display?.setTime(time + 10); 15 19 }, [display, time]); 16 - if (!props.inControl) return null; 20 + if (!props.inControl || enableDoubleClickToSeek) return null; 17 21 return ( 18 22 <VideoPlayerButton 19 23 iconSizeClass={props.iconSizeClass} ··· 29 33 }) { 30 34 const display = usePlayerStore((s) => s.display); 31 35 const time = usePlayerStore((s) => s.progress.time); 36 + const enableDoubleClickToSeek = usePreferencesStore( 37 + (s) => s.enableDoubleClickToSeek, 38 + ); 32 39 const commit = useCallback(() => { 33 40 display?.setTime(time - 10); 34 41 }, [display, time]); 35 - if (!props.inControl) return null; 42 + if (!props.inControl || enableDoubleClickToSeek) return null; 36 43 return ( 37 44 <VideoPlayerButton 38 45 iconSizeClass={props.iconSizeClass}
+103 -14
src/components/player/internals/VideoClickTarget.tsx
··· 1 1 import classNames from "classnames"; 2 - import { PointerEvent, useCallback, useRef, useState } from "react"; 2 + import { PointerEvent, useCallback, useEffect, useRef, useState } from "react"; 3 3 import { useEffectOnce, useTimeoutFn } from "react-use"; 4 4 5 + import { Seek, SeekDirection } from "@/components/player/atoms/Seek"; 5 6 import { useShouldShowVideoElement } from "@/components/player/internals/VideoContainer"; 6 7 import { useOverlayStack } from "@/stores/interface/overlayStack"; 7 8 import { PlayerHoverState } from "@/stores/player/slices/interface"; ··· 12 13 export function VideoClickTarget(props: { showingControls: boolean }) { 13 14 const show = useShouldShowVideoElement(); 14 15 const display = usePlayerStore((s) => s.display); 16 + const time = usePlayerStore((s) => s.progress.time); 15 17 const isPaused = usePlayerStore((s) => s.mediaPlaying.isPaused); 16 18 const playbackRate = usePlayerStore((s) => s.mediaPlaying.playbackRate); 17 19 const updateInterfaceHovering = usePlayerStore( ··· 23 25 const setCurrentOverlay = useOverlayStack((s) => s.setCurrentOverlay); 24 26 const isInWatchParty = useWatchPartyStore((s) => s.enabled); 25 27 const enableHoldToBoost = usePreferencesStore((s) => s.enableHoldToBoost); 28 + const enableDoubleClickToSeek = usePreferencesStore( 29 + (s) => s.enableDoubleClickToSeek, 30 + ); 26 31 27 32 const [_, cancel, reset] = useTimeoutFn(() => { 28 33 updateInterfaceHovering(PlayerHoverState.NOT_HOVERING); ··· 36 41 const speedIndicatorTimeoutRef = useRef<NodeJS.Timeout | null>(null); 37 42 const boostTimeoutRef = useRef<NodeJS.Timeout | null>(null); 38 43 const [isPendingBoost, setIsPendingBoost] = useState(false); 44 + const [seekDirection, setSeekDirection] = useState<SeekDirection | null>( 45 + null, 46 + ); 47 + const [seekId, setSeekId] = useState(0); 48 + const [isSeeking, setIsSeeking] = useState(false); 49 + const seekTimeoutRef = useRef<NodeJS.Timeout | null>(null); 50 + const singleTapTimeout = useRef<NodeJS.Timeout | null>(null); 39 51 40 52 const toggleFullscreen = useCallback(() => { 41 53 display?.toggleFullscreen(); 42 54 }, [display]); 43 55 56 + const handleDoubleClick = useCallback( 57 + (e: PointerEvent<HTMLDivElement>) => { 58 + if (!enableDoubleClickToSeek) { 59 + toggleFullscreen(); 60 + return; 61 + } 62 + const rect = e.currentTarget.getBoundingClientRect(); 63 + const x = e.clientX - rect.left; 64 + const oneThird = rect.width / 3; 65 + 66 + if (x < oneThird) { 67 + display?.setTime(time - 10); 68 + setSeekDirection("backward"); 69 + setSeekId((s) => s + 1); 70 + setIsSeeking(true); 71 + } else if (x > oneThird * 2) { 72 + display?.setTime(time + 10); 73 + setSeekDirection("forward"); 74 + setSeekId((s) => s + 1); 75 + setIsSeeking(true); 76 + } else { 77 + toggleFullscreen(); 78 + } 79 + }, 80 + [display, toggleFullscreen, enableDoubleClickToSeek, time], 81 + ); 82 + 83 + useEffect(() => { 84 + if (isSeeking) { 85 + if (seekTimeoutRef.current) { 86 + clearTimeout(seekTimeoutRef.current); 87 + } 88 + seekTimeoutRef.current = setTimeout(() => { 89 + setIsSeeking(false); 90 + }, 400); 91 + } 92 + }, [seekId, isSeeking]); 93 + 44 94 const togglePause = useCallback( 45 95 (e: PointerEvent<HTMLDivElement>) => { 46 96 // Don't toggle pause if holding for speed change ··· 65 115 } 66 116 67 117 // toggle on other types of clicks 118 + if (isSeeking) return; 68 119 if (hovering !== PlayerHoverState.MOBILE_TAPPED) { 69 120 updateInterfaceHovering(PlayerHoverState.MOBILE_TAPPED); 70 121 reset(); ··· 81 132 reset, 82 133 cancel, 83 134 isPendingBoost, 135 + isSeeking, 84 136 ], 85 137 ); 86 138 139 + const handleTap = useCallback( 140 + (e: PointerEvent<HTMLDivElement>) => { 141 + if (e.pointerType === "mouse" && e.button !== 0) return; 142 + 143 + if (singleTapTimeout.current) { 144 + clearTimeout(singleTapTimeout.current); 145 + singleTapTimeout.current = null; 146 + handleDoubleClick(e); 147 + } else { 148 + if (!enableDoubleClickToSeek) { 149 + togglePause(e); 150 + } 151 + singleTapTimeout.current = setTimeout(() => { 152 + if (enableDoubleClickToSeek) { 153 + togglePause(e); 154 + } 155 + singleTapTimeout.current = null; 156 + }, 250); 157 + } 158 + }, 159 + [handleDoubleClick, togglePause, enableDoubleClickToSeek], 160 + ); 161 + 87 162 const handlePointerDown = useCallback( 88 163 (e: PointerEvent<HTMLDivElement>) => { 89 164 if ( ··· 141 216 if (isPendingBoost) { 142 217 clearTimeout(boostTimeoutRef.current!); 143 218 setIsPendingBoost(false); 144 - togglePause(e); 219 + handleTap(e); 145 220 return; 146 221 } 147 222 ··· 170 245 }, 1500); 171 246 } else { 172 247 // Regular click handler 173 - togglePause(e); 248 + handleTap(e); 174 249 } 175 250 }, 176 251 [ 177 252 display, 178 - togglePause, 253 + handleTap, 179 254 setSpeedBoosted, 180 255 setShowSpeedIndicator, 181 256 setCurrentOverlay, ··· 221 296 if (!show) return null; 222 297 223 298 return ( 224 - <div 225 - className={classNames("absolute inset-0", { 226 - "absolute inset-0": true, 227 - "cursor-none": !props.showingControls, 228 - })} 229 - onDoubleClick={toggleFullscreen} 230 - onPointerDown={handlePointerDown} 231 - onPointerUp={handlePointerUp} 232 - onPointerLeave={handlePointerLeave} 233 - /> 299 + <> 300 + {seekDirection ? ( 301 + <div 302 + key={seekId} 303 + onAnimationEnd={() => setSeekDirection(null)} 304 + className={ 305 + seekDirection === "backward" 306 + ? "absolute inset-0 flex items-center justify-start ml-32" 307 + : "absolute inset-0 flex items-center justify-end mr-32" 308 + } 309 + > 310 + <Seek direction={seekDirection} /> 311 + </div> 312 + ) : null} 313 + <div 314 + className={classNames("absolute inset-0", { 315 + "absolute inset-0": true, 316 + "cursor-none": !props.showingControls, 317 + })} 318 + onPointerDown={handlePointerDown} 319 + onPointerUp={handlePointerUp} 320 + onPointerLeave={handlePointerLeave} 321 + /> 322 + </> 234 323 ); 235 324 }
+14
src/hooks/useSettingsState.ts
··· 66 66 forceCompactEpisodeView: boolean, 67 67 enableLowPerformanceMode: boolean, 68 68 enableHoldToBoost: boolean, 69 + enableDoubleClickToSeek: boolean, 69 70 homeSectionOrder: string[], 70 71 manualSourceSelection: boolean, 71 72 ) { ··· 183 184 resetEnableHoldToBoost, 184 185 enableHoldToBoostChanged, 185 186 ] = useDerived(enableHoldToBoost); 187 + const [ 188 + enableDoubleClickToSeekState, 189 + setEnableDoubleClickToSeekState, 190 + resetEnableDoubleClickToSeek, 191 + enableDoubleClickToSeekChanged, 192 + ] = useDerived(enableDoubleClickToSeek); 186 193 const [ 187 194 homeSectionOrderState, 188 195 setHomeSectionOrderState, ··· 221 228 resetForceCompactEpisodeView(); 222 229 resetEnableLowPerformanceMode(); 223 230 resetEnableHoldToBoost(); 231 + resetEnableDoubleClickToSeek(); 224 232 resetHomeSectionOrder(); 225 233 resetManualSourceSelection(); 226 234 } ··· 249 257 forceCompactEpisodeViewChanged || 250 258 enableLowPerformanceModeChanged || 251 259 enableHoldToBoostChanged || 260 + enableDoubleClickToSeekChanged || 252 261 homeSectionOrderChanged || 253 262 manualSourceSelectionChanged; 254 263 ··· 369 378 state: enableHoldToBoostState, 370 379 set: setEnableHoldToBoostState, 371 380 changed: enableHoldToBoostChanged, 381 + }, 382 + enableDoubleClickToSeek: { 383 + state: enableDoubleClickToSeekState, 384 + set: setEnableDoubleClickToSeekState, 385 + changed: enableDoubleClickToSeekChanged, 372 386 }, 373 387 homeSectionOrder: { 374 388 state: homeSectionOrderState,
+13
src/pages/Settings.tsx
··· 197 197 (s) => s.setEnableHoldToBoost, 198 198 ); 199 199 200 + const enableDoubleClickToSeek = usePreferencesStore( 201 + (s) => s.enableDoubleClickToSeek, 202 + ); 203 + const setEnableDoubleClickToSeek = usePreferencesStore( 204 + (s) => s.setEnableDoubleClickToSeek, 205 + ); 206 + 200 207 const homeSectionOrder = usePreferencesStore((s) => s.homeSectionOrder); 201 208 const setHomeSectionOrder = usePreferencesStore((s) => s.setHomeSectionOrder); 202 209 ··· 259 266 forceCompactEpisodeView, 260 267 enableLowPerformanceMode, 261 268 enableHoldToBoost, 269 + enableDoubleClickToSeek, 262 270 homeSectionOrder, 263 271 manualSourceSelection, 264 272 ); ··· 320 328 state.forceCompactEpisodeView.changed || 321 329 state.enableLowPerformanceMode.changed || 322 330 state.enableHoldToBoost.changed || 331 + state.enableDoubleClickToSeek.changed || 323 332 state.manualSourceSelection.changed 324 333 ) { 325 334 await updateSettings(backendUrl, account, { ··· 342 351 forceCompactEpisodeView: state.forceCompactEpisodeView.state, 343 352 enableLowPerformanceMode: state.enableLowPerformanceMode.state, 344 353 enableHoldToBoost: state.enableHoldToBoost.state, 354 + enableDoubleClickToSeek: state.enableDoubleClickToSeek.state, 345 355 manualSourceSelection: state.manualSourceSelection.state, 346 356 }); 347 357 } ··· 383 393 setForceCompactEpisodeView(state.forceCompactEpisodeView.state); 384 394 setEnableLowPerformanceMode(state.enableLowPerformanceMode.state); 385 395 setEnableHoldToBoost(state.enableHoldToBoost.state); 396 + setEnableDoubleClickToSeek(state.enableDoubleClickToSeek.state); 386 397 setHomeSectionOrder(state.homeSectionOrder.state); 387 398 setManualSourceSelection(state.manualSourceSelection.state); 388 399 ··· 483 494 setEnableLowPerformanceMode={state.enableLowPerformanceMode.set} 484 495 enableHoldToBoost={state.enableHoldToBoost.state} 485 496 setEnableHoldToBoost={state.enableHoldToBoost.set} 497 + enableDoubleClickToSeek={state.enableDoubleClickToSeek.state} 498 + setEnableDoubleClickToSeek={state.enableDoubleClickToSeek.set} 486 499 manualSourceSelection={state.manualSourceSelection.state} 487 500 setManualSourceSelection={state.manualSourceSelection.set} 488 501 />
+22
src/pages/parts/settings/PreferencesPart.tsx
··· 31 31 setEnableLowPerformanceMode: (v: boolean) => void; 32 32 enableHoldToBoost: boolean; 33 33 setEnableHoldToBoost: (v: boolean) => void; 34 + enableDoubleClickToSeek: boolean; 35 + setEnableDoubleClickToSeek: (v: boolean) => void; 34 36 manualSourceSelection: boolean; 35 37 setManualSourceSelection: (v: boolean) => void; 36 38 }) { ··· 215 217 {t("settings.preferences.holdToBoostLabel")} 216 218 </p> 217 219 </div> 220 + </div> 221 + </div> 222 + {/* double click to seek preference */} 223 + <div> 224 + <p className="text-white font-bold mb-3"> 225 + {t("settings.preferences.doubleClickToSeek")} 226 + </p> 227 + <p className="max-w-[25rem] font-medium"> 228 + {t("settings.preferences.doubleClickToSeekDescription")} 229 + </p> 230 + <div 231 + onClick={() => 232 + props.setEnableDoubleClickToSeek(!props.enableDoubleClickToSeek) 233 + } 234 + className="bg-dropdown-background hover:bg-dropdown-hoverBackground select-none my-4 cursor-pointer space-x-3 flex items-center max-w-[25rem] py-3 px-4 rounded-lg" 235 + > 236 + <Toggle enabled={props.enableDoubleClickToSeek} /> 237 + <p className="flex-1 text-white font-bold"> 238 + {t("settings.preferences.doubleClickToSeekLabel")} 239 + </p> 218 240 </div> 219 241 </div> 220 242
+8
src/stores/preferences/index.tsx
··· 22 22 enableLowPerformanceMode: boolean; 23 23 enableNativeSubtitles: boolean; 24 24 enableHoldToBoost: boolean; 25 + enableDoubleClickToSeek: boolean; 25 26 homeSectionOrder: string[]; 26 27 manualSourceSelection: boolean; 27 28 ··· 44 45 setEnableLowPerformanceMode(v: boolean): void; 45 46 setEnableNativeSubtitles(v: boolean): void; 46 47 setEnableHoldToBoost(v: boolean): void; 48 + setEnableDoubleClickToSeek(v: boolean): void; 47 49 setHomeSectionOrder(v: string[]): void; 48 50 setManualSourceSelection(v: boolean): void; 49 51 } ··· 70 72 enableLowPerformanceMode: false, 71 73 enableNativeSubtitles: false, 72 74 enableHoldToBoost: true, 75 + enableDoubleClickToSeek: false, 73 76 homeSectionOrder: ["watching", "bookmarks"], 74 77 manualSourceSelection: false, 75 78 setEnableThumbnails(v) { ··· 165 168 setEnableHoldToBoost(v) { 166 169 set((s) => { 167 170 s.enableHoldToBoost = v; 171 + }); 172 + }, 173 + setEnableDoubleClickToSeek(v) { 174 + set((s) => { 175 + s.enableDoubleClickToSeek = v; 168 176 }); 169 177 }, 170 178 setHomeSectionOrder(v) {
+10
tailwind.config.ts
··· 33 33 "0%": { opacity: "0" }, 34 34 "100%": { opacity: "1" }, 35 35 }, 36 + "seek-left": { 37 + "0%": { transform: "translateX(0) scale(1)", opacity: "1" }, 38 + "100%": { transform: "translateX(-50px) scale(1.2)", opacity: "0" }, 39 + }, 40 + "seek-right": { 41 + "0%": { transform: "translateX(0) scale(1)", opacity: "1" }, 42 + "100%": { transform: "translateX(50px) scale(1.2)", opacity: "0" }, 43 + }, 36 44 }, 37 45 animation: { 38 46 "loading-pin": "loading-pin 1.8s ease-in-out infinite", 39 47 "fade-in": "fade-in 200ms ease-out forwards", 48 + "seek-left": "seek-left 0.5s cubic-bezier(0, 0, 0.2, 1) forwards", 49 + "seek-right": "seek-right 0.5s cubic-bezier(0, 0, 0.2, 1) forwards", 40 50 }, 41 51 }, 42 52 },