diff --git a/src/components/features/DialogAddVideo/DialogAddVideo.tsx b/src/components/features/DialogAddVideo/DialogAddVideo.tsx index 8cf21314f1..89318c3d32 100644 --- a/src/components/features/DialogAddVideo/DialogAddVideo.tsx +++ b/src/components/features/DialogAddVideo/DialogAddVideo.tsx @@ -14,7 +14,6 @@ import { ButtonSubmitAdd } from "./ButtonSumbitAdd"; import { DialogAddMediaStatuses } from "./enums"; import { LoadingScreens } from "./LoadingScreens"; -// import { useRenderCounter } from "~/hooks/useRenderCounter"; import { ApiResponse } from "~/Classes/ApiManager/ApiManager"; import { MediaFile } from "~/Classes/ApiManager/models/MediaFile"; @@ -97,7 +96,7 @@ export const DialogAddVideo = ({ onFileStaged={(file) => { setStates((curr) => ({ ...curr, file })); }} - trimData={trimData} + trimDataSignal={trimData} onTrimChange={(newTrimData: TrimData) => { trimData.value = newTrimData; }} diff --git a/src/components/features/DialogAddVideo/QuickTrimVideoUploader.tsx b/src/components/features/DialogAddVideo/QuickTrimVideoUploader.tsx index 247aee868a..d089f06397 100644 --- a/src/components/features/DialogAddVideo/QuickTrimVideoUploader.tsx +++ b/src/components/features/DialogAddVideo/QuickTrimVideoUploader.tsx @@ -1,5 +1,5 @@ -import { useCallback, useState } from "react"; -import { Signal } from "@preact/signals-react"; +import { useCallback, useState, useRef } from "react"; +import { signal, Signal } from "@preact/signals-react"; import { FileUploader } from "../FileUploader"; import { VideoControls } from "./VideoControls"; @@ -14,15 +14,14 @@ export const QuickTrimVideoUploader = ({ file, onFileStaged, onTrimChange, - trimData, + trimDataSignal, }: { file: File | null; onFileStaged: (newFile: File | null) => void; onTrimChange: (trimData: TrimData) => void; - trimData?: Signal; + trimDataSignal: Signal; }) => { const [vidEl, setVidEl] = useState(undefined); - const videoRefCallback = useCallback( (node: HTMLVideoElement) => { if (node !== null) { @@ -62,10 +61,14 @@ export const QuickTrimVideoUploader = ({ src={URL.createObjectURL(file)} /> -
- +
+ void; +}) => { + const handleOnClick = () => { + if (trimDurationMs === 6000) { + onChange(3000); + return; + } + if (trimDurationMs === 3000) { + onChange(1000); + return; + } + onChange(6000); + return; + }; + return ( + + + + ); +}; diff --git a/src/components/features/DialogAddVideo/TrimmerPlaybar/PlayProgressCursor.tsx b/src/components/features/DialogAddVideo/TrimmerPlaybar/PlayProgressCursor.tsx index 048e8385bf..6c95e42b20 100644 --- a/src/components/features/DialogAddVideo/TrimmerPlaybar/PlayProgressCursor.tsx +++ b/src/components/features/DialogAddVideo/TrimmerPlaybar/PlayProgressCursor.tsx @@ -92,7 +92,7 @@ export const PlayProgressCursor = ({ className={twMerge( verticalPositionStyles, buttonStyles, - "size-5 -translate-x-1/2 rounded-full", + "z-20 size-5 -translate-x-1/2 rounded-full", isScrubbing && "cursor-grabbing", )} onMouseDown={handleScrubbingCurrentTime} diff --git a/src/components/features/DialogAddVideo/TrimmerPlaybar/TrimAreaScrubber.tsx b/src/components/features/DialogAddVideo/TrimmerPlaybar/TrimAreaScrubber.tsx new file mode 100644 index 0000000000..e3c604e57c --- /dev/null +++ b/src/components/features/DialogAddVideo/TrimmerPlaybar/TrimAreaScrubber.tsx @@ -0,0 +1,171 @@ +import { useCallback, useEffect, useState, useRef } from "react"; +import { twMerge } from "tailwind-merge"; +import { faGripDots } from "@fortawesome/pro-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { formatSecondsToHHMMSSCS, ONE_MIN } from "./utilities"; +import { Tooltip } from "~/components/ui"; + +export const TrimAreaScrubber = ({ + className, + onChange, + trimStartMs, + trimDurationMs, + totalDurationMs, +}: { + className?: string; + onChange: (newPosMs: number) => void; + trimStartMs: number; + trimDurationMs: number; + totalDurationMs: number; +}) => { + const trimPosPercent = (trimStartMs / totalDurationMs) * 100; + + const [state, setState] = useState<{ + isScrubbing: boolean; + scrubPosPercent: number; + }>({ + isScrubbing: false, + scrubPosPercent: trimPosPercent, + }); + const { isScrubbing, scrubPosPercent } = state; + const containerRef = useRef(null); + const scrubberRef = useRef(null); + const mountCallback = useCallback((node: HTMLDivElement) => { + if (node) { + scrubberRef.current = node; + containerRef.current = node.parentElement; + } + }, []); + const scrubPosPercentRef = useRef(scrubPosPercent); + + const handleScrubbing = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + + if (containerRef.current === null || scrubberRef.current === null) { + return; + } + + const calcPosition = makeTrimPositionCalculator({ + parentEl: containerRef.current, + mouseOffset: + e.clientX - + scrubberRef.current.offsetLeft - + containerRef.current.offsetLeft, + trimDurationMs: trimDurationMs, + totalDurationMs: totalDurationMs, + }); + + setState({ + isScrubbing: true, + scrubPosPercent: calcPosition(e), + }); + + const handleMouseMove = (e: MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + setState({ + isScrubbing: true, + scrubPosPercent: calcPosition(e), + }); + }; + const handleMouseUp = (e: MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + setState((prev) => { + scrubPosPercentRef.current = prev.scrubPosPercent; + return { + ...prev, + isScrubbing: false, + }; + }); + + window.removeEventListener("mousemove", handleMouseMove); + window.removeEventListener("mouseup", handleMouseUp); + }; + window.addEventListener("mousemove", handleMouseMove); + window.addEventListener("mouseup", handleMouseUp); + }, + [trimDurationMs, totalDurationMs], + ); + + useEffect(() => { + if (!isScrubbing) { + const newTrimMs = (scrubPosPercentRef.current / 100) * totalDurationMs; + onChange(newTrimMs); + } + }, [isScrubbing]); + + // this determines if trimmer is its taking value + //from parent's control or user's mouse movement + const positionPercent = isScrubbing ? scrubPosPercent : trimPosPercent; + const leftOffset = `${positionPercent}%`; + // the rest are values that dervied from that position + const trimStartTime = formatSecondsToHHMMSSCS( + (positionPercent / 100) * totalDurationMs, + ); + const displayedTrimStartTime = + totalDurationMs >= ONE_MIN * 60 + ? trimStartTime.substring(3, 8) + : totalDurationMs >= ONE_MIN + ? trimStartTime.substring(0, 5) + : trimStartTime.substring(3, 8); + const tip = `Trim ${Math.round(trimDurationMs / 1000)}s from ${displayedTrimStartTime}`; + + return ( + +
+ +
+
+ ); +}; + +function makeTrimPositionCalculator({ + parentEl, + mouseOffset, + trimDurationMs, + totalDurationMs, +}: { + parentEl: HTMLElement; + mouseOffset: number; + trimDurationMs: number; + totalDurationMs: number; +}) { + const parentWidth = parentEl.getBoundingClientRect().width; + const maxMs = + totalDurationMs - trimDurationMs > 0 ? totalDurationMs - trimDurationMs : 0; + const maxPercent = Math.floor((maxMs / totalDurationMs) * 10000) / 100; + + return (currE: MouseEvent | React.MouseEvent) => { + const result = + ((currE.clientX - mouseOffset - parentEl.offsetLeft) / parentWidth) * 100; + + if (result < 0) { + return 0; + } + if (result > maxPercent) { + return maxPercent; + } + return result; + }; +} diff --git a/src/components/features/DialogAddVideo/TrimmerPlaybar/TrimmerPlaybarCore.tsx b/src/components/features/DialogAddVideo/TrimmerPlaybar/TrimmerPlaybarCore.tsx index 7cda417477..eb833d91fa 100644 --- a/src/components/features/DialogAddVideo/TrimmerPlaybar/TrimmerPlaybarCore.tsx +++ b/src/components/features/DialogAddVideo/TrimmerPlaybar/TrimmerPlaybarCore.tsx @@ -1,15 +1,11 @@ -import { useCallback, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { twMerge } from "tailwind-merge"; -import { - faBracketCurly, - faBracketCurlyRight, -} from "@fortawesome/pro-solid-svg-icons"; - -import { MAX_TRIM_DURATION } from "./utilities"; +import { ButtonTrimDuration } from "./ButtonTrimDuration"; import { TrimmingPlaybarLoading } from "./TrimmerPlaybarLoading"; import { PlayProgressCursor } from "./PlayProgressCursor"; -import { TrimScrubber } from "./TrimScrubber"; +import { TrimAreaScrubber } from "./TrimAreaScrubber"; + import { TrimData } from "./utilities"; export const TrimmerPlaybarCore = ({ @@ -28,18 +24,24 @@ export const TrimmerPlaybarCore = ({ currentTimeMs: number | undefined; trimStartMs: number | undefined; trimEndMs: number | undefined; + trimDurationMs: number; }>({ durationMs: undefined, currentTimeMs: undefined, trimStartMs: undefined, trimEndMs: undefined, + trimDurationMs: 6000, }); + const { durationMs, currentTimeMs, trimStartMs, trimEndMs, trimDurationMs } = + states; - const { durationMs, currentTimeMs, trimStartMs, trimEndMs } = states; + const handleChangeTrimDuration = (newTrimDurationMs: number) => { + setStates((curr) => ({ ...curr, trimDurationMs: newTrimDurationMs })); + }; - const setTrimStartMs = useCallback((newTrimMs: number) => { + const setTrimStartMs = (newTrimMs: number) => { setStates((prev) => { - if (prev.trimEndMs === undefined) { + if (prev.trimEndMs === undefined || prev.durationMs === undefined) { if (import.meta.env.DEV) { console.warn("Logical Error in Trim Start Setting"); } @@ -49,30 +51,42 @@ export const TrimmerPlaybarCore = ({ ...prev, trimStartMs: newTrimMs, trimEndMs: - prev.trimEndMs - newTrimMs >= MAX_TRIM_DURATION - ? newTrimMs + MAX_TRIM_DURATION - : prev.trimEndMs, + trimDurationMs >= prev.durationMs + ? prev.durationMs + : newTrimMs + trimDurationMs, }; }); - }, []); - const setTrimEndMs = useCallback((newTrimMs: number) => { + }; + + useEffect(() => { setStates((prev) => { - if (prev.trimStartMs === undefined) { - if (import.meta.env.DEV) { - console.warn("Logical Error in Trim End Setting"); - } + if (prev.trimStartMs === undefined || prev.durationMs === undefined) { + // video is not ready, no data return prev; } + // normal case + if (prev.trimStartMs + trimDurationMs <= prev.durationMs) { + return { + ...prev, + trimEndMs: prev.trimStartMs + trimDurationMs, + }; + } + // not enough duration to fit + if (trimDurationMs >= prev.durationMs) { + return { + ...prev, + trimStartMs: 0, + trimEndMs: prev.durationMs, + }; + } + // shimmy the area to left return { ...prev, - trimEndMs: newTrimMs, - trimStartMs: - newTrimMs - prev.trimStartMs >= MAX_TRIM_DURATION - ? newTrimMs - MAX_TRIM_DURATION - : prev.trimStartMs, + trimStartMs: prev.durationMs - trimDurationMs, + trimEndMs: prev.durationMs, }; }); - }, []); + }, [trimDurationMs]); useEffect(() => { if (trimStartMs !== undefined && trimEndMs !== undefined) { @@ -89,8 +103,8 @@ export const TrimmerPlaybarCore = ({ trimStartMs: trimData?.trimStartMs ?? 0, trimEndMs: trimData?.trimEndMs ? trimData.trimEndMs - : vidEl.duration * 1000 >= MAX_TRIM_DURATION - ? MAX_TRIM_DURATION + : vidEl.duration * 1000 >= trimDurationMs + ? trimDurationMs : vidEl.duration * 1000, })); }; @@ -120,35 +134,30 @@ export const TrimmerPlaybarCore = ({ } return ( -
-
- - - +
+
+ + trimDurationMs ? trimDurationMs : durationMs + } + totalDurationMs={durationMs} + onChange={setTrimStartMs} + /> +
+
); diff --git a/src/components/features/DialogAddVideo/TrimmerPlaybar/index.tsx b/src/components/features/DialogAddVideo/TrimmerPlaybar/index.tsx index 6f3fae4d9d..5bd6a420ba 100644 --- a/src/components/features/DialogAddVideo/TrimmerPlaybar/index.tsx +++ b/src/components/features/DialogAddVideo/TrimmerPlaybar/index.tsx @@ -1,16 +1,17 @@ +import { Signal } from "@preact/signals-react"; import { TrimmerPlaybarCore } from "./TrimmerPlaybarCore"; import { TrimmingPlaybarLoading } from "./TrimmerPlaybarLoading"; import { TrimData } from "./utilities"; export type { TrimData }; export const TrimmerPlaybar = ({ vidEl, - trimData, + trimDataSignal, className, onTrimChange, }: { vidEl: HTMLVideoElement | undefined; className?: string; - trimData?: TrimData; + trimDataSignal: Signal; onTrimChange: (trimData: TrimData) => void; }) => { if (!vidEl) { @@ -20,7 +21,7 @@ export const TrimmerPlaybar = ({ ); diff --git a/src/components/features/DialogAddVideo/TrimmerPlaybar/utilities.ts b/src/components/features/DialogAddVideo/TrimmerPlaybar/utilities.ts index 234876ab7a..9cf7773d7b 100644 --- a/src/components/features/DialogAddVideo/TrimmerPlaybar/utilities.ts +++ b/src/components/features/DialogAddVideo/TrimmerPlaybar/utilities.ts @@ -1,6 +1,7 @@ export const buttonStyles = "border border-ui-border bg-ui-panel cursor-grab"; export const verticalPositionStyles = "absolute top-1/2 -translate-y-1/2"; export const MAX_TRIM_DURATION = 6000; +export const ONE_MIN = 1000 * 60; export enum MouseState { IDLE = "idle", DRAGGING = "dragging", @@ -10,10 +11,15 @@ export type TrimData = { trimEndMs: number; }; -export function formatSecondsToHHMMSSCS(seconds: number) { - //example of the ISO String: 1970-01-01T00:01:40.774Z - const isoString = new Date(Math.round(seconds * 1000)).toISOString(); - if (seconds > 3600) +export function formatSecondsToHHMMSSCS(msArg: number) { + //example of the ISO String: 1970-01-01T 00:01:40.77 4Z + // index count=01234567890 12345678901 23 + const milisecond = isNaN(msArg) ? 0 : msArg; + if (isNaN(milisecond) && import.meta.env.DEV) { + console.warn("formatSecondsToHHMMSSCS recieved a NaN"); + } + const isoString = new Date(Math.round(milisecond)).toISOString(); + if (milisecond >= ONE_MIN * 60) return isoString.substring(11, 19) + "." + isoString.substring(20, 22); else return isoString.substring(14, 19) + "." + isoString.substring(20, 22); } diff --git a/src/components/features/DialogAddVideo/VideoControls/ButtonMute.tsx b/src/components/features/DialogAddVideo/VideoControls/ButtonMute.tsx index 70490f2129..e4ec3ff977 100644 --- a/src/components/features/DialogAddVideo/VideoControls/ButtonMute.tsx +++ b/src/components/features/DialogAddVideo/VideoControls/ButtonMute.tsx @@ -3,7 +3,7 @@ import { useEffect, useState } from "react"; import { faVolumeSlash, faVolume } from "@fortawesome/pro-solid-svg-icons"; import { Button } from "~/components/ui"; - +import { Tooltip } from "~/components/ui"; export function ButtonMute({ vidEl }: { vidEl: HTMLVideoElement }) { const [isMuted, setIsMuted] = useState(vidEl.muted || false); @@ -26,11 +26,13 @@ export function ButtonMute({ vidEl }: { vidEl: HTMLVideoElement }) { }, [vidEl]); return ( -