forked from Github/frigate
Webui cleanups (#8991)
* Fix mobile event timeago * Reduce preview playback rate for safari browser * Fix dashboard buttons * Update recent events correctly * Fix opening page on icon toggle * Fix video player remote playback check * fix history image * Add sticky headers to history page * Fix iOS empty frame * reduce duplicate items and improve time format * Organize data more effictively and ensure data is not overwritten * Use icon to indicate preview
This commit is contained in:
committed by
Blake Blackshear
parent
bdebb99b5a
commit
f8d114cd33
@@ -21,8 +21,9 @@ function ConfigEditor() {
|
||||
const [success, setSuccess] = useState<string | undefined>();
|
||||
const [error, setError] = useState<string | undefined>();
|
||||
|
||||
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>();
|
||||
const modelRef = useRef<monaco.editor.IEditorModel | null>();
|
||||
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
|
||||
const modelRef = useRef<monaco.editor.IEditorModel | null>(null);
|
||||
const configRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const onHandleSaveConfig = useCallback(
|
||||
async (save_option: SaveOptions) => {
|
||||
@@ -72,6 +73,7 @@ function ConfigEditor() {
|
||||
|
||||
if (modelRef.current != null) {
|
||||
// we don't need to recreate the editor if it already exists
|
||||
editorRef.current?.layout();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -97,9 +99,9 @@ function ConfigEditor() {
|
||||
],
|
||||
});
|
||||
|
||||
const container = document.getElementById("container");
|
||||
const container = configRef.current;
|
||||
|
||||
if (container != undefined) {
|
||||
if (container != null) {
|
||||
editorRef.current = monaco.editor.create(container, {
|
||||
language: "yaml",
|
||||
model: modelRef.current,
|
||||
@@ -107,6 +109,12 @@ function ConfigEditor() {
|
||||
theme: theme == "dark" ? "vs-dark" : "vs-light",
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
configRef.current = null;
|
||||
editorRef.current = null;
|
||||
modelRef.current = null;
|
||||
};
|
||||
});
|
||||
|
||||
if (!config) {
|
||||
@@ -149,7 +157,7 @@ function ConfigEditor() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div id="container" className="h-full mt-2" />
|
||||
<div ref={configRef} className="h-full mt-2" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,18 +18,15 @@ import { FaWalking } from "react-icons/fa";
|
||||
import { LuEar } from "react-icons/lu";
|
||||
import { TbMovie } from "react-icons/tb";
|
||||
import MiniEventCard from "@/components/card/MiniEventCard";
|
||||
import { Event } from "@/types/event";
|
||||
import { Event as FrigateEvent } from "@/types/event";
|
||||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
||||
|
||||
export function Dashboard() {
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
|
||||
const recentTimestamp = useMemo(() => {
|
||||
const now = new Date();
|
||||
now.setMinutes(now.getMinutes() - 30);
|
||||
return now.getTime() / 1000;
|
||||
}, []);
|
||||
const { data: events, mutate: updateEvents } = useSWR<Event[]>([
|
||||
const now = new Date();
|
||||
now.setMinutes(now.getMinutes() - 30, 0, 0);
|
||||
const recentTimestamp = now.getTime() / 1000;
|
||||
const { data: events, mutate: updateEvents } = useSWR<FrigateEvent[]>([
|
||||
"events",
|
||||
{ limit: 10, after: recentTimestamp },
|
||||
]);
|
||||
@@ -97,7 +94,7 @@ function Camera({ camera }: { camera: CameraConfig }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="">
|
||||
<Card>
|
||||
<a href={`/live/${camera.name}`}>
|
||||
<AspectRatio
|
||||
ratio={16 / 9}
|
||||
@@ -116,7 +113,11 @@ function Camera({ camera }: { camera: CameraConfig }) {
|
||||
className={`${
|
||||
detectValue == "ON" ? "text-primary" : "text-gray-400"
|
||||
}`}
|
||||
onClick={() => sendDetect(detectValue == "ON" ? "OFF" : "ON")}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
sendDetect(detectValue == "ON" ? "OFF" : "ON");
|
||||
}}
|
||||
>
|
||||
<FaWalking />
|
||||
</Button>
|
||||
@@ -130,11 +131,13 @@ function Camera({ camera }: { camera: CameraConfig }) {
|
||||
: "text-gray-400"
|
||||
: "text-red-500"
|
||||
}
|
||||
onClick={() =>
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
camera.record.enabled_in_config
|
||||
? sendRecord(recordValue == "ON" ? "OFF" : "ON")
|
||||
: {}
|
||||
}
|
||||
: {};
|
||||
}}
|
||||
>
|
||||
<TbMovie />
|
||||
</Button>
|
||||
@@ -144,7 +147,11 @@ function Camera({ camera }: { camera: CameraConfig }) {
|
||||
className={`${
|
||||
snapshotValue == "ON" ? "text-primary" : "text-gray-400"
|
||||
}`}
|
||||
onClick={() => sendSnapshot(detectValue == "ON" ? "OFF" : "ON")}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
sendSnapshot(detectValue == "ON" ? "OFF" : "ON");
|
||||
}}
|
||||
>
|
||||
<AiOutlinePicture />
|
||||
</Button>
|
||||
@@ -155,7 +162,11 @@ function Camera({ camera }: { camera: CameraConfig }) {
|
||||
className={`${
|
||||
audioValue == "ON" ? "text-primary" : "text-gray-400"
|
||||
}`}
|
||||
onClick={() => sendAudio(detectValue == "ON" ? "OFF" : "ON")}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
sendAudio(detectValue == "ON" ? "OFF" : "ON");
|
||||
}}
|
||||
>
|
||||
<LuEar />
|
||||
</Button>
|
||||
|
||||
@@ -7,9 +7,10 @@ import ActivityIndicator from "@/components/ui/activity-indicator";
|
||||
import HistoryCard from "@/components/card/HistoryCard";
|
||||
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
|
||||
import axios from "axios";
|
||||
import TimelinePlayerCard from "@/components/card/TimelineCardPlayer";
|
||||
import TimelinePlayerCard from "@/components/card/TimelinePlayerCard";
|
||||
import { getHourlyTimelineData } from "@/utils/historyUtil";
|
||||
|
||||
const API_LIMIT = 120;
|
||||
const API_LIMIT = 200;
|
||||
|
||||
function History() {
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
@@ -59,83 +60,7 @@ function History() {
|
||||
return [];
|
||||
}
|
||||
|
||||
const cards: CardsData = {};
|
||||
timelinePages.forEach((hourlyTimeline) => {
|
||||
Object.keys(hourlyTimeline["hours"])
|
||||
.reverse()
|
||||
.forEach((hour) => {
|
||||
const day = new Date(parseInt(hour) * 1000);
|
||||
day.setHours(0, 0, 0, 0);
|
||||
const dayKey = (day.getTime() / 1000).toString();
|
||||
const source_to_types: { [key: string]: string[] } = {};
|
||||
Object.values(hourlyTimeline["hours"][hour]).forEach((i) => {
|
||||
const time = new Date(i.timestamp * 1000);
|
||||
time.setSeconds(0);
|
||||
time.setMilliseconds(0);
|
||||
const key = `${i.source_id}-${time.getMinutes()}`;
|
||||
if (key in source_to_types) {
|
||||
source_to_types[key].push(i.class_type);
|
||||
} else {
|
||||
source_to_types[key] = [i.class_type];
|
||||
}
|
||||
});
|
||||
|
||||
if (!Object.keys(cards).includes(dayKey)) {
|
||||
cards[dayKey] = {};
|
||||
}
|
||||
cards[dayKey][hour] = {};
|
||||
Object.values(hourlyTimeline["hours"][hour]).forEach((i) => {
|
||||
const time = new Date(i.timestamp * 1000);
|
||||
const key = `${i.camera}-${time.getMinutes()}`;
|
||||
|
||||
// detail level for saving items
|
||||
// detail level determines which timeline items for each moment is returned
|
||||
// values can be normal, extra, or full
|
||||
// normal: return all items except active / attribute / gone / stationary / visible unless that is the only item.
|
||||
// extra: return all items except attribute / gone / visible unless that is the only item
|
||||
// full: return all items
|
||||
|
||||
let add = true;
|
||||
if (detailLevel == "normal") {
|
||||
if (
|
||||
source_to_types[`${i.source_id}-${time.getMinutes()}`].length >
|
||||
1 &&
|
||||
[
|
||||
"active",
|
||||
"attribute",
|
||||
"gone",
|
||||
"stationary",
|
||||
"visible",
|
||||
].includes(i.class_type)
|
||||
) {
|
||||
add = false;
|
||||
}
|
||||
} else if (detailLevel == "extra") {
|
||||
if (
|
||||
source_to_types[`${i.source_id}-${time.getMinutes()}`].length >
|
||||
1 &&
|
||||
i.class_type in ["attribute", "gone", "visible"]
|
||||
) {
|
||||
add = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (add) {
|
||||
if (key in cards[dayKey][hour]) {
|
||||
cards[dayKey][hour][key].entries.push(i);
|
||||
} else {
|
||||
cards[dayKey][hour][key] = {
|
||||
camera: i.camera,
|
||||
time: time.getTime() / 1000,
|
||||
entries: [i],
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return cards;
|
||||
return getHourlyTimelineData(timelinePages, detailLevel);
|
||||
}, [detailLevel, timelinePages]);
|
||||
|
||||
const isDone =
|
||||
@@ -168,9 +93,6 @@ function History() {
|
||||
return (
|
||||
<>
|
||||
<Heading as="h2">Review</Heading>
|
||||
<div className="text-xs mb-4">
|
||||
Dates and times are based on the timezone {timezone}
|
||||
</div>
|
||||
|
||||
<TimelinePlayerCard
|
||||
timeline={playback}
|
||||
@@ -183,7 +105,10 @@ function History() {
|
||||
.map(([day, timelineDay], dayIdx) => {
|
||||
return (
|
||||
<div key={day}>
|
||||
<Heading as="h3">
|
||||
<Heading
|
||||
className="sticky py-2 -top-4 left-0 bg-background w-full z-10"
|
||||
as="h3"
|
||||
>
|
||||
{formatUnixTimestampToDateTime(parseInt(day), {
|
||||
strftime_fmt: "%A %b %d",
|
||||
time_style: "medium",
|
||||
@@ -206,7 +131,10 @@ function History() {
|
||||
<div key={hour} ref={lastRow ? lastTimelineRef : null}>
|
||||
<Heading as="h4">
|
||||
{formatUnixTimestampToDateTime(parseInt(hour), {
|
||||
strftime_fmt: "%I:00",
|
||||
strftime_fmt:
|
||||
config.ui.time_format == "24hour"
|
||||
? "%H:00"
|
||||
: "%I:00 %p",
|
||||
time_style: "medium",
|
||||
date_style: "medium",
|
||||
})}
|
||||
@@ -229,6 +157,7 @@ function History() {
|
||||
preview.end > startTs
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<HistoryCard
|
||||
key={key}
|
||||
|
||||
Reference in New Issue
Block a user