From d2afcbb98df21ffd37bd20ed7b695080d307a50e Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 11 Mar 2026 19:10:18 +0200 Subject: [PATCH] feat(audio): introduce volume slider --- .../src/widgets/type_widgets/file/Audio.tsx | 8 ++- .../widgets/type_widgets/file/MediaPlayer.css | 11 ++++ .../widgets/type_widgets/file/MediaPlayer.tsx | 59 +++++++++++++++++ .../src/widgets/type_widgets/file/Video.css | 11 ---- .../src/widgets/type_widgets/file/Video.tsx | 63 +------------------ 5 files changed, 79 insertions(+), 73 deletions(-) diff --git a/apps/client/src/widgets/type_widgets/file/Audio.tsx b/apps/client/src/widgets/type_widgets/file/Audio.tsx index 623b4efbfc..99de10caf2 100644 --- a/apps/client/src/widgets/type_widgets/file/Audio.tsx +++ b/apps/client/src/widgets/type_widgets/file/Audio.tsx @@ -2,7 +2,7 @@ import { useRef, useState } from "preact/hooks"; import FNote from "../../../entities/fnote"; import { getUrlForDownload } from "../../../services/open"; -import { PlayPauseButton, SeekBar } from "./MediaPlayer"; +import { PlayPauseButton, SeekBar, VolumeControl } from "./MediaPlayer"; export default function AudioPreview({ note }: { note: FNote }) { const [playing, setPlaying] = useState(false); @@ -21,9 +21,15 @@ export default function AudioPreview({ note }: { note: FNote }) {
+
+
+ +
+ +
diff --git a/apps/client/src/widgets/type_widgets/file/MediaPlayer.css b/apps/client/src/widgets/type_widgets/file/MediaPlayer.css index 9e7c5dbdc6..f387b41ebd 100644 --- a/apps/client/src/widgets/type_widgets/file/MediaPlayer.css +++ b/apps/client/src/widgets/type_widgets/file/MediaPlayer.css @@ -55,4 +55,15 @@ cursor: pointer; } } + + .media-volume-row { + display: flex; + align-items: center; + gap: 0.25em; + + .media-volume-slider { + width: 80px; + cursor: pointer; + } + } } diff --git a/apps/client/src/widgets/type_widgets/file/MediaPlayer.tsx b/apps/client/src/widgets/type_widgets/file/MediaPlayer.tsx index efdcfb8948..376984ca5c 100644 --- a/apps/client/src/widgets/type_widgets/file/MediaPlayer.tsx +++ b/apps/client/src/widgets/type_widgets/file/MediaPlayer.tsx @@ -75,3 +75,62 @@ export function PlayPauseButton({ mediaRef, playing }: { mediaRef: RefObject ); } + +export function VolumeControl({ mediaRef }: { mediaRef: RefObject }) { + const [volume, setVolume] = useState(() => mediaRef.current?.volume ?? 1); + const [muted, setMuted] = useState(() => mediaRef.current?.muted ?? false); + + // Sync state when the video element changes volume externally. + useEffect(() => { + const media = mediaRef.current; + if (!media) return; + + setVolume(media.volume); + setMuted(media.muted); + + const onVolumeChange = () => { + setVolume(media.volume); + setMuted(media.muted); + }; + media.addEventListener("volumechange", onVolumeChange); + return () => media.removeEventListener("volumechange", onVolumeChange); + }, []); + + const onVolumeChange = (e: Event) => { + const media = mediaRef.current; + if (!media) return; + const val = parseFloat((e.target as HTMLInputElement).value); + media.volume = val; + setVolume(val); + if (val > 0 && media.muted) { + media.muted = false; + setMuted(false); + } + }; + + const toggleMute = () => { + const media = mediaRef.current; + if (!media) return; + media.muted = !media.muted; + setMuted(media.muted); + }; + + return ( +
+ + +
+ ); +} diff --git a/apps/client/src/widgets/type_widgets/file/Video.css b/apps/client/src/widgets/type_widgets/file/Video.css index 7d2516cab0..9224c02752 100644 --- a/apps/client/src/widgets/type_widgets/file/Video.css +++ b/apps/client/src/widgets/type_widgets/file/Video.css @@ -19,17 +19,6 @@ } } - .video-volume-row { - display: flex; - align-items: center; - gap: 0.25em; - } - - .video-volume-slider { - width: 80px; - cursor: pointer; - } - .media-preview-controls { --icon-button-hover-color: white; --icon-button-hover-background: rgba(255, 255, 255, 0.2); diff --git a/apps/client/src/widgets/type_widgets/file/Video.tsx b/apps/client/src/widgets/type_widgets/file/Video.tsx index 2b9a5fb473..f6a22e3d4c 100644 --- a/apps/client/src/widgets/type_widgets/file/Video.tsx +++ b/apps/client/src/widgets/type_widgets/file/Video.tsx @@ -10,7 +10,7 @@ import ActionButton from "../../react/ActionButton"; import Dropdown from "../../react/Dropdown"; import Icon from "../../react/Icon"; import NoItems from "../../react/NoItems"; -import { PlayPauseButton, SeekBar } from "./MediaPlayer"; +import { PlayPauseButton, SeekBar, VolumeControl } from "./MediaPlayer"; const AUTO_HIDE_DELAY = 3000; @@ -128,7 +128,7 @@ export default function VideoPreview({ note }: { note: FNote }) {
- + @@ -181,65 +181,6 @@ function SkipButton({ videoRef, seconds, icon, text }: { videoRef: RefObject }) { - const [volume, setVolume] = useState(() => videoRef.current?.volume ?? 1); - const [muted, setMuted] = useState(() => videoRef.current?.muted ?? false); - - // Sync state when the video element changes volume externally. - useEffect(() => { - const video = videoRef.current; - if (!video) return; - - setVolume(video.volume); - setMuted(video.muted); - - const onVolumeChange = () => { - setVolume(video.volume); - setMuted(video.muted); - }; - video.addEventListener("volumechange", onVolumeChange); - return () => video.removeEventListener("volumechange", onVolumeChange); - }, []); - - const onVolumeChange = (e: Event) => { - const video = videoRef.current; - if (!video) return; - const val = parseFloat((e.target as HTMLInputElement).value); - video.volume = val; - setVolume(val); - if (val > 0 && video.muted) { - video.muted = false; - setMuted(false); - } - }; - - const toggleMute = () => { - const video = videoRef.current; - if (!video) return; - video.muted = !video.muted; - setMuted(video.muted); - }; - - return ( -
- - -
- ); -} - const PLAYBACK_SPEEDS = [0.5, 1, 1.25, 1.5, 2]; function PlaybackSpeed({ videoRef }: { videoRef: RefObject }) {