From df2a53e0109a260675ad097084d4611fdbd9ea35 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 21 Mar 2026 10:15:22 +0200 Subject: [PATCH] feat(video): group less common options into a dropdown menu --- .../src/translations/en/translation.json | 3 +- .../src/widgets/type_widgets/file/Video.tsx | 115 ++++++++++++++---- 2 files changed, 91 insertions(+), 27 deletions(-) diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index f0b5e7a479..13982e1a75 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -1055,7 +1055,8 @@ "exit-fullscreen": "Exit fullscreen", "unsupported-format": "Media preview is not available for this file format:\n{{mime}}", "zoom-to-fit": "Zoom to fill", - "zoom-reset": "Reset zoom to fill" + "zoom-reset": "Reset zoom to fill", + "more-options": "More options" }, "protected_session": { "enter_password_instruction": "Showing protected note requires entering your password:", diff --git a/apps/client/src/widgets/type_widgets/file/Video.tsx b/apps/client/src/widgets/type_widgets/file/Video.tsx index 180b8ce4db..fd4b10a3a7 100644 --- a/apps/client/src/widgets/type_widgets/file/Video.tsx +++ b/apps/client/src/widgets/type_widgets/file/Video.tsx @@ -7,8 +7,11 @@ import FNote from "../../../entities/fnote"; import { t } from "../../../services/i18n"; import { getUrlForDownload } from "../../../services/open"; import ActionButton from "../../react/ActionButton"; +import Dropdown from "../../react/Dropdown"; +import { FormListHeader, FormListItem } from "../../react/FormList"; +import Icon from "../../react/Icon"; import NoItems from "../../react/NoItems"; -import { LoopButton, PlaybackSpeed, PlayPauseButton, SeekBar, SkipButton, VolumeControl } from "./MediaPlayer"; +import { PlayPauseButton, SeekBar, SkipButton, VolumeControl } from "./MediaPlayer"; const AUTO_HIDE_DELAY = 3000; @@ -67,19 +70,17 @@ export function VideoPreviewContent({ url, mime }: { url: string, mime: string }
- - +
- +
-
@@ -179,8 +180,49 @@ function useAutoHideControls(videoRef: RefObject, playing: boo return { visible, onMouseMove, flash: onMouseMove }; } -function RotateButton({ videoRef }: { videoRef: RefObject }) { +const PLAYBACK_SPEEDS = [0.5, 1, 1.25, 1.5, 2]; + +function OverflowMenu({ videoRef }: { videoRef: RefObject }) { + const [speed, setSpeed] = useState(() => videoRef.current?.playbackRate ?? 1); + const [loop, setLoop] = useState(() => videoRef.current?.loop ?? false); const [rotation, setRotation] = useState(0); + const [fitted, setFitted] = useState(false); + + // Sync playback rate + useEffect(() => { + const video = videoRef.current; + if (!video) return; + + setSpeed(video.playbackRate); + const onRateChange = () => setSpeed(video.playbackRate); + video.addEventListener("ratechange", onRateChange); + return () => video.removeEventListener("ratechange", onRateChange); + }, [videoRef]); + + // Sync loop state + useEffect(() => { + const video = videoRef.current; + if (!video) return; + setLoop(video.loop); + + const observer = new MutationObserver(() => setLoop(video.loop)); + observer.observe(video, { attributes: true, attributeFilter: ["loop"] }); + return () => observer.disconnect(); + }, [videoRef]); + + const selectSpeed = (rate: number) => { + const video = videoRef.current; + if (!video) return; + video.playbackRate = rate; + setSpeed(rate); + }; + + const toggleLoop = () => { + const video = videoRef.current; + if (!video) return; + video.loop = !video.loop; + setLoop(video.loop); + }; const rotate = () => { const video = videoRef.current; @@ -190,7 +232,6 @@ function RotateButton({ videoRef }: { videoRef: RefObject }) { const isSideways = next === 90 || next === 270; if (isSideways) { - // Scale down so the rotated video fits within its container. const container = video.parentElement; if (container) { const ratio = container.clientWidth / container.clientHeight; @@ -203,19 +244,7 @@ function RotateButton({ videoRef }: { videoRef: RefObject }) { } }; - return ( - - ); -} - -function ZoomToFitButton({ videoRef }: { videoRef: RefObject }) { - const [fitted, setFitted] = useState(false); - - const toggle = () => { + const toggleFit = () => { const video = videoRef.current; if (!video) return; const next = !fitted; @@ -224,12 +253,46 @@ function ZoomToFitButton({ videoRef }: { videoRef: RefObject } }; return ( - + } + title={t("media.more-options")} + > + + {PLAYBACK_SPEEDS.map((rate) => ( + selectSpeed(rate)} + > + {rate}x + + ))} +