WebUI Fixes (#10481)

* Update previews on the hour

* Allow tap to toggle controls so zooming still works

* Use hash location insteaad of state for live camera view

* Add typing
This commit is contained in:
Nicolas Mowen 2024-03-15 12:46:17 -06:00 committed by GitHub
parent 93260f6cfd
commit 380b15b286
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 91 additions and 24 deletions

View File

@ -21,6 +21,7 @@ type LivePlayerProps = {
windowVisible?: boolean; windowVisible?: boolean;
playAudio?: boolean; playAudio?: boolean;
micEnabled?: boolean; // only webrtc supports mic micEnabled?: boolean; // only webrtc supports mic
iOSCompatFullScreen?: boolean;
onClick?: () => void; onClick?: () => void;
}; };
@ -32,6 +33,7 @@ export default function LivePlayer({
windowVisible = true, windowVisible = true,
playAudio = false, playAudio = false,
micEnabled = false, micEnabled = false,
iOSCompatFullScreen = false,
onClick, onClick,
}: LivePlayerProps) { }: LivePlayerProps) {
// camera activity // camera activity
@ -100,6 +102,7 @@ export default function LivePlayer({
playbackEnabled={cameraActive} playbackEnabled={cameraActive}
audioEnabled={playAudio} audioEnabled={playAudio}
microphoneEnabled={micEnabled} microphoneEnabled={micEnabled}
iOSCompatFullScreen={iOSCompatFullScreen}
onPlaying={() => setLiveReady(true)} onPlaying={() => setLiveReady(true)}
/> />
); );

View File

@ -1,5 +1,5 @@
import { baseUrl } from "@/api/baseUrl"; import { baseUrl } from "@/api/baseUrl";
import { useCallback, useEffect, useMemo, useRef } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
type WebRtcPlayerProps = { type WebRtcPlayerProps = {
className?: string; className?: string;
@ -7,6 +7,7 @@ type WebRtcPlayerProps = {
playbackEnabled?: boolean; playbackEnabled?: boolean;
audioEnabled?: boolean; audioEnabled?: boolean;
microphoneEnabled?: boolean; microphoneEnabled?: boolean;
iOSCompatFullScreen?: boolean; // ios doesn't support fullscreen divs so we must support the video element
onPlaying?: () => void; onPlaying?: () => void;
}; };
@ -16,6 +17,7 @@ export default function WebRtcPlayer({
playbackEnabled = true, playbackEnabled = true,
audioEnabled = false, audioEnabled = false,
microphoneEnabled = false, microphoneEnabled = false,
iOSCompatFullScreen = false,
onPlaying, onPlaying,
}: WebRtcPlayerProps) { }: WebRtcPlayerProps) {
// metadata // metadata
@ -170,14 +172,23 @@ export default function WebRtcPlayer({
microphoneEnabled, microphoneEnabled,
]); ]);
// ios compat
const [iOSCompatControls, setiOSCompatControls] = useState(false);
return ( return (
<video <video
ref={videoRef} ref={videoRef}
className={className} className={className}
controls={iOSCompatControls}
autoPlay autoPlay
playsInline playsInline
muted={!audioEnabled} muted={!audioEnabled}
onLoadedData={onPlaying} onLoadedData={onPlaying}
onClick={
iOSCompatFullScreen
? () => setiOSCompatControls(!iOSCompatControls)
: undefined
}
/> />
); );
} }

View File

@ -2,7 +2,7 @@ import { useCallback, useMemo } from "react";
import { useLocation, useNavigate } from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
import { usePersistence } from "./use-persistence"; import { usePersistence } from "./use-persistence";
export default function useOverlayState<S extends string>( export function useOverlayState<S extends string>(
key: string, key: string,
defaultValue: S | undefined = undefined, defaultValue: S | undefined = undefined,
): [S | undefined, (value: S, replace?: boolean) => void] { ): [S | undefined, (value: S, replace?: boolean) => void] {
@ -63,3 +63,31 @@ export function usePersistedOverlayState<S extends string>(
setOverlayStateValue, setOverlayStateValue,
]; ];
} }
export function useHashState<S extends string>(): [
S | undefined,
(value: S) => void,
] {
const location = useLocation();
const navigate = useNavigate();
const setHash = useCallback(
(value: S | undefined) => {
if (!value) {
navigate(location.pathname);
} else {
navigate(`${location.pathname}#${value}`);
}
},
// we know that these deps are correct
// eslint-disable-next-line react-hooks/exhaustive-deps
[location, navigate],
);
const hash = useMemo(
() => location.hash.substring(1) as unknown as S,
[location.hash],
);
return [hash, setHash];
}

View File

@ -1,7 +1,7 @@
import ActivityIndicator from "@/components/indicators/activity-indicator"; import ActivityIndicator from "@/components/indicators/activity-indicator";
import useApiFilter from "@/hooks/use-api-filter"; import useApiFilter from "@/hooks/use-api-filter";
import { useTimezone } from "@/hooks/use-date-utils"; import { useTimezone } from "@/hooks/use-date-utils";
import useOverlayState from "@/hooks/use-overlay-state"; import { useOverlayState } from "@/hooks/use-overlay-state";
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
import { Preview } from "@/types/preview"; import { Preview } from "@/types/preview";
import { import {
@ -16,7 +16,7 @@ import {
MobileRecordingView, MobileRecordingView,
} from "@/views/events/RecordingView"; } from "@/views/events/RecordingView";
import axios from "axios"; import axios from "axios";
import { useCallback, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { isMobile } from "react-device-detect"; import { isMobile } from "react-device-detect";
import useSWR from "swr"; import useSWR from "swr";
@ -103,7 +103,7 @@ export default function Events() {
}, [updateSummary]); }, [updateSummary]);
// preview videos // preview videos
const [previewKey, setPreviewKey] = useState(0);
const previewTimes = useMemo(() => { const previewTimes = useMemo(() => {
if (!reviews || reviews.length == 0) { if (!reviews || reviews.length == 0) {
return undefined; return undefined;
@ -112,13 +112,19 @@ export default function Events() {
const startDate = new Date(); const startDate = new Date();
startDate.setMinutes(0, 0, 0); startDate.setMinutes(0, 0, 0);
const endDate = new Date(reviews.at(-1)?.end_time || 0); let endDate;
endDate.setHours(0, 0, 0, 0); if (previewKey == 0) {
endDate = new Date(reviews.at(-1)?.end_time || 0);
endDate.setHours(0, 0, 0, 0);
} else {
endDate = new Date();
}
return { return {
start: startDate.getTime() / 1000, start: startDate.getTime() / 1000,
end: endDate.getTime() / 1000, end: endDate.getTime() / 1000,
}; };
}, [reviews]); }, [reviews, previewKey]);
const { data: allPreviews } = useSWR<Preview[]>( const { data: allPreviews } = useSWR<Preview[]>(
previewTimes previewTimes
? `preview/all/start/${previewTimes.start}/end/${previewTimes.end}` ? `preview/all/start/${previewTimes.start}/end/${previewTimes.end}`
@ -126,6 +132,20 @@ export default function Events() {
{ revalidateOnFocus: false }, { revalidateOnFocus: false },
); );
// Set a timeout to update previews on the hour
useEffect(() => {
const future = new Date();
future.setHours(future.getHours() + 1, 0, 30, 0);
const timeoutId = setTimeout(
() => setPreviewKey(10 * Math.random()),
future.getTime() - Date.now(),
);
return () => {
clearTimeout(timeoutId);
};
}, []);
// review status // review status
const markAllItemsAsReviewed = useCallback( const markAllItemsAsReviewed = useCallback(

View File

@ -1,4 +1,5 @@
import useOverlayState, { import {
useHashState,
usePersistedOverlayState, usePersistedOverlayState,
} from "@/hooks/use-overlay-state"; } from "@/hooks/use-overlay-state";
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
@ -10,7 +11,7 @@ import useSWR from "swr";
function Live() { function Live() {
const { data: config } = useSWR<FrigateConfig>("config"); const { data: config } = useSWR<FrigateConfig>("config");
const [selectedCameraName, setSelectedCameraName] = useOverlayState("camera"); const [selectedCameraName, setSelectedCameraName] = useHashState();
const [cameraGroup] = usePersistedOverlayState( const [cameraGroup] = usePersistedOverlayState(
"cameraGroup", "cameraGroup",
"default" as string, "default" as string,

View File

@ -28,6 +28,7 @@ import React, {
} from "react"; } from "react";
import { import {
isDesktop, isDesktop,
isIOS,
isMobile, isMobile,
isSafari, isSafari,
useMobileOrientation, useMobileOrientation,
@ -189,20 +190,22 @@ export default function LiveCameraView({ camera }: LiveCameraViewProps) {
<div <div
className={`flex flex-row items-center gap-2 mr-1 *:rounded-lg ${isMobile ? "landscape:flex-col" : ""}`} className={`flex flex-row items-center gap-2 mr-1 *:rounded-lg ${isMobile ? "landscape:flex-col" : ""}`}
> >
<CameraFeatureToggle {!isIOS && (
className="p-2 md:p-0" <CameraFeatureToggle
variant={fullscreen ? "overlay" : "primary"} className="p-2 md:p-0"
Icon={fullscreen ? FaCompress : FaExpand} variant={fullscreen ? "overlay" : "primary"}
isActive={fullscreen} Icon={fullscreen ? FaCompress : FaExpand}
title={fullscreen ? "Close" : "Fullscreen"} isActive={fullscreen}
onClick={() => { title={fullscreen ? "Close" : "Fullscreen"}
if (fullscreen) { onClick={() => {
document.exitFullscreen(); if (fullscreen) {
} else { document.exitFullscreen();
mainRef.current?.requestFullscreen(); } else {
} mainRef.current?.requestFullscreen();
}} }
/> }}
/>
)}
{window.isSecureContext && ( {window.isSecureContext && (
<CameraFeatureToggle <CameraFeatureToggle
className="p-2 md:p-0" className="p-2 md:p-0"
@ -286,6 +289,7 @@ export default function LiveCameraView({ camera }: LiveCameraViewProps) {
cameraConfig={camera} cameraConfig={camera}
playAudio={audio} playAudio={audio}
micEnabled={mic} micEnabled={mic}
iOSCompatFullScreen={isIOS}
preferredLiveMode={preferredLiveMode} preferredLiveMode={preferredLiveMode}
/> />
</div> </div>