mirror of
https://github.com/p-stream/p-stream.git
synced 2026-04-21 12:02:18 +00:00
add double-tap to seek feature
This commit is contained in:
parent
bf774106ff
commit
c240ccefd5
10 changed files with 204 additions and 16 deletions
|
|
@ -1050,6 +1050,9 @@
|
||||||
"holdToBoost": "Hold to boost speed",
|
"holdToBoost": "Hold to boost speed",
|
||||||
"holdToBoostDescription": "Hold spacebar or touch and hold the screen to temporarily increase playback speed to 2x. Release to return to previous speed.",
|
"holdToBoostDescription": "Hold spacebar or touch and hold the screen to temporarily increase playback speed to 2x. Release to return to previous speed.",
|
||||||
"holdToBoostLabel": "Enable hold to boost",
|
"holdToBoostLabel": "Enable hold to boost",
|
||||||
|
"doubleClickToSeek": "Double tap to seek",
|
||||||
|
"doubleClickToSeekDescription": "Double tap on the left or right side of the player to seek 10 seconds forward or backward.",
|
||||||
|
"doubleClickToSeekLabel": "Enable double tap to seek",
|
||||||
"sourceOrder": "Reordering sources",
|
"sourceOrder": "Reordering sources",
|
||||||
"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>",
|
"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>",
|
||||||
"sourceOrderEnableLabel": "Custom source order",
|
"sourceOrderEnableLabel": "Custom source order",
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ export interface SettingsInput {
|
||||||
enableLowPerformanceMode?: boolean;
|
enableLowPerformanceMode?: boolean;
|
||||||
enableNativeSubtitles?: boolean;
|
enableNativeSubtitles?: boolean;
|
||||||
enableHoldToBoost?: boolean;
|
enableHoldToBoost?: boolean;
|
||||||
|
enableDoubleClickToSeek?: boolean;
|
||||||
manualSourceSelection?: boolean;
|
manualSourceSelection?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,6 +54,7 @@ export interface SettingsResponse {
|
||||||
enableLowPerformanceMode?: boolean;
|
enableLowPerformanceMode?: boolean;
|
||||||
enableNativeSubtitles?: boolean;
|
enableNativeSubtitles?: boolean;
|
||||||
enableHoldToBoost?: boolean;
|
enableHoldToBoost?: boolean;
|
||||||
|
enableDoubleClickToSeek?: boolean;
|
||||||
manualSourceSelection?: boolean;
|
manualSourceSelection?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
20
src/components/player/atoms/Seek.tsx
Normal file
20
src/components/player/atoms/Seek.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
|
|
||||||
|
export type SeekDirection = "backward" | "forward";
|
||||||
|
|
||||||
|
export function Seek(props: { direction: SeekDirection }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`pointer-events-none flex h-20 w-20 items-center justify-center rounded-full bg-black bg-opacity-50 text-white ${
|
||||||
|
props.direction === "backward" ? "animate-seek-left" : "animate-seek-right"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon={
|
||||||
|
props.direction === "backward" ? Icons.SKIP_BACKWARD : Icons.SKIP_FORWARD
|
||||||
|
}
|
||||||
|
className="text-3xl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ import { useCallback } from "react";
|
||||||
import { Icons } from "@/components/Icon";
|
import { Icons } from "@/components/Icon";
|
||||||
import { VideoPlayerButton } from "@/components/player/internals/Button";
|
import { VideoPlayerButton } from "@/components/player/internals/Button";
|
||||||
import { usePlayerStore } from "@/stores/player/store";
|
import { usePlayerStore } from "@/stores/player/store";
|
||||||
|
import { usePreferencesStore } from "@/stores/preferences";
|
||||||
|
|
||||||
export function SkipForward(props: {
|
export function SkipForward(props: {
|
||||||
iconSizeClass?: string;
|
iconSizeClass?: string;
|
||||||
|
|
@ -10,10 +11,13 @@ export function SkipForward(props: {
|
||||||
}) {
|
}) {
|
||||||
const display = usePlayerStore((s) => s.display);
|
const display = usePlayerStore((s) => s.display);
|
||||||
const time = usePlayerStore((s) => s.progress.time);
|
const time = usePlayerStore((s) => s.progress.time);
|
||||||
|
const enableDoubleClickToSeek = usePreferencesStore(
|
||||||
|
(s) => s.enableDoubleClickToSeek,
|
||||||
|
);
|
||||||
const commit = useCallback(() => {
|
const commit = useCallback(() => {
|
||||||
display?.setTime(time + 10);
|
display?.setTime(time + 10);
|
||||||
}, [display, time]);
|
}, [display, time]);
|
||||||
if (!props.inControl) return null;
|
if (!props.inControl || enableDoubleClickToSeek) return null;
|
||||||
return (
|
return (
|
||||||
<VideoPlayerButton
|
<VideoPlayerButton
|
||||||
iconSizeClass={props.iconSizeClass}
|
iconSizeClass={props.iconSizeClass}
|
||||||
|
|
@ -29,10 +33,13 @@ export function SkipBackward(props: {
|
||||||
}) {
|
}) {
|
||||||
const display = usePlayerStore((s) => s.display);
|
const display = usePlayerStore((s) => s.display);
|
||||||
const time = usePlayerStore((s) => s.progress.time);
|
const time = usePlayerStore((s) => s.progress.time);
|
||||||
|
const enableDoubleClickToSeek = usePreferencesStore(
|
||||||
|
(s) => s.enableDoubleClickToSeek,
|
||||||
|
);
|
||||||
const commit = useCallback(() => {
|
const commit = useCallback(() => {
|
||||||
display?.setTime(time - 10);
|
display?.setTime(time - 10);
|
||||||
}, [display, time]);
|
}, [display, time]);
|
||||||
if (!props.inControl) return null;
|
if (!props.inControl || enableDoubleClickToSeek) return null;
|
||||||
return (
|
return (
|
||||||
<VideoPlayerButton
|
<VideoPlayerButton
|
||||||
iconSizeClass={props.iconSizeClass}
|
iconSizeClass={props.iconSizeClass}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { PointerEvent, useCallback, useRef, useState } from "react";
|
import { PointerEvent, useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { useEffectOnce, useTimeoutFn } from "react-use";
|
import { useEffectOnce, useTimeoutFn } from "react-use";
|
||||||
|
|
||||||
|
import { Seek, SeekDirection } from "@/components/player/atoms/Seek";
|
||||||
import { useShouldShowVideoElement } from "@/components/player/internals/VideoContainer";
|
import { useShouldShowVideoElement } from "@/components/player/internals/VideoContainer";
|
||||||
import { useOverlayStack } from "@/stores/interface/overlayStack";
|
import { useOverlayStack } from "@/stores/interface/overlayStack";
|
||||||
import { PlayerHoverState } from "@/stores/player/slices/interface";
|
import { PlayerHoverState } from "@/stores/player/slices/interface";
|
||||||
|
|
@ -12,6 +13,7 @@ import { useWatchPartyStore } from "@/stores/watchParty";
|
||||||
export function VideoClickTarget(props: { showingControls: boolean }) {
|
export function VideoClickTarget(props: { showingControls: boolean }) {
|
||||||
const show = useShouldShowVideoElement();
|
const show = useShouldShowVideoElement();
|
||||||
const display = usePlayerStore((s) => s.display);
|
const display = usePlayerStore((s) => s.display);
|
||||||
|
const time = usePlayerStore((s) => s.progress.time);
|
||||||
const isPaused = usePlayerStore((s) => s.mediaPlaying.isPaused);
|
const isPaused = usePlayerStore((s) => s.mediaPlaying.isPaused);
|
||||||
const playbackRate = usePlayerStore((s) => s.mediaPlaying.playbackRate);
|
const playbackRate = usePlayerStore((s) => s.mediaPlaying.playbackRate);
|
||||||
const updateInterfaceHovering = usePlayerStore(
|
const updateInterfaceHovering = usePlayerStore(
|
||||||
|
|
@ -23,6 +25,9 @@ export function VideoClickTarget(props: { showingControls: boolean }) {
|
||||||
const setCurrentOverlay = useOverlayStack((s) => s.setCurrentOverlay);
|
const setCurrentOverlay = useOverlayStack((s) => s.setCurrentOverlay);
|
||||||
const isInWatchParty = useWatchPartyStore((s) => s.enabled);
|
const isInWatchParty = useWatchPartyStore((s) => s.enabled);
|
||||||
const enableHoldToBoost = usePreferencesStore((s) => s.enableHoldToBoost);
|
const enableHoldToBoost = usePreferencesStore((s) => s.enableHoldToBoost);
|
||||||
|
const enableDoubleClickToSeek = usePreferencesStore(
|
||||||
|
(s) => s.enableDoubleClickToSeek,
|
||||||
|
);
|
||||||
|
|
||||||
const [_, cancel, reset] = useTimeoutFn(() => {
|
const [_, cancel, reset] = useTimeoutFn(() => {
|
||||||
updateInterfaceHovering(PlayerHoverState.NOT_HOVERING);
|
updateInterfaceHovering(PlayerHoverState.NOT_HOVERING);
|
||||||
|
|
@ -36,11 +41,56 @@ export function VideoClickTarget(props: { showingControls: boolean }) {
|
||||||
const speedIndicatorTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const speedIndicatorTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const boostTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const boostTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const [isPendingBoost, setIsPendingBoost] = useState(false);
|
const [isPendingBoost, setIsPendingBoost] = useState(false);
|
||||||
|
const [seekDirection, setSeekDirection] = useState<SeekDirection | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const [seekId, setSeekId] = useState(0);
|
||||||
|
const [isSeeking, setIsSeeking] = useState(false);
|
||||||
|
const seekTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const singleTapTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
const toggleFullscreen = useCallback(() => {
|
const toggleFullscreen = useCallback(() => {
|
||||||
display?.toggleFullscreen();
|
display?.toggleFullscreen();
|
||||||
}, [display]);
|
}, [display]);
|
||||||
|
|
||||||
|
const handleDoubleClick = useCallback(
|
||||||
|
(e: PointerEvent<HTMLDivElement>) => {
|
||||||
|
if (!enableDoubleClickToSeek) {
|
||||||
|
toggleFullscreen();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rect = e.currentTarget.getBoundingClientRect();
|
||||||
|
const x = e.clientX - rect.left;
|
||||||
|
const oneThird = rect.width / 3;
|
||||||
|
|
||||||
|
if (x < oneThird) {
|
||||||
|
display?.setTime(time - 10);
|
||||||
|
setSeekDirection("backward");
|
||||||
|
setSeekId((s) => s + 1);
|
||||||
|
setIsSeeking(true);
|
||||||
|
} else if (x > oneThird * 2) {
|
||||||
|
display?.setTime(time + 10);
|
||||||
|
setSeekDirection("forward");
|
||||||
|
setSeekId((s) => s + 1);
|
||||||
|
setIsSeeking(true);
|
||||||
|
} else {
|
||||||
|
toggleFullscreen();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[display, toggleFullscreen, enableDoubleClickToSeek, time],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSeeking) {
|
||||||
|
if (seekTimeoutRef.current) {
|
||||||
|
clearTimeout(seekTimeoutRef.current);
|
||||||
|
}
|
||||||
|
seekTimeoutRef.current = setTimeout(() => {
|
||||||
|
setIsSeeking(false);
|
||||||
|
}, 400);
|
||||||
|
}
|
||||||
|
}, [seekId, isSeeking]);
|
||||||
|
|
||||||
const togglePause = useCallback(
|
const togglePause = useCallback(
|
||||||
(e: PointerEvent<HTMLDivElement>) => {
|
(e: PointerEvent<HTMLDivElement>) => {
|
||||||
// Don't toggle pause if holding for speed change
|
// Don't toggle pause if holding for speed change
|
||||||
|
|
@ -65,6 +115,7 @@ export function VideoClickTarget(props: { showingControls: boolean }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// toggle on other types of clicks
|
// toggle on other types of clicks
|
||||||
|
if (isSeeking) return;
|
||||||
if (hovering !== PlayerHoverState.MOBILE_TAPPED) {
|
if (hovering !== PlayerHoverState.MOBILE_TAPPED) {
|
||||||
updateInterfaceHovering(PlayerHoverState.MOBILE_TAPPED);
|
updateInterfaceHovering(PlayerHoverState.MOBILE_TAPPED);
|
||||||
reset();
|
reset();
|
||||||
|
|
@ -81,9 +132,33 @@ export function VideoClickTarget(props: { showingControls: boolean }) {
|
||||||
reset,
|
reset,
|
||||||
cancel,
|
cancel,
|
||||||
isPendingBoost,
|
isPendingBoost,
|
||||||
|
isSeeking,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleTap = useCallback(
|
||||||
|
(e: PointerEvent<HTMLDivElement>) => {
|
||||||
|
if (e.pointerType === "mouse" && e.button !== 0) return;
|
||||||
|
|
||||||
|
if (singleTapTimeout.current) {
|
||||||
|
clearTimeout(singleTapTimeout.current);
|
||||||
|
singleTapTimeout.current = null;
|
||||||
|
handleDoubleClick(e);
|
||||||
|
} else {
|
||||||
|
if (!enableDoubleClickToSeek) {
|
||||||
|
togglePause(e);
|
||||||
|
}
|
||||||
|
singleTapTimeout.current = setTimeout(() => {
|
||||||
|
if (enableDoubleClickToSeek) {
|
||||||
|
togglePause(e);
|
||||||
|
}
|
||||||
|
singleTapTimeout.current = null;
|
||||||
|
}, 250);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[handleDoubleClick, togglePause, enableDoubleClickToSeek],
|
||||||
|
);
|
||||||
|
|
||||||
const handlePointerDown = useCallback(
|
const handlePointerDown = useCallback(
|
||||||
(e: PointerEvent<HTMLDivElement>) => {
|
(e: PointerEvent<HTMLDivElement>) => {
|
||||||
if (
|
if (
|
||||||
|
|
@ -141,7 +216,7 @@ export function VideoClickTarget(props: { showingControls: boolean }) {
|
||||||
if (isPendingBoost) {
|
if (isPendingBoost) {
|
||||||
clearTimeout(boostTimeoutRef.current!);
|
clearTimeout(boostTimeoutRef.current!);
|
||||||
setIsPendingBoost(false);
|
setIsPendingBoost(false);
|
||||||
togglePause(e);
|
handleTap(e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -170,12 +245,12 @@ export function VideoClickTarget(props: { showingControls: boolean }) {
|
||||||
}, 1500);
|
}, 1500);
|
||||||
} else {
|
} else {
|
||||||
// Regular click handler
|
// Regular click handler
|
||||||
togglePause(e);
|
handleTap(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
display,
|
display,
|
||||||
togglePause,
|
handleTap,
|
||||||
setSpeedBoosted,
|
setSpeedBoosted,
|
||||||
setShowSpeedIndicator,
|
setShowSpeedIndicator,
|
||||||
setCurrentOverlay,
|
setCurrentOverlay,
|
||||||
|
|
@ -221,15 +296,29 @@ export function VideoClickTarget(props: { showingControls: boolean }) {
|
||||||
if (!show) return null;
|
if (!show) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<>
|
||||||
className={classNames("absolute inset-0", {
|
{seekDirection ? (
|
||||||
"absolute inset-0": true,
|
<div
|
||||||
"cursor-none": !props.showingControls,
|
key={seekId}
|
||||||
})}
|
onAnimationEnd={() => setSeekDirection(null)}
|
||||||
onDoubleClick={toggleFullscreen}
|
className={
|
||||||
onPointerDown={handlePointerDown}
|
seekDirection === "backward"
|
||||||
onPointerUp={handlePointerUp}
|
? "absolute inset-0 flex items-center justify-start ml-32"
|
||||||
onPointerLeave={handlePointerLeave}
|
: "absolute inset-0 flex items-center justify-end mr-32"
|
||||||
/>
|
}
|
||||||
|
>
|
||||||
|
<Seek direction={seekDirection} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div
|
||||||
|
className={classNames("absolute inset-0", {
|
||||||
|
"absolute inset-0": true,
|
||||||
|
"cursor-none": !props.showingControls,
|
||||||
|
})}
|
||||||
|
onPointerDown={handlePointerDown}
|
||||||
|
onPointerUp={handlePointerUp}
|
||||||
|
onPointerLeave={handlePointerLeave}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,7 @@ export function useSettingsState(
|
||||||
forceCompactEpisodeView: boolean,
|
forceCompactEpisodeView: boolean,
|
||||||
enableLowPerformanceMode: boolean,
|
enableLowPerformanceMode: boolean,
|
||||||
enableHoldToBoost: boolean,
|
enableHoldToBoost: boolean,
|
||||||
|
enableDoubleClickToSeek: boolean,
|
||||||
homeSectionOrder: string[],
|
homeSectionOrder: string[],
|
||||||
manualSourceSelection: boolean,
|
manualSourceSelection: boolean,
|
||||||
) {
|
) {
|
||||||
|
|
@ -183,6 +184,12 @@ export function useSettingsState(
|
||||||
resetEnableHoldToBoost,
|
resetEnableHoldToBoost,
|
||||||
enableHoldToBoostChanged,
|
enableHoldToBoostChanged,
|
||||||
] = useDerived(enableHoldToBoost);
|
] = useDerived(enableHoldToBoost);
|
||||||
|
const [
|
||||||
|
enableDoubleClickToSeekState,
|
||||||
|
setEnableDoubleClickToSeekState,
|
||||||
|
resetEnableDoubleClickToSeek,
|
||||||
|
enableDoubleClickToSeekChanged,
|
||||||
|
] = useDerived(enableDoubleClickToSeek);
|
||||||
const [
|
const [
|
||||||
homeSectionOrderState,
|
homeSectionOrderState,
|
||||||
setHomeSectionOrderState,
|
setHomeSectionOrderState,
|
||||||
|
|
@ -221,6 +228,7 @@ export function useSettingsState(
|
||||||
resetForceCompactEpisodeView();
|
resetForceCompactEpisodeView();
|
||||||
resetEnableLowPerformanceMode();
|
resetEnableLowPerformanceMode();
|
||||||
resetEnableHoldToBoost();
|
resetEnableHoldToBoost();
|
||||||
|
resetEnableDoubleClickToSeek();
|
||||||
resetHomeSectionOrder();
|
resetHomeSectionOrder();
|
||||||
resetManualSourceSelection();
|
resetManualSourceSelection();
|
||||||
}
|
}
|
||||||
|
|
@ -249,6 +257,7 @@ export function useSettingsState(
|
||||||
forceCompactEpisodeViewChanged ||
|
forceCompactEpisodeViewChanged ||
|
||||||
enableLowPerformanceModeChanged ||
|
enableLowPerformanceModeChanged ||
|
||||||
enableHoldToBoostChanged ||
|
enableHoldToBoostChanged ||
|
||||||
|
enableDoubleClickToSeekChanged ||
|
||||||
homeSectionOrderChanged ||
|
homeSectionOrderChanged ||
|
||||||
manualSourceSelectionChanged;
|
manualSourceSelectionChanged;
|
||||||
|
|
||||||
|
|
@ -370,6 +379,11 @@ export function useSettingsState(
|
||||||
set: setEnableHoldToBoostState,
|
set: setEnableHoldToBoostState,
|
||||||
changed: enableHoldToBoostChanged,
|
changed: enableHoldToBoostChanged,
|
||||||
},
|
},
|
||||||
|
enableDoubleClickToSeek: {
|
||||||
|
state: enableDoubleClickToSeekState,
|
||||||
|
set: setEnableDoubleClickToSeekState,
|
||||||
|
changed: enableDoubleClickToSeekChanged,
|
||||||
|
},
|
||||||
homeSectionOrder: {
|
homeSectionOrder: {
|
||||||
state: homeSectionOrderState,
|
state: homeSectionOrderState,
|
||||||
set: setHomeSectionOrderState,
|
set: setHomeSectionOrderState,
|
||||||
|
|
|
||||||
|
|
@ -197,6 +197,13 @@ export function SettingsPage() {
|
||||||
(s) => s.setEnableHoldToBoost,
|
(s) => s.setEnableHoldToBoost,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const enableDoubleClickToSeek = usePreferencesStore(
|
||||||
|
(s) => s.enableDoubleClickToSeek,
|
||||||
|
);
|
||||||
|
const setEnableDoubleClickToSeek = usePreferencesStore(
|
||||||
|
(s) => s.setEnableDoubleClickToSeek,
|
||||||
|
);
|
||||||
|
|
||||||
const homeSectionOrder = usePreferencesStore((s) => s.homeSectionOrder);
|
const homeSectionOrder = usePreferencesStore((s) => s.homeSectionOrder);
|
||||||
const setHomeSectionOrder = usePreferencesStore((s) => s.setHomeSectionOrder);
|
const setHomeSectionOrder = usePreferencesStore((s) => s.setHomeSectionOrder);
|
||||||
|
|
||||||
|
|
@ -259,6 +266,7 @@ export function SettingsPage() {
|
||||||
forceCompactEpisodeView,
|
forceCompactEpisodeView,
|
||||||
enableLowPerformanceMode,
|
enableLowPerformanceMode,
|
||||||
enableHoldToBoost,
|
enableHoldToBoost,
|
||||||
|
enableDoubleClickToSeek,
|
||||||
homeSectionOrder,
|
homeSectionOrder,
|
||||||
manualSourceSelection,
|
manualSourceSelection,
|
||||||
);
|
);
|
||||||
|
|
@ -320,6 +328,7 @@ export function SettingsPage() {
|
||||||
state.forceCompactEpisodeView.changed ||
|
state.forceCompactEpisodeView.changed ||
|
||||||
state.enableLowPerformanceMode.changed ||
|
state.enableLowPerformanceMode.changed ||
|
||||||
state.enableHoldToBoost.changed ||
|
state.enableHoldToBoost.changed ||
|
||||||
|
state.enableDoubleClickToSeek.changed ||
|
||||||
state.manualSourceSelection.changed
|
state.manualSourceSelection.changed
|
||||||
) {
|
) {
|
||||||
await updateSettings(backendUrl, account, {
|
await updateSettings(backendUrl, account, {
|
||||||
|
|
@ -342,6 +351,7 @@ export function SettingsPage() {
|
||||||
forceCompactEpisodeView: state.forceCompactEpisodeView.state,
|
forceCompactEpisodeView: state.forceCompactEpisodeView.state,
|
||||||
enableLowPerformanceMode: state.enableLowPerformanceMode.state,
|
enableLowPerformanceMode: state.enableLowPerformanceMode.state,
|
||||||
enableHoldToBoost: state.enableHoldToBoost.state,
|
enableHoldToBoost: state.enableHoldToBoost.state,
|
||||||
|
enableDoubleClickToSeek: state.enableDoubleClickToSeek.state,
|
||||||
manualSourceSelection: state.manualSourceSelection.state,
|
manualSourceSelection: state.manualSourceSelection.state,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -383,6 +393,7 @@ export function SettingsPage() {
|
||||||
setForceCompactEpisodeView(state.forceCompactEpisodeView.state);
|
setForceCompactEpisodeView(state.forceCompactEpisodeView.state);
|
||||||
setEnableLowPerformanceMode(state.enableLowPerformanceMode.state);
|
setEnableLowPerformanceMode(state.enableLowPerformanceMode.state);
|
||||||
setEnableHoldToBoost(state.enableHoldToBoost.state);
|
setEnableHoldToBoost(state.enableHoldToBoost.state);
|
||||||
|
setEnableDoubleClickToSeek(state.enableDoubleClickToSeek.state);
|
||||||
setHomeSectionOrder(state.homeSectionOrder.state);
|
setHomeSectionOrder(state.homeSectionOrder.state);
|
||||||
setManualSourceSelection(state.manualSourceSelection.state);
|
setManualSourceSelection(state.manualSourceSelection.state);
|
||||||
|
|
||||||
|
|
@ -483,6 +494,8 @@ export function SettingsPage() {
|
||||||
setEnableLowPerformanceMode={state.enableLowPerformanceMode.set}
|
setEnableLowPerformanceMode={state.enableLowPerformanceMode.set}
|
||||||
enableHoldToBoost={state.enableHoldToBoost.state}
|
enableHoldToBoost={state.enableHoldToBoost.state}
|
||||||
setEnableHoldToBoost={state.enableHoldToBoost.set}
|
setEnableHoldToBoost={state.enableHoldToBoost.set}
|
||||||
|
enableDoubleClickToSeek={state.enableDoubleClickToSeek.state}
|
||||||
|
setEnableDoubleClickToSeek={state.enableDoubleClickToSeek.set}
|
||||||
manualSourceSelection={state.manualSourceSelection.state}
|
manualSourceSelection={state.manualSourceSelection.state}
|
||||||
setManualSourceSelection={state.manualSourceSelection.set}
|
setManualSourceSelection={state.manualSourceSelection.set}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,8 @@ export function PreferencesPart(props: {
|
||||||
setEnableLowPerformanceMode: (v: boolean) => void;
|
setEnableLowPerformanceMode: (v: boolean) => void;
|
||||||
enableHoldToBoost: boolean;
|
enableHoldToBoost: boolean;
|
||||||
setEnableHoldToBoost: (v: boolean) => void;
|
setEnableHoldToBoost: (v: boolean) => void;
|
||||||
|
enableDoubleClickToSeek: boolean;
|
||||||
|
setEnableDoubleClickToSeek: (v: boolean) => void;
|
||||||
manualSourceSelection: boolean;
|
manualSourceSelection: boolean;
|
||||||
setManualSourceSelection: (v: boolean) => void;
|
setManualSourceSelection: (v: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
|
|
@ -217,6 +219,26 @@ export function PreferencesPart(props: {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/* double click to seek preference */}
|
||||||
|
<div>
|
||||||
|
<p className="text-white font-bold mb-3">
|
||||||
|
{t("settings.preferences.doubleClickToSeek")}
|
||||||
|
</p>
|
||||||
|
<p className="max-w-[25rem] font-medium">
|
||||||
|
{t("settings.preferences.doubleClickToSeekDescription")}
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
onClick={() =>
|
||||||
|
props.setEnableDoubleClickToSeek(!props.enableDoubleClickToSeek)
|
||||||
|
}
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<Toggle enabled={props.enableDoubleClickToSeek} />
|
||||||
|
<p className="flex-1 text-white font-bold">
|
||||||
|
{t("settings.preferences.doubleClickToSeekLabel")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Column */}
|
{/* Column */}
|
||||||
<div id="source-order" className="space-y-8">
|
<div id="source-order" className="space-y-8">
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ export interface PreferencesStore {
|
||||||
enableLowPerformanceMode: boolean;
|
enableLowPerformanceMode: boolean;
|
||||||
enableNativeSubtitles: boolean;
|
enableNativeSubtitles: boolean;
|
||||||
enableHoldToBoost: boolean;
|
enableHoldToBoost: boolean;
|
||||||
|
enableDoubleClickToSeek: boolean;
|
||||||
homeSectionOrder: string[];
|
homeSectionOrder: string[];
|
||||||
manualSourceSelection: boolean;
|
manualSourceSelection: boolean;
|
||||||
|
|
||||||
|
|
@ -44,6 +45,7 @@ export interface PreferencesStore {
|
||||||
setEnableLowPerformanceMode(v: boolean): void;
|
setEnableLowPerformanceMode(v: boolean): void;
|
||||||
setEnableNativeSubtitles(v: boolean): void;
|
setEnableNativeSubtitles(v: boolean): void;
|
||||||
setEnableHoldToBoost(v: boolean): void;
|
setEnableHoldToBoost(v: boolean): void;
|
||||||
|
setEnableDoubleClickToSeek(v: boolean): void;
|
||||||
setHomeSectionOrder(v: string[]): void;
|
setHomeSectionOrder(v: string[]): void;
|
||||||
setManualSourceSelection(v: boolean): void;
|
setManualSourceSelection(v: boolean): void;
|
||||||
}
|
}
|
||||||
|
|
@ -70,6 +72,7 @@ export const usePreferencesStore = create(
|
||||||
enableLowPerformanceMode: false,
|
enableLowPerformanceMode: false,
|
||||||
enableNativeSubtitles: false,
|
enableNativeSubtitles: false,
|
||||||
enableHoldToBoost: true,
|
enableHoldToBoost: true,
|
||||||
|
enableDoubleClickToSeek: false,
|
||||||
homeSectionOrder: ["watching", "bookmarks"],
|
homeSectionOrder: ["watching", "bookmarks"],
|
||||||
manualSourceSelection: false,
|
manualSourceSelection: false,
|
||||||
setEnableThumbnails(v) {
|
setEnableThumbnails(v) {
|
||||||
|
|
@ -167,6 +170,11 @@ export const usePreferencesStore = create(
|
||||||
s.enableHoldToBoost = v;
|
s.enableHoldToBoost = v;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
setEnableDoubleClickToSeek(v) {
|
||||||
|
set((s) => {
|
||||||
|
s.enableDoubleClickToSeek = v;
|
||||||
|
});
|
||||||
|
},
|
||||||
setHomeSectionOrder(v) {
|
setHomeSectionOrder(v) {
|
||||||
set((s) => {
|
set((s) => {
|
||||||
s.homeSectionOrder = v;
|
s.homeSectionOrder = v;
|
||||||
|
|
|
||||||
|
|
@ -33,10 +33,20 @@ const config: Config = {
|
||||||
"0%": { opacity: "0" },
|
"0%": { opacity: "0" },
|
||||||
"100%": { opacity: "1" },
|
"100%": { opacity: "1" },
|
||||||
},
|
},
|
||||||
|
"seek-left": {
|
||||||
|
"0%": { transform: "translateX(0) scale(1)", opacity: "1" },
|
||||||
|
"100%": { transform: "translateX(-50px) scale(1.2)", opacity: "0" },
|
||||||
|
},
|
||||||
|
"seek-right": {
|
||||||
|
"0%": { transform: "translateX(0) scale(1)", opacity: "1" },
|
||||||
|
"100%": { transform: "translateX(50px) scale(1.2)", opacity: "0" },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
animation: {
|
animation: {
|
||||||
"loading-pin": "loading-pin 1.8s ease-in-out infinite",
|
"loading-pin": "loading-pin 1.8s ease-in-out infinite",
|
||||||
"fade-in": "fade-in 200ms ease-out forwards",
|
"fade-in": "fade-in 200ms ease-out forwards",
|
||||||
|
"seek-left": "seek-left 0.5s cubic-bezier(0, 0, 0.2, 1) forwards",
|
||||||
|
"seek-right": "seek-right 0.5s cubic-bezier(0, 0, 0.2, 1) forwards",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue