From 0daf2295a37e73d5a99c3600de727736f01908e0 Mon Sep 17 00:00:00 2001 From: AjTheSpidey Date: Sat, 23 May 2026 03:39:26 +0800 Subject: [PATCH] fix: accept decimal custom speeds --- src/components/video-editor/SettingsPanel.tsx | 33 ++++++------ .../video-editor/customPlaybackSpeed.test.ts | 50 +++++++++++++++++++ .../video-editor/customPlaybackSpeed.ts | 37 ++++++++++++++ src/i18n/locales/it/settings.json | 3 +- 4 files changed, 106 insertions(+), 17 deletions(-) create mode 100644 src/components/video-editor/customPlaybackSpeed.test.ts create mode 100644 src/components/video-editor/customPlaybackSpeed.ts diff --git a/src/components/video-editor/SettingsPanel.tsx b/src/components/video-editor/SettingsPanel.tsx index b0b46df..3bd8750 100644 --- a/src/components/video-editor/SettingsPanel.tsx +++ b/src/components/video-editor/SettingsPanel.tsx @@ -53,6 +53,7 @@ import ColorPicker from "../ui/color-picker"; import { AnnotationSettingsPanel } from "./AnnotationSettingsPanel"; import { BlurSettingsPanel } from "./BlurSettingsPanel"; import { CropControl } from "./CropControl"; +import { parseCustomPlaybackSpeedInput } from "./customPlaybackSpeed"; import { KeyboardShortcutsHelp } from "./KeyboardShortcutsHelp"; import type { AnnotationRegion, @@ -71,7 +72,6 @@ import type { } from "./types"; import { DEFAULT_WEBCAM_SIZE_PRESET, - MAX_PLAYBACK_SPEED, MAX_ZOOM_SCALE, MIN_ZOOM_SCALE, ROTATION_3D_PRESET_ORDER, @@ -90,37 +90,38 @@ function CustomSpeedInput({ onError: () => void; }) { const isPreset = SPEED_OPTIONS.some((o) => o.speed === value); - const [draft, setDraft] = useState(isPreset ? "" : String(Math.round(value))); + const [draft, setDraft] = useState(isPreset ? "" : String(value)); const [isFocused, setIsFocused] = useState(false); const prevValue = useRef(value); if (!isFocused && prevValue.current !== value) { prevValue.current = value; - setDraft(isPreset ? "" : String(Math.round(value))); + setDraft(isPreset ? "" : String(value)); } const handleChange = useCallback( (e: React.ChangeEvent) => { - const digits = e.target.value.replace(/\D/g, ""); - if (digits === "") { - setDraft(""); - return; - } - const num = Number(digits); - if (num > MAX_PLAYBACK_SPEED) { + const result = parseCustomPlaybackSpeedInput(e.target.value); + if (result.status === "too-fast") { onError(); return; } - setDraft(digits); - if (num >= 1) onChange(num); + + setDraft(result.draft); + if (result.status === "valid") { + onChange(result.speed); + } }, [onChange, onError], ); const handleBlur = useCallback(() => { setIsFocused(false); - if (!draft || Number(draft) < 1) { - setDraft(isPreset ? "" : String(Math.round(value))); + const result = parseCustomPlaybackSpeedInput(draft); + if (result.status === "valid") { + setDraft(String(result.speed)); + } else { + setDraft(isPreset ? "" : String(value)); } }, [draft, isPreset, value]); @@ -128,8 +129,8 @@ function CustomSpeedInput({
setIsFocused(true)} diff --git a/src/components/video-editor/customPlaybackSpeed.test.ts b/src/components/video-editor/customPlaybackSpeed.test.ts new file mode 100644 index 0000000..223175c --- /dev/null +++ b/src/components/video-editor/customPlaybackSpeed.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from "vitest"; +import { parseCustomPlaybackSpeedInput } from "./customPlaybackSpeed"; + +describe("parseCustomPlaybackSpeedInput", () => { + it("accepts decimal playback speeds", () => { + expect(parseCustomPlaybackSpeedInput("1.1")).toEqual({ + status: "valid", + draft: "1.1", + speed: 1.1, + }); + }); + + it("keeps a single decimal point while typing", () => { + expect(parseCustomPlaybackSpeedInput("1.2.3")).toEqual({ + status: "valid", + draft: "1.23", + speed: 1.23, + }); + }); + + it("allows sub-1 custom speeds down to the editor minimum", () => { + expect(parseCustomPlaybackSpeedInput("0.1")).toEqual({ + status: "valid", + draft: "0.1", + speed: 0.1, + }); + }); + + it("rejects speeds below the editor minimum", () => { + expect(parseCustomPlaybackSpeedInput("0.09")).toEqual({ + status: "too-slow", + draft: "0.09", + }); + }); + + it("accepts comma decimal input by normalizing to a dot", () => { + expect(parseCustomPlaybackSpeedInput("1,1")).toEqual({ + status: "valid", + draft: "1.1", + speed: 1.1, + }); + }); + + it("rejects speeds above the editor maximum", () => { + expect(parseCustomPlaybackSpeedInput("16.1")).toEqual({ + status: "too-fast", + draft: "16.1", + }); + }); +}); diff --git a/src/components/video-editor/customPlaybackSpeed.ts b/src/components/video-editor/customPlaybackSpeed.ts new file mode 100644 index 0000000..64cc3e3 --- /dev/null +++ b/src/components/video-editor/customPlaybackSpeed.ts @@ -0,0 +1,37 @@ +import { + clampPlaybackSpeed, + MAX_PLAYBACK_SPEED, + MIN_PLAYBACK_SPEED, + type PlaybackSpeed, +} from "./types"; + +export type CustomPlaybackSpeedInputResult = + | { status: "empty"; draft: string } + | { status: "too-fast"; draft: string } + | { status: "too-slow"; draft: string } + | { status: "valid"; draft: string; speed: PlaybackSpeed }; + +export function parseCustomPlaybackSpeedInput(rawValue: string): CustomPlaybackSpeedInputResult { + const decimalDraft = rawValue.replace(/,/g, ".").replace(/[^\d.]/g, ""); + const [whole = "", ...fractionParts] = decimalDraft.split("."); + const draft = fractionParts.length > 0 ? `${whole}.${fractionParts.join("")}` : whole; + + if (draft === "" || draft === ".") { + return { status: "empty", draft }; + } + + const speed = Number(draft); + if (!Number.isFinite(speed)) { + return { status: "empty", draft }; + } + + if (speed > MAX_PLAYBACK_SPEED) { + return { status: "too-fast", draft }; + } + + if (speed < MIN_PLAYBACK_SPEED) { + return { status: "too-slow", draft }; + } + + return { status: "valid", draft, speed: clampPlaybackSpeed(speed) }; +} diff --git a/src/i18n/locales/it/settings.json b/src/i18n/locales/it/settings.json index e609d08..0515a76 100644 --- a/src/i18n/locales/it/settings.json +++ b/src/i18n/locales/it/settings.json @@ -104,8 +104,9 @@ "gifButton": "Esporta GIF", "chooseSaveLocation": "Scegli posizione di salvataggio" }, - "links": { + "support": { "reportBug": "Segnala bug", + "saveDiagnostics": "Salva dati diagnostici", "starOnGithub": "Metti stella su GitHub" }, "imageUpload": {