From 310b5dfe05f5bed5515b4313c6dcbf833b7d123d Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Sun, 15 Mar 2026 07:26:23 -0500 Subject: [PATCH] UI tweaks and fixes (#22448) * fix double scrollbar in debug replay * always hide ffmpeg cpu warnings for replay cameras * add slovenian * fix motion previews on safari and ios match the logic used in ScrubbablePreview for manually stepping currentTime at the correct rate * prevent motion recalibration when opening motion tuner --- web/src/hooks/use-date-locale.ts | 1 + web/src/hooks/use-stats.ts | 6 +- web/src/lib/const.ts | 1 + web/src/pages/Replay.tsx | 17 +++- web/src/views/events/MotionPreviewsPane.tsx | 89 +++++++++++---------- web/src/views/settings/MotionTunerView.tsx | 16 +++- 6 files changed, 74 insertions(+), 56 deletions(-) diff --git a/web/src/hooks/use-date-locale.ts b/web/src/hooks/use-date-locale.ts index 7a8085666..901162fe4 100644 --- a/web/src/hooks/use-date-locale.ts +++ b/web/src/hooks/use-date-locale.ts @@ -38,6 +38,7 @@ const localeMap: Record Promise> = { th: () => import("date-fns/locale/th").then((module) => module.th), ca: () => import("date-fns/locale/ca").then((module) => module.ca), hr: () => import("date-fns/locale/hr").then((module) => module.hr), + sl: () => import("date-fns/locale/sl").then((module) => module.sl), }; export function useDateLocale(): Locale { diff --git a/web/src/hooks/use-stats.ts b/web/src/hooks/use-stats.ts index 5bddb75ac..bfb3bafd8 100644 --- a/web/src/hooks/use-stats.ts +++ b/web/src/hooks/use-stats.ts @@ -106,13 +106,11 @@ export default function useStats(stats: FrigateStats | undefined) { const cameraName = config?.cameras?.[name]?.friendly_name ?? name; - // Skip ffmpeg warnings for replay cameras when debug replay is active + // Skip ffmpeg warnings for replay cameras if ( !isNaN(ffmpegAvg) && ffmpegAvg >= CameraFfmpegThreshold.error && - !( - debugReplayStatus?.active && debugReplayStatus?.replay_camera === name - ) + !isReplayCamera(name) ) { problems.push({ text: t("stats.ffmpegHighCpuUsage", { diff --git a/web/src/lib/const.ts b/web/src/lib/const.ts index 5000d7a0b..a11239613 100644 --- a/web/src/lib/const.ts +++ b/web/src/lib/const.ts @@ -26,6 +26,7 @@ export const supportedLanguageKeys = [ "pl", "hr", "sk", + "sl", "lt", "uk", "cs", diff --git a/web/src/pages/Replay.tsx b/web/src/pages/Replay.tsx index 187a9a76b..50927d1dd 100644 --- a/web/src/pages/Replay.tsx +++ b/web/src/pages/Replay.tsx @@ -381,7 +381,7 @@ export default function Replay() { {/* Side panel */} -
+
{t("title")} @@ -399,7 +399,10 @@ export default function Replay() {

{t("description")}

- + {t("debug.debugging", { ns: "views/settings" })} @@ -409,7 +412,10 @@ export default function Replay() { {t("websocket_messages")} - +
{DEBUG_OPTION_KEYS.map((key) => { @@ -554,7 +560,10 @@ export default function Replay() {
- + | null>(null); + + useEffect(() => { + return () => { + if (compatIntervalRef.current) { + clearInterval(compatIntervalRef.current); + } + }; + }, []); + const resetPlayback = useCallback(() => { if (!videoRef.current || !preview) { return; } + if (compatIntervalRef.current) { + clearInterval(compatIntervalRef.current); + compatIntervalRef.current = null; + } + videoRef.current.currentTime = clipStart; - videoRef.current.playbackRate = playbackRate; - }, [clipStart, playbackRate, preview]); - useEffect(() => { - if (!videoRef.current || !preview) { - return; - } - - if (!isVisible) { + if (isSafari || (isFirefox && isMobile)) { + // Safari / iOS can't play at speeds > 2x, so manually step through frames videoRef.current.pause(); - videoRef.current.currentTime = clipStart; - return; - } + compatIntervalRef.current = setInterval(() => { + if (!videoRef.current) { + return; + } - if (videoRef.current.readyState >= 2) { - resetPlayback(); - void videoRef.current.play().catch(() => undefined); + videoRef.current.currentTime += 1; + + if (videoRef.current.currentTime >= clipEnd) { + videoRef.current.currentTime = clipStart; + } + }, 1000 / playbackRate); + } else { + videoRef.current.playbackRate = playbackRate; } - }, [clipStart, isVisible, preview, resetPlayback]); + }, [clipStart, clipEnd, playbackRate, preview]); const drawDimOverlay = useCallback(() => { if (!dimOverlayCanvasRef.current) { @@ -463,15 +479,17 @@ function MotionPreviewClip({ {showLoadingIndicator && ( )} - {preview ? ( + {preview && isVisible ? ( <> {motionHeatmap && ( { if (config && selectedCamera) { return config.cameras[selectedCamera]; @@ -70,6 +72,7 @@ export default function MotionTunerView({ }, [config, selectedCamera]); useEffect(() => { + userInteractedRef.current = false; if (cameraConfig) { setMotionSettings({ threshold: cameraConfig.motion.threshold, @@ -87,24 +90,29 @@ export default function MotionTunerView({ }, [selectedCamera]); useEffect(() => { - if (!motionSettings.threshold) return; + if (!motionSettings.threshold || !userInteractedRef.current) return; sendMotionThreshold(motionSettings.threshold); }, [motionSettings.threshold, sendMotionThreshold]); useEffect(() => { - if (!motionSettings.contour_area) return; + if (!motionSettings.contour_area || !userInteractedRef.current) return; sendMotionContourArea(motionSettings.contour_area); }, [motionSettings.contour_area, sendMotionContourArea]); useEffect(() => { - if (motionSettings.improve_contrast === undefined) return; + if ( + motionSettings.improve_contrast === undefined || + !userInteractedRef.current + ) + return; sendImproveContrast(motionSettings.improve_contrast ? "ON" : "OFF"); }, [motionSettings.improve_contrast, sendImproveContrast]); const handleMotionConfigChange = (newConfig: Partial) => { + userInteractedRef.current = true; setMotionSettings((prevConfig) => ({ ...prevConfig, ...newConfig })); setUnsavedChanges(true); setChangedValue(true);