mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-07-30 13:48:07 +02:00
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:
parent
93260f6cfd
commit
380b15b286
@ -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)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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];
|
||||||
|
}
|
||||||
|
@ -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(
|
||||||
|
@ -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,
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user