mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-01-21 00:06:44 +01:00
Optimistic UI (#10825)
* debounce motion only button * implement custom hook * optimistic severity toggle * optimistic reviewed switch
This commit is contained in:
parent
0096a6d778
commit
fbc0da6016
@ -2,7 +2,7 @@ import { Button } from "../ui/button";
|
|||||||
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
|
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { CameraGroupConfig, FrigateConfig } from "@/types/frigateConfig";
|
import { CameraGroupConfig, FrigateConfig } from "@/types/frigateConfig";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@ -29,6 +29,7 @@ import ReviewActivityCalendar from "../overlay/ReviewActivityCalendar";
|
|||||||
import MobileReviewSettingsDrawer, {
|
import MobileReviewSettingsDrawer, {
|
||||||
DrawerFeatures,
|
DrawerFeatures,
|
||||||
} from "../overlay/MobileReviewSettingsDrawer";
|
} from "../overlay/MobileReviewSettingsDrawer";
|
||||||
|
import useOptimisticState from "@/hooks/use-optimistic-state";
|
||||||
|
|
||||||
const REVIEW_FILTERS = [
|
const REVIEW_FILTERS = [
|
||||||
"cameras",
|
"cameras",
|
||||||
@ -361,13 +362,19 @@ function ShowReviewFilter({
|
|||||||
showReviewed,
|
showReviewed,
|
||||||
setShowReviewed,
|
setShowReviewed,
|
||||||
}: ShowReviewedFilterProps) {
|
}: ShowReviewedFilterProps) {
|
||||||
|
const [showReviewedSwitch, setShowReviewedSwitch] = useOptimisticState(
|
||||||
|
showReviewed,
|
||||||
|
setShowReviewed,
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="hidden h-9 md:flex p-2 justify-start items-center text-sm bg-secondary hover:bg-secondary/80 text-secondary-foreground rounded-md cursor-pointer">
|
<div className="hidden h-9 md:flex p-2 justify-start items-center text-sm bg-secondary hover:bg-secondary/80 text-secondary-foreground rounded-md cursor-pointer">
|
||||||
<Switch
|
<Switch
|
||||||
id="reviewed"
|
id="reviewed"
|
||||||
checked={showReviewed == 1}
|
checked={showReviewedSwitch == 1}
|
||||||
onCheckedChange={() => setShowReviewed(showReviewed == 0 ? 1 : 0)}
|
onCheckedChange={() =>
|
||||||
|
setShowReviewedSwitch(showReviewedSwitch == 0 ? 1 : 0)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Label className="ml-2 cursor-pointer" htmlFor="reviewed">
|
<Label className="ml-2 cursor-pointer" htmlFor="reviewed">
|
||||||
Show Reviewed
|
Show Reviewed
|
||||||
@ -631,11 +638,9 @@ function ShowMotionOnlyButton({
|
|||||||
motionOnly,
|
motionOnly,
|
||||||
setMotionOnly,
|
setMotionOnly,
|
||||||
}: ShowMotionOnlyButtonProps) {
|
}: ShowMotionOnlyButtonProps) {
|
||||||
const [motionOnlyButton, setMotionOnlyButton] = useState(motionOnly);
|
const [motionOnlyButton, setMotionOnlyButton] = useOptimisticState(
|
||||||
|
motionOnly,
|
||||||
useEffect(
|
setMotionOnly,
|
||||||
() => setMotionOnly(motionOnlyButton),
|
|
||||||
[motionOnlyButton, setMotionOnly],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
43
web/src/hooks/use-optimistic-state.ts
Normal file
43
web/src/hooks/use-optimistic-state.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { useState, useEffect, useCallback, useRef } from "react";
|
||||||
|
|
||||||
|
type OptimisticStateResult<T> = [T, (newValue: T) => void];
|
||||||
|
|
||||||
|
const useOptimisticState = <T>(
|
||||||
|
initialState: T,
|
||||||
|
setState: (newValue: T) => void,
|
||||||
|
delay: number = 20,
|
||||||
|
): OptimisticStateResult<T> => {
|
||||||
|
const [optimisticValue, setOptimisticValue] = useState<T>(initialState);
|
||||||
|
const debounceTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
|
const handleValueChange = useCallback(
|
||||||
|
(newValue: T) => {
|
||||||
|
// Update the optimistic value immediately
|
||||||
|
setOptimisticValue(newValue);
|
||||||
|
|
||||||
|
// Clear any pending debounce timeout
|
||||||
|
if (debounceTimeout.current) {
|
||||||
|
clearTimeout(debounceTimeout.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set a new debounce timeout
|
||||||
|
debounceTimeout.current = setTimeout(() => {
|
||||||
|
// Update the actual value using the provided setter function
|
||||||
|
setState(newValue);
|
||||||
|
}, delay);
|
||||||
|
},
|
||||||
|
[delay, setState],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (debounceTimeout.current) {
|
||||||
|
clearTimeout(debounceTimeout.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return [optimisticValue, handleValueChange];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useOptimisticState;
|
@ -41,6 +41,7 @@ import { RecordingStartingPoint } from "@/types/record";
|
|||||||
import VideoControls from "@/components/player/VideoControls";
|
import VideoControls from "@/components/player/VideoControls";
|
||||||
import { TimeRange } from "@/types/timeline";
|
import { TimeRange } from "@/types/timeline";
|
||||||
import { useCameraMotionNextTimestamp } from "@/hooks/use-camera-activity";
|
import { useCameraMotionNextTimestamp } from "@/hooks/use-camera-activity";
|
||||||
|
import useOptimisticState from "@/hooks/use-optimistic-state";
|
||||||
|
|
||||||
type EventViewProps = {
|
type EventViewProps = {
|
||||||
reviews?: ReviewSegment[];
|
reviews?: ReviewSegment[];
|
||||||
@ -199,6 +200,11 @@ export default function EventView({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const [motionOnly, setMotionOnly] = useState(false);
|
const [motionOnly, setMotionOnly] = useState(false);
|
||||||
|
const [severityToggle, setSeverityToggle] = useOptimisticState(
|
||||||
|
severity,
|
||||||
|
setSeverity,
|
||||||
|
100,
|
||||||
|
);
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
return <ActivityIndicator />;
|
return <ActivityIndicator />;
|
||||||
@ -214,13 +220,13 @@ export default function EventView({
|
|||||||
className="*:px-3 *:py-4 *:rounded-md"
|
className="*:px-3 *:py-4 *:rounded-md"
|
||||||
type="single"
|
type="single"
|
||||||
size="sm"
|
size="sm"
|
||||||
value={severity}
|
value={severityToggle}
|
||||||
onValueChange={(value: ReviewSeverity) =>
|
onValueChange={(value: ReviewSeverity) =>
|
||||||
value ? setSeverity(value) : null
|
value ? setSeverityToggle(value) : null
|
||||||
} // don't allow the severity to be unselected
|
} // don't allow the severity to be unselected
|
||||||
>
|
>
|
||||||
<ToggleGroupItem
|
<ToggleGroupItem
|
||||||
className={`${severity == "alert" ? "" : "text-gray-500"}`}
|
className={`${severityToggle == "alert" ? "" : "text-gray-500"}`}
|
||||||
value="alert"
|
value="alert"
|
||||||
aria-label="Select alerts"
|
aria-label="Select alerts"
|
||||||
>
|
>
|
||||||
@ -230,7 +236,7 @@ export default function EventView({
|
|||||||
</div>
|
</div>
|
||||||
</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
<ToggleGroupItem
|
<ToggleGroupItem
|
||||||
className={`${severity == "detection" ? "" : "text-gray-500"}`}
|
className={`${severityToggle == "detection" ? "" : "text-gray-500"}`}
|
||||||
value="detection"
|
value="detection"
|
||||||
aria-label="Select detections"
|
aria-label="Select detections"
|
||||||
>
|
>
|
||||||
@ -242,7 +248,7 @@ export default function EventView({
|
|||||||
</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
<ToggleGroupItem
|
<ToggleGroupItem
|
||||||
className={`px-3 py-4 rounded-2xl ${
|
className={`px-3 py-4 rounded-2xl ${
|
||||||
severity == "significant_motion" ? "" : "text-gray-500"
|
severityToggle == "significant_motion" ? "" : "text-gray-500"
|
||||||
}`}
|
}`}
|
||||||
value="significant_motion"
|
value="significant_motion"
|
||||||
aria-label="Select motion"
|
aria-label="Select motion"
|
||||||
|
Loading…
Reference in New Issue
Block a user