From 49425e2fa76093747329d68dfbd1c70998d08bda Mon Sep 17 00:00:00 2001 From: ponderouspawn Date: Wed, 20 May 2026 22:32:40 -0400 Subject: [PATCH 1/5] feat: add volume scrolling --- src/controls/bar/ControlsBar.jsx | 14 +++++++--- src/controls/container/Container.jsx | 18 +++++++++++++ src/controls/settings/MainMenu.jsx | 33 +++++++++++++++++++++++ src/controls/settings/SettingsButton.jsx | 4 +++ src/controls/usePreferences.js | 34 ++++++++++++++++++++++++ src/controls/volume/VolumeControls.jsx | 2 ++ src/controls/volume/useVolumeControl.js | 16 ++++++++++- 7 files changed, 117 insertions(+), 4 deletions(-) diff --git a/src/controls/bar/ControlsBar.jsx b/src/controls/bar/ControlsBar.jsx index 076aa5c..ce86f9e 100644 --- a/src/controls/bar/ControlsBar.jsx +++ b/src/controls/bar/ControlsBar.jsx @@ -1,7 +1,6 @@ import React from "react"; import * as Tooltip from "@radix-ui/react-tooltip"; import clsx from "clsx"; -import { useVolumeControl } from "../volume/useVolumeControl.js"; import { useFullscreenControl } from "../fullscreen/useFullscreenControl.js"; import { useChannelInfo } from "../info/useChannelInfo.js"; import { useKeyboardControls } from "./useKeyboardControls.js"; @@ -22,9 +21,14 @@ export default function ControlsBar({ showControls, clickToPlayPause, onClickToPlayChange, + volume, + isMuted, + volumeScrollStep, + handleVolumeChange, + handleVolumeScroll, + handleMuteToggle, + setVolumeScrollStep, }) { - const { volume, isMuted, handleVolumeChange, handleMuteToggle } = - useVolumeControl(core); const { isFullscreen, handleFullscreenToggle } = useFullscreenControl(videoContainer); const { username, viewerCount, uptime } = useChannelInfo(); @@ -45,6 +49,7 @@ export default function ControlsBar({ >
event.stopPropagation()} className={clsx("controls-bar", !shouldShow && "controls-bar--hidden")} >
@@ -57,6 +62,7 @@ export default function ControlsBar({ volume={volume} isMuted={isMuted} onVolumeChange={handleVolumeChange} + onVolumeScroll={handleVolumeScroll} onMuteToggle={handleMuteToggle} />
@@ -76,6 +82,8 @@ export default function ControlsBar({ shouldShow={shouldShow} clickToPlayPause={clickToPlayPause} onClickToPlayChange={onClickToPlayChange} + volumeScrollStep={volumeScrollStep} + setVolumeScrollStep={setVolumeScrollStep} /> { const isInControlsBar = barRef.current?.contains(e.target); @@ -29,6 +39,7 @@ export default function Container({ core, videoContainer }) { ref={containerRef} className="kickstiny-container" onClick={handleContainerClick} + onWheel={handleVolumeScroll} >
); diff --git a/src/controls/settings/MainMenu.jsx b/src/controls/settings/MainMenu.jsx index c76d6f4..d7dc0f2 100644 --- a/src/controls/settings/MainMenu.jsx +++ b/src/controls/settings/MainMenu.jsx @@ -10,6 +10,8 @@ export default function MainMenu({ onIvsDebugChange, clickToPlayPause, onClickToPlayChange, + volumeScrollStep, + setVolumeScrollStep, }) { return ( <> @@ -19,6 +21,8 @@ export default function MainMenu({ e.preventDefault(); onNavigateQuality(); }} + onPointerMove={(e) => e.preventDefault()} + onPointerLeave={(e) => e.preventDefault()} > Quality @@ -32,6 +36,8 @@ export default function MainMenu({ onSelect={(e) => { e.preventDefault(); }} + onPointerMove={(e) => e.preventDefault()} + onPointerLeave={(e) => e.preventDefault()} > Pause on Click + { + e.preventDefault(); + }} + onPointerMove={(e) => e.preventDefault()} + onPointerLeave={(e) => e.preventDefault()} + > + Volume Scroll Step +
+
+
+ { + const valid = /^(100|[1-9]\d?|0)?$/.test(e.target.value); // 0-100 or empty + if (valid) setVolumeScrollStep(e.target.value); + }} + /> +
+
+
+
+ {process.env.NODE_ENV === "dev" && ( { e.preventDefault(); }} + onPointerMove={(e) => e.preventDefault()} + onPointerLeave={(e) => e.preventDefault()} > IVS Debug diff --git a/src/controls/settings/SettingsButton.jsx b/src/controls/settings/SettingsButton.jsx index 83aaa09..4050f87 100644 --- a/src/controls/settings/SettingsButton.jsx +++ b/src/controls/settings/SettingsButton.jsx @@ -16,6 +16,8 @@ export default function SettingsButton({ shouldShow, clickToPlayPause, onClickToPlayChange, + volumeScrollStep, + setVolumeScrollStep, }) { const { currentMenu, @@ -67,6 +69,8 @@ export default function SettingsButton({ onIvsDebugChange={setIsIvsDebug} clickToPlayPause={clickToPlayPause} onClickToPlayChange={onClickToPlayChange} + volumeScrollStep={volumeScrollStep} + setVolumeScrollStep={setVolumeScrollStep} /> )} diff --git a/src/controls/usePreferences.js b/src/controls/usePreferences.js index 9ec56ba..5ff401a 100644 --- a/src/controls/usePreferences.js +++ b/src/controls/usePreferences.js @@ -4,8 +4,10 @@ const QUALITY_STORAGE_KEY = "kickstiny.preference.quality"; const VOLUME_STORAGE_KEY = "kickstiny.preference.volume"; const IVS_DEBUG_STORAGE_KEY = "kickstiny.preference.ivsDebug"; const CLICK_TO_PLAY_PAUSE_STORAGE_KEY = "kickstiny.preference.clickToPlayPause"; +const VOLUME_SCROLL_STEP_STORAGE_KEY = "kickstiny.preference.volumeScrollStep"; const DEFAULT_VOLUME = 100; +const DEFAULT_VOLUME_SCROLL_STEP = 5; export function usePreferences() { const [savedQuality, setSavedQualityState] = useState(() => { @@ -52,6 +54,21 @@ export function usePreferences() { } }); + const [volumeScrollStep, setVolumeScrollStepState] = useState(() => { + try { + const stored = window.localStorage.getItem( + VOLUME_SCROLL_STEP_STORAGE_KEY, + ); + return stored !== null ? Number(stored) : DEFAULT_VOLUME_SCROLL_STEP; + } catch (err) { + console.log( + "[Kickstiny] Unable to read volume scroll step preference", + err, + ); + return DEFAULT_VOLUME_SCROLL_STEP; + } + }); + const setSavedQuality = useCallback((value) => { try { window.localStorage.setItem(QUALITY_STORAGE_KEY, value); @@ -94,14 +111,31 @@ export function usePreferences() { } }, []); + const setVolumeScrollStep = useCallback((value) => { + try { + window.localStorage.setItem( + VOLUME_SCROLL_STEP_STORAGE_KEY, + value.toString(), + ); + setVolumeScrollStepState(value); + } catch (err) { + console.log( + "[Kickstiny] Unable to persist volume scroll step preference", + err, + ); + } + }, []); + return { savedQuality, savedVolume, isIvsDebug, clickToPlayPause, + volumeScrollStep, setSavedQuality, setSavedVolume, setIsIvsDebug, setClickToPlayPause, + setVolumeScrollStep, }; } diff --git a/src/controls/volume/VolumeControls.jsx b/src/controls/volume/VolumeControls.jsx index a211a54..456d3a0 100644 --- a/src/controls/volume/VolumeControls.jsx +++ b/src/controls/volume/VolumeControls.jsx @@ -9,6 +9,7 @@ export default function VolumeControls({ volume, isMuted, onVolumeChange, + onVolumeScroll, onMuteToggle, }) { const label = isMuted ? "Unmute" : "Mute"; @@ -36,6 +37,7 @@ export default function VolumeControls({ className="slider" value={[volume]} onValueChange={([value]) => onVolumeChange(value)} + onWheel={onVolumeScroll} min={0} max={100} step={1} diff --git a/src/controls/volume/useVolumeControl.js b/src/controls/volume/useVolumeControl.js index c8c048b..41ab1be 100644 --- a/src/controls/volume/useVolumeControl.js +++ b/src/controls/volume/useVolumeControl.js @@ -18,7 +18,8 @@ function getUrlMuted() { } export function useVolumeControl(core) { - const { savedVolume, setSavedVolume } = usePreferences(); + const { savedVolume, setSavedVolume, volumeScrollStep, setVolumeScrollStep } = + usePreferences(); const urlMuted = getUrlMuted(); const [volume, setVolume] = useState(savedVolume); const [isMuted, setIsMuted] = useState(urlMuted); @@ -36,6 +37,16 @@ export function useVolumeControl(core) { [core, setSavedVolume], ); + const handleVolumeScroll = useCallback( + (event) => { + event.preventDefault(); + handleVolumeChange( + volume + (event.deltaY < 0 ? +volumeScrollStep : -volumeScrollStep), + ); + }, + [handleVolumeChange, volume, volumeScrollStep], + ); + const handleMuteToggle = useCallback(() => { setVolume(isMuted ? savedVolume : 0); setIsMuted(!isMuted); @@ -82,6 +93,9 @@ export function useVolumeControl(core) { volume, isMuted, handleVolumeChange, + handleVolumeScroll, handleMuteToggle, + volumeScrollStep, + setVolumeScrollStep, }; } From e002f60cef18bc2baca5747cbcb0101b9b7b0433 Mon Sep 17 00:00:00 2001 From: ponderouspawn Date: Sun, 31 May 2026 22:05:30 -0400 Subject: [PATCH 2/5] fix: remove `volumeScrollStep` preference --- src/controls/bar/ControlsBar.jsx | 4 --- src/controls/container/Container.jsx | 4 --- src/controls/settings/MainMenu.jsx | 33 ----------------------- src/controls/settings/SettingsButton.jsx | 4 --- src/controls/usePreferences.js | 34 ------------------------ src/controls/volume/useVolumeControl.js | 11 +++----- 6 files changed, 3 insertions(+), 87 deletions(-) diff --git a/src/controls/bar/ControlsBar.jsx b/src/controls/bar/ControlsBar.jsx index ce86f9e..9bf8820 100644 --- a/src/controls/bar/ControlsBar.jsx +++ b/src/controls/bar/ControlsBar.jsx @@ -23,11 +23,9 @@ export default function ControlsBar({ onClickToPlayChange, volume, isMuted, - volumeScrollStep, handleVolumeChange, handleVolumeScroll, handleMuteToggle, - setVolumeScrollStep, }) { const { isFullscreen, handleFullscreenToggle } = useFullscreenControl(videoContainer); @@ -82,8 +80,6 @@ export default function ControlsBar({ shouldShow={shouldShow} clickToPlayPause={clickToPlayPause} onClickToPlayChange={onClickToPlayChange} - volumeScrollStep={volumeScrollStep} - setVolumeScrollStep={setVolumeScrollStep} /> { @@ -53,11 +51,9 @@ export default function Container({ core, videoContainer }) { onClickToPlayChange={setClickToPlayPause} volume={volume} isMuted={isMuted} - volumeScrollStep={volumeScrollStep} handleVolumeChange={handleVolumeChange} handleVolumeScroll={handleVolumeScroll} handleMuteToggle={handleMuteToggle} - setVolumeScrollStep={setVolumeScrollStep} /> ); diff --git a/src/controls/settings/MainMenu.jsx b/src/controls/settings/MainMenu.jsx index d7dc0f2..c76d6f4 100644 --- a/src/controls/settings/MainMenu.jsx +++ b/src/controls/settings/MainMenu.jsx @@ -10,8 +10,6 @@ export default function MainMenu({ onIvsDebugChange, clickToPlayPause, onClickToPlayChange, - volumeScrollStep, - setVolumeScrollStep, }) { return ( <> @@ -21,8 +19,6 @@ export default function MainMenu({ e.preventDefault(); onNavigateQuality(); }} - onPointerMove={(e) => e.preventDefault()} - onPointerLeave={(e) => e.preventDefault()} > Quality @@ -36,8 +32,6 @@ export default function MainMenu({ onSelect={(e) => { e.preventDefault(); }} - onPointerMove={(e) => e.preventDefault()} - onPointerLeave={(e) => e.preventDefault()} > Pause on Click - { - e.preventDefault(); - }} - onPointerMove={(e) => e.preventDefault()} - onPointerLeave={(e) => e.preventDefault()} - > - Volume Scroll Step -
-
-
- { - const valid = /^(100|[1-9]\d?|0)?$/.test(e.target.value); // 0-100 or empty - if (valid) setVolumeScrollStep(e.target.value); - }} - /> -
-
-
-
- {process.env.NODE_ENV === "dev" && ( { e.preventDefault(); }} - onPointerMove={(e) => e.preventDefault()} - onPointerLeave={(e) => e.preventDefault()} > IVS Debug diff --git a/src/controls/settings/SettingsButton.jsx b/src/controls/settings/SettingsButton.jsx index 4050f87..83aaa09 100644 --- a/src/controls/settings/SettingsButton.jsx +++ b/src/controls/settings/SettingsButton.jsx @@ -16,8 +16,6 @@ export default function SettingsButton({ shouldShow, clickToPlayPause, onClickToPlayChange, - volumeScrollStep, - setVolumeScrollStep, }) { const { currentMenu, @@ -69,8 +67,6 @@ export default function SettingsButton({ onIvsDebugChange={setIsIvsDebug} clickToPlayPause={clickToPlayPause} onClickToPlayChange={onClickToPlayChange} - volumeScrollStep={volumeScrollStep} - setVolumeScrollStep={setVolumeScrollStep} /> )} diff --git a/src/controls/usePreferences.js b/src/controls/usePreferences.js index 5ff401a..9ec56ba 100644 --- a/src/controls/usePreferences.js +++ b/src/controls/usePreferences.js @@ -4,10 +4,8 @@ const QUALITY_STORAGE_KEY = "kickstiny.preference.quality"; const VOLUME_STORAGE_KEY = "kickstiny.preference.volume"; const IVS_DEBUG_STORAGE_KEY = "kickstiny.preference.ivsDebug"; const CLICK_TO_PLAY_PAUSE_STORAGE_KEY = "kickstiny.preference.clickToPlayPause"; -const VOLUME_SCROLL_STEP_STORAGE_KEY = "kickstiny.preference.volumeScrollStep"; const DEFAULT_VOLUME = 100; -const DEFAULT_VOLUME_SCROLL_STEP = 5; export function usePreferences() { const [savedQuality, setSavedQualityState] = useState(() => { @@ -54,21 +52,6 @@ export function usePreferences() { } }); - const [volumeScrollStep, setVolumeScrollStepState] = useState(() => { - try { - const stored = window.localStorage.getItem( - VOLUME_SCROLL_STEP_STORAGE_KEY, - ); - return stored !== null ? Number(stored) : DEFAULT_VOLUME_SCROLL_STEP; - } catch (err) { - console.log( - "[Kickstiny] Unable to read volume scroll step preference", - err, - ); - return DEFAULT_VOLUME_SCROLL_STEP; - } - }); - const setSavedQuality = useCallback((value) => { try { window.localStorage.setItem(QUALITY_STORAGE_KEY, value); @@ -111,31 +94,14 @@ export function usePreferences() { } }, []); - const setVolumeScrollStep = useCallback((value) => { - try { - window.localStorage.setItem( - VOLUME_SCROLL_STEP_STORAGE_KEY, - value.toString(), - ); - setVolumeScrollStepState(value); - } catch (err) { - console.log( - "[Kickstiny] Unable to persist volume scroll step preference", - err, - ); - } - }, []); - return { savedQuality, savedVolume, isIvsDebug, clickToPlayPause, - volumeScrollStep, setSavedQuality, setSavedVolume, setIsIvsDebug, setClickToPlayPause, - setVolumeScrollStep, }; } diff --git a/src/controls/volume/useVolumeControl.js b/src/controls/volume/useVolumeControl.js index 41ab1be..8846d1c 100644 --- a/src/controls/volume/useVolumeControl.js +++ b/src/controls/volume/useVolumeControl.js @@ -18,8 +18,7 @@ function getUrlMuted() { } export function useVolumeControl(core) { - const { savedVolume, setSavedVolume, volumeScrollStep, setVolumeScrollStep } = - usePreferences(); + const { savedVolume, setSavedVolume } = usePreferences(); const urlMuted = getUrlMuted(); const [volume, setVolume] = useState(savedVolume); const [isMuted, setIsMuted] = useState(urlMuted); @@ -40,11 +39,9 @@ export function useVolumeControl(core) { const handleVolumeScroll = useCallback( (event) => { event.preventDefault(); - handleVolumeChange( - volume + (event.deltaY < 0 ? +volumeScrollStep : -volumeScrollStep), - ); + handleVolumeChange(volume + (event.deltaY < 0 ? +5 : -5)); }, - [handleVolumeChange, volume, volumeScrollStep], + [handleVolumeChange, volume], ); const handleMuteToggle = useCallback(() => { @@ -95,7 +92,5 @@ export function useVolumeControl(core) { handleVolumeChange, handleVolumeScroll, handleMuteToggle, - volumeScrollStep, - setVolumeScrollStep, }; } From edf168b139df351e99981008036081f6e7d73e20 Mon Sep 17 00:00:00 2001 From: ponderouspawn Date: Sun, 31 May 2026 22:32:56 -0400 Subject: [PATCH 3/5] imp: use lower step size on laptop trackpads --- src/controls/volume/useVolumeControl.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/controls/volume/useVolumeControl.js b/src/controls/volume/useVolumeControl.js index 8846d1c..1b048cf 100644 --- a/src/controls/volume/useVolumeControl.js +++ b/src/controls/volume/useVolumeControl.js @@ -39,7 +39,15 @@ export function useVolumeControl(core) { const handleVolumeScroll = useCallback( (event) => { event.preventDefault(); - handleVolumeChange(volume + (event.deltaY < 0 ? +5 : -5)); + + // Step 5 units if mouse wheel, 1 unit if trackpad + const isLikelyTrackpad = + !Number.isInteger(event.deltaY) || + (event.deltaMode === 0 && Math.abs(event.deltaY) < 10); + const stepSize = isLikelyTrackpad ? 1 : 5; + const direction = Math.sign(-event.deltaY); + + handleVolumeChange(volume + direction * stepSize); }, [handleVolumeChange, volume], ); From d22c94d22f60d4c3883512d084acba52f07faa89 Mon Sep 17 00:00:00 2001 From: ponderouspawn Date: Sun, 31 May 2026 22:35:30 -0400 Subject: [PATCH 4/5] docs: match comment to conditional --- src/controls/volume/useVolumeControl.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controls/volume/useVolumeControl.js b/src/controls/volume/useVolumeControl.js index 1b048cf..c623e61 100644 --- a/src/controls/volume/useVolumeControl.js +++ b/src/controls/volume/useVolumeControl.js @@ -40,7 +40,7 @@ export function useVolumeControl(core) { (event) => { event.preventDefault(); - // Step 5 units if mouse wheel, 1 unit if trackpad + // Step 1 unit if trackpad, 5 units if mouse wheel const isLikelyTrackpad = !Number.isInteger(event.deltaY) || (event.deltaMode === 0 && Math.abs(event.deltaY) < 10); From a17809d488842e06c80e8636989cfb738ab10a2d Mon Sep 17 00:00:00 2001 From: ponderouspawn Date: Sun, 31 May 2026 23:19:03 -0400 Subject: [PATCH 5/5] imp: prune function calls --- src/controls/volume/useVolumeControl.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/controls/volume/useVolumeControl.js b/src/controls/volume/useVolumeControl.js index c623e61..d5895bc 100644 --- a/src/controls/volume/useVolumeControl.js +++ b/src/controls/volume/useVolumeControl.js @@ -27,19 +27,22 @@ export function useVolumeControl(core) { const handleVolumeChange = useCallback( (newVolume) => { const clampedVolume = clampVolume(newVolume); + if (clampedVolume === volume) return; setVolume(clampedVolume); setSavedVolume(clampedVolume); core.setVolume(normalizeVolume(clampedVolume)); core.setMuted(clampedVolume === 0); setIsMuted(clampedVolume === 0); }, - [core, setSavedVolume], + [core, volume, setSavedVolume], ); const handleVolumeScroll = useCallback( (event) => { event.preventDefault(); + if (event.deltaY === 0) return; + // Step 1 unit if trackpad, 5 units if mouse wheel const isLikelyTrackpad = !Number.isInteger(event.deltaY) || @@ -49,7 +52,7 @@ export function useVolumeControl(core) { handleVolumeChange(volume + direction * stepSize); }, - [handleVolumeChange, volume], + [volume, handleVolumeChange], ); const handleMuteToggle = useCallback(() => {