Respect device speed bounds in clip controls

This commit is contained in:
webadderall
2026-09-15 17:09:46 +10:00
parent 95a6f38f0b
commit 709388fa79
4 changed files with 41 additions and 13 deletions
+17 -12
View File
@@ -91,7 +91,7 @@ import {
} from "./types";
import { fromCursorSwaySliderValue, toCursorSwaySliderValue } from "./videoPlayback/cursorSway";
import { isZeroPadding } from "./videoPlayback/layoutUtils";
import { supportsPreviewPlaybackRate } from "./videoPlayback/playbackRate";
import { getPreviewPlaybackRateRange } from "./videoPlayback/playbackRate";
import {
cursorSetAssets,
getCursorStyleSizeMultiplier,
@@ -1089,12 +1089,7 @@ export function SettingsPanel({
const { preference: themePreference, setPreference: setThemePreference } = useTheme();
const isBackgroundPanel = panelMode === "background";
const initialEditorPreferences = useMemo(() => loadEditorPreferences(), []);
const maxClipSpeed = useMemo(() => {
for (let speed = 30; speed >= 1; speed -= 0.25) {
if (supportsPreviewPlaybackRate(speed)) return speed;
}
return 1;
}, []);
const clipSpeedRange = useMemo(getPreviewPlaybackRateRange, []);
const [builtInWallpapers, setBuiltInWallpapers] =
useState<BuiltInWallpaper[]>(BUILT_IN_WALLPAPERS);
const [wallpaperPreviewPaths, setWallpaperPreviewPaths] = useState<string[]>([]);
@@ -2441,6 +2436,12 @@ export function SettingsPanel({
className="data-[state=checked]:bg-[#2563EB] scale-75"
/>
</div>
{selectedClipSpeed != null &&
(selectedClipSpeed < clipSpeedRange.min || selectedClipSpeed > clipSpeedRange.max) && (
<p className="text-[11px] text-muted-foreground" role="status">
{selectedClipSpeed}× — {tSettings("speed.unsupported", "Not supported for preview on this device")}
</p>
)}
<label className="flex items-center justify-between rounded-lg bg-foreground/[0.03] px-2.5 py-2">
<span className="text-[10px] text-muted-foreground">
{tSettings("captions.textColor", "Text color")}
@@ -3011,22 +3012,26 @@ export function SettingsPanel({
</section>
);
const clipSectionContent = (
<section className="flex flex-col gap-3">
<SectionLabel>{tSettings("clip.title", "Clip")}</SectionLabel>
<SliderControl
label={tSettings("speed.label", "Speed")}
value={selectedClipSpeed ?? 1}
value={Math.min(clipSpeedRange.max, Math.max(clipSpeedRange.min, selectedClipSpeed ?? 1))}
defaultValue={1}
min={0.25}
max={maxClipSpeed}
min={clipSpeedRange.min}
max={clipSpeedRange.max}
step={0.25}
onChange={(value) => onClipSpeedChange?.(value)}
formatValue={(value) => `${value}×`}
parseInput={(text) => Number.parseFloat(text)}
/>
{selectedClipSpeed != null &&
(selectedClipSpeed < clipSpeedRange.min || selectedClipSpeed > clipSpeedRange.max) && (
<p className="text-[11px] text-muted-foreground" role="status">
{selectedClipSpeed}× — {tSettings("speed.unsupported", "Not supported for preview on this device")}
</p>
)}
<label className="flex items-center justify-between rounded-lg bg-foreground/[0.03] px-2.5 py-2">
<span className="text-[11px] text-muted-foreground">
{tSettings("clip.mute", "Mute clip")}
@@ -20,3 +20,17 @@ it("probes and caches the runtime's accepted rates", async () => {
for (const speed of [0, -1, NaN, Infinity])
expect(supportsPreviewPlaybackRate(speed)).toBe(false);
});
it.each([
{ lower: 0.25, upper: 16 },
{ lower: 0.5, upper: 4 },
{ lower: 1, upper: 30 },
])("exposes only supported steps between $lower and $upper", async ({ lower, upper }) => {
vi.stubGlobal("document", { createElement: () => ({
set playbackRate(rate: number) {
if (rate < lower || rate > upper) throw new DOMException("Unsupported", "NotSupportedError");
},
}) });
const { getPreviewPlaybackRateRange } = await import("./playbackRate");
expect(getPreviewPlaybackRateRange()).toEqual({ min: lower, max: upper });
});
@@ -16,3 +16,12 @@ export function supportsPreviewPlaybackRate(rate: number): boolean {
return false;
}
}
/** Contiguous quarter-step range supported by the runtime, anchored at normal speed. */
export function getPreviewPlaybackRateRange(): { min: number; max: number } {
let min = 1;
let max = 1;
while (min > 0.25 && supportsPreviewPlaybackRate(min - 0.25)) min -= 0.25;
while (max < 30 && supportsPreviewPlaybackRate(max + 0.25)) max += 0.25;
return { min, max };
}
+1 -1
View File
@@ -19,7 +19,7 @@
},
"clip": {
"title": "Clip",
"mute": "Disattiva audio clip",
"mute": "Disattiva l'audio del clip",
"mutedState": "Audio disattivato",
"unmutedState": "Audio in riproduzione",
"separateClipFromAudio": "Separa clip dall'audio",