From f4310862aa036ae32b74fe8f38b31ded615e0eed Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Mon, 5 Feb 2024 16:54:08 -0700 Subject: [PATCH] WebUI Improvements and fixes (#9613) * Show toast instead of text for success and errors * Show correct times * Start playing next hour when current hour ends * Fix refreshing camera image * Fix timeline --- web/package-lock.json | 311 +++++++++++++++++- web/package.json | 2 + .../components/camera/DynamicCameraImage.tsx | 8 +- .../components/player/DynamicVideoPlayer.tsx | 12 + web/src/components/ui/sonner.tsx | 32 ++ web/src/pages/ConfigEditor.tsx | 9 +- web/src/pages/Export.tsx | 56 ++-- web/src/utils/dateUtil.ts | 9 +- web/src/utils/historyUtil.ts | 4 +- web/src/views/history/DesktopTimelineView.tsx | 49 ++- 10 files changed, 435 insertions(+), 57 deletions(-) create mode 100644 web/src/components/ui/sonner.tsx diff --git a/web/package-lock.json b/web/package-lock.json index 49085680b..4073ae3a6 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -34,6 +34,7 @@ "immer": "^10.0.3", "lucide-react": "^0.294.0", "monaco-yaml": "^5.1.0", + "next-themes": "^0.2.1", "react": "^18.2.0", "react-apexcharts": "^1.4.1", "react-day-picker": "^8.9.1", @@ -43,6 +44,7 @@ "react-router-dom": "^6.20.1", "react-use-websocket": "^4.5.0", "recoil": "^0.7.7", + "sonner": "^1.4.0", "sort-by": "^1.2.0", "strftime": "^0.10.2", "swr": "^2.2.4", @@ -804,6 +806,156 @@ "node": ">=18" } }, + "node_modules/@next/env": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.1.0.tgz", + "integrity": "sha512-Py8zIo+02ht82brwwhTg36iogzFqGLPXlRGKQw5s+qP/kMNc4MAyDeEwBKDijk6zTIbegEgu8Qy7C1LboslQAw==", + "peer": true + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.1.0.tgz", + "integrity": "sha512-nUDn7TOGcIeyQni6lZHfzNoo9S0euXnu0jhsbMOmMJUBfgsnESdjN97kM7cBqQxZa8L/bM9om/S5/1dzCrW6wQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.1.0.tgz", + "integrity": "sha512-1jgudN5haWxiAl3O1ljUS2GfupPmcftu2RYJqZiMJmmbBT5M1XDffjUtRUzP4W3cBHsrvkfOFdQ71hAreNQP6g==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.1.0.tgz", + "integrity": "sha512-RHo7Tcj+jllXUbK7xk2NyIDod3YcCPDZxj1WLIYxd709BQ7WuRYl3OWUNG+WUfqeQBds6kvZYlc42NJJTNi4tQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.1.0.tgz", + "integrity": "sha512-v6kP8sHYxjO8RwHmWMJSq7VZP2nYCkRVQ0qolh2l6xroe9QjbgV8siTbduED4u0hlk0+tjS6/Tuy4n5XCp+l6g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.1.0.tgz", + "integrity": "sha512-zJ2pnoFYB1F4vmEVlb/eSe+VH679zT1VdXlZKX+pE66grOgjmKJHKacf82g/sWE4MQ4Rk2FMBCRnX+l6/TVYzQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.1.0.tgz", + "integrity": "sha512-rbaIYFt2X9YZBSbH/CwGAjbBG2/MrACCVu2X0+kSykHzHnYH5FjHxwXLkcoJ10cX0aWCEynpu+rP76x0914atg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.1.0.tgz", + "integrity": "sha512-o1N5TsYc8f/HpGt39OUQpQ9AKIGApd3QLueu7hXk//2xq5Z9OxmV6sQfNp8C7qYmiOlHYODOGqNNa0e9jvchGQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.1.0.tgz", + "integrity": "sha512-XXIuB1DBRCFwNO6EEzCTMHT5pauwaSj4SWs7CYnME57eaReAKBXCnkUE80p/pAZcewm7hs+vGvNqDPacEXHVkw==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.1.0.tgz", + "integrity": "sha512-9WEbVRRAqJ3YFVqEZIxUqkiO8l1nool1LmNxygr5HWF8AcSYsEpneUDhmjUVJEzO2A04+oPtZdombzzPPkTtgg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2191,6 +2343,15 @@ "integrity": "sha512-9F4ys4C74eSTEUNndnER3VJ15oru2NumfQxS8geE+f3eB5xvfxpWyqE5XlVnxb/R14uoXi6SLbBwwiDSkv+XEw==", "dev": true }, + "node_modules/@swc/helpers": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", + "integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==", + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@swc/types": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz", @@ -3211,6 +3372,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "peer": true, + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -3238,10 +3411,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001566", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001566.tgz", - "integrity": "sha512-ggIhCsTxmITBAMmK8yZjEhCO5/47jKXPu6Dha/wuCS4JePVL+3uiDEBuhu2aIoT+bqTOR8L76Ip1ARL9xYsEJA==", - "dev": true, + "version": "1.0.30001583", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001583.tgz", + "integrity": "sha512-acWTYaha8xfhA/Du/z4sNZjHUWjkiuoAi2LM+T/aL+kemKQgPT1xBb/YKjlQ0Qo8gvbHsGNplrEJ+9G3gL7i4Q==", "funding": [ { "type": "opencollective", @@ -4635,6 +4807,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "peer": true + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -5804,6 +5982,90 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/next": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/next/-/next-14.1.0.tgz", + "integrity": "sha512-wlzrsbfeSU48YQBjZhDzOwhWhGsy+uQycR8bHAOt1LY1bn3zZEcDyHQOEoN3aWzQ8LHCAJ1nqrWCc9XF2+O45Q==", + "peer": true, + "dependencies": { + "@next/env": "14.1.0", + "@swc/helpers": "0.5.2", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001579", + "graceful-fs": "^4.2.11", + "postcss": "8.4.31", + "styled-jsx": "5.1.1" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=18.17.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "14.1.0", + "@next/swc-darwin-x64": "14.1.0", + "@next/swc-linux-arm64-gnu": "14.1.0", + "@next/swc-linux-arm64-musl": "14.1.0", + "@next/swc-linux-x64-gnu": "14.1.0", + "@next/swc-linux-x64-musl": "14.1.0", + "@next/swc-win32-arm64-msvc": "14.1.0", + "@next/swc-win32-ia32-msvc": "14.1.0", + "@next/swc-win32-x64-msvc": "14.1.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next-themes": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.2.1.tgz", + "integrity": "sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==", + "peerDependencies": { + "next": "*", + "react": "*", + "react-dom": "*" + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "peer": true, + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/node-releases": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", @@ -7112,6 +7374,15 @@ "node": ">=8" } }, + "node_modules/sonner": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.4.0.tgz", + "integrity": "sha512-nvkTsIuOmi9e5Wz5If8ldasJjZNVfwiXYijBi2dbijvTQnQppvMcXTFNxL/NUFWlI2yJ1JX7TREDsg+gYm9WyA==", + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/sort-by": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/sort-by/-/sort-by-1.2.0.tgz", @@ -7158,6 +7429,15 @@ "integrity": "sha512-aFZ19IgVmhdB2uX599ve2kE6BIE3YMnQ6Gp6BURhW/oIzpXGKr878TQfAQZn1+i0Flcc/UKUy1gOlcfaUBCryg==", "dev": true }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "peer": true, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/strftime": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/strftime/-/strftime-0.10.2.tgz", @@ -7255,6 +7535,29 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/styled-jsx": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", + "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", + "peer": true, + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, "node_modules/sucrase": { "version": "3.34.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz", diff --git a/web/package.json b/web/package.json index 8acc21769..b7734e3ab 100644 --- a/web/package.json +++ b/web/package.json @@ -39,6 +39,7 @@ "immer": "^10.0.3", "lucide-react": "^0.294.0", "monaco-yaml": "^5.1.0", + "next-themes": "^0.2.1", "react": "^18.2.0", "react-apexcharts": "^1.4.1", "react-day-picker": "^8.9.1", @@ -48,6 +49,7 @@ "react-router-dom": "^6.20.1", "react-use-websocket": "^4.5.0", "recoil": "^0.7.7", + "sonner": "^1.4.0", "sort-by": "^1.2.0", "strftime": "^0.10.2", "swr": "^2.2.4", diff --git a/web/src/components/camera/DynamicCameraImage.tsx b/web/src/components/camera/DynamicCameraImage.tsx index 5bc05f83f..0ef249964 100644 --- a/web/src/components/camera/DynamicCameraImage.tsx +++ b/web/src/components/camera/DynamicCameraImage.tsx @@ -24,6 +24,9 @@ export default function DynamicCameraImage({ aspect, }: DynamicCameraImageProps) { const [key, setKey] = useState(Date.now()); + const [timeoutId, setTimeoutId] = useState( + undefined + ); const [activeObjects, setActiveObjects] = useState([]); const hasActiveObjects = useMemo( () => activeObjects.length > 0, @@ -58,6 +61,8 @@ export default function DynamicCameraImage({ if (eventIndex == -1) { const newActiveObjects = [...activeObjects, event.after.id]; setActiveObjects(newActiveObjects); + clearTimeout(timeoutId); + setKey(Date.now()); } } } @@ -69,12 +74,13 @@ export default function DynamicCameraImage({ ? INTERVAL_ACTIVE_MS : INTERVAL_INACTIVE_MS; - setTimeout( + const tId = setTimeout( () => { setKey(Date.now()); }, loadTime > loadInterval ? 1 : loadInterval ); + setTimeoutId(tId); }, [key]); return ( diff --git a/web/src/components/player/DynamicVideoPlayer.tsx b/web/src/components/player/DynamicVideoPlayer.tsx index c2a9503fc..306f10392 100644 --- a/web/src/components/player/DynamicVideoPlayer.tsx +++ b/web/src/components/player/DynamicVideoPlayer.tsx @@ -228,6 +228,7 @@ export default function DynamicVideoPlayer({ player.on("timeupdate", () => { controller.updateProgress(player.currentTime() || 0); }); + player.on("ended", () => controller.fireClipEndEvent()); if (onControllerReady) { onControllerReady(controller); @@ -284,6 +285,7 @@ export class DynamicVideoController { // playback private recordings: Recording[] = []; private onPlaybackTimestamp: ((time: number) => void) | undefined = undefined; + private onClipEnded: (() => void) | undefined = undefined; private annotationOffset: number; private timeToStart: number | undefined = undefined; @@ -393,6 +395,16 @@ export class DynamicVideoController { this.onPlaybackTimestamp = listener; } + onClipEndedEvent(listener: () => void) { + this.onClipEnded = listener; + } + + fireClipEndEvent() { + if (this.onClipEnded) { + this.onClipEnded(); + } + } + scrubToTimestamp(time: number) { if (this.playerMode != "scrubbing") { this.playerMode = "scrubbing"; diff --git a/web/src/components/ui/sonner.tsx b/web/src/components/ui/sonner.tsx new file mode 100644 index 000000000..1b0ae457a --- /dev/null +++ b/web/src/components/ui/sonner.tsx @@ -0,0 +1,32 @@ +import { useTheme } from "next-themes"; +import { Toaster as Sonner } from "sonner"; + +type ToasterProps = React.ComponentProps; + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = "system" } = useTheme(); + + return ( + + ); +}; + +export { Toaster }; diff --git a/web/src/pages/ConfigEditor.tsx b/web/src/pages/ConfigEditor.tsx index 629570d91..7b35f3ed8 100644 --- a/web/src/pages/ConfigEditor.tsx +++ b/web/src/pages/ConfigEditor.tsx @@ -9,6 +9,8 @@ import { Button } from "@/components/ui/button"; import axios from "axios"; import copy from "copy-to-clipboard"; import { useTheme } from "@/context/theme-provider"; +import { Toaster } from "@/components/ui/sonner"; +import { toast } from "sonner"; type SaveOptions = "saveonly" | "restart"; @@ -18,7 +20,6 @@ function ConfigEditor() { const { data: config } = useSWR("config/raw"); const { theme } = useTheme(); - const [success, setSuccess] = useState(); const [error, setError] = useState(); const editorRef = useRef(null); @@ -42,11 +43,11 @@ function ConfigEditor() { .then((response) => { if (response.status === 200) { setError(""); - setSuccess(response.data.message); + toast.success(response.data.message, { position: "top-center" }); } }) .catch((error) => { - setSuccess(""); + toast.error("Error saving config", { position: "top-center" }); if (error.response) { setError(error.response.data.message); @@ -150,7 +151,6 @@ function ConfigEditor() { - {success &&
{success}
} {error && (
{error} @@ -158,6 +158,7 @@ function ConfigEditor() { )}
+
); } diff --git a/web/src/pages/Export.tsx b/web/src/pages/Export.tsx index e0cbbabc4..a2ca53a9c 100644 --- a/web/src/pages/Export.tsx +++ b/web/src/pages/Export.tsx @@ -34,11 +34,13 @@ import { PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; +import { Toaster } from "@/components/ui/sonner"; import { FrigateConfig } from "@/types/frigateConfig"; import axios from "axios"; import { format } from "date-fns"; import { useCallback, useState } from "react"; import { DateRange } from "react-day-picker"; +import { toast } from "sonner"; import useSWR from "swr"; type ExportItem = { @@ -55,7 +57,6 @@ function Export() { // Export States const [camera, setCamera] = useState(); const [playback, setPlayback] = useState(); - const [message, setMessage] = useState({ text: "", error: false }); const currentDate = new Date(); currentDate.setHours(0, 0, 0, 0); @@ -70,23 +71,21 @@ function Export() { const [deleteClip, setDeleteClip] = useState(); const onHandleExport = () => { - if (camera == "select") { - setMessage({ text: "A camera needs to be selected.", error: true }); + if (!camera) { + toast.error("A camera needs to be selected.", { position: "top-center" }); return; } - if (playback == "select") { - setMessage({ - text: "A playback factor needs to be selected.", - error: true, + if (!playback) { + toast.error("A playback factor needs to be selected.", { + position: "top-center", }); return; } if (!date?.from || !startTime || !endTime) { - setMessage({ - text: "A start and end time needs to be selected", - error: true, + toast.error("A start and end time needs to be selected", { + position: "top-center", }); return; } @@ -106,9 +105,8 @@ function Export() { const end = endDate.getTime() / 1000; if (end <= start) { - setMessage({ - text: "The end time must be after the start time.", - error: true, + toast.error("The end time must be after the start time.", { + position: "top-center", }); return; } @@ -117,24 +115,23 @@ function Export() { .post(`export/${camera}/start/${start}/end/${end}`, { playback }) .then((response) => { if (response.status == 200) { - setMessage({ - text: "Successfully started export. View the file in the /exports folder.", - error: false, - }); + toast.success( + "Successfully started export. View the file in the /exports folder.", + { position: "top-center" } + ); } mutate(); }) .catch((error) => { if (error.response?.data?.message) { - setMessage({ - text: `Failed to start export: ${error.response.data.message}`, - error: true, - }); + toast.error( + `Failed to start export: ${error.response.data.message}`, + { position: "top-center" } + ); } else { - setMessage({ - text: `Failed to start export: ${error.message}`, - error: true, + toast.error(`Failed to start export: ${error.message}`, { + position: "top-center", }); } }); @@ -156,16 +153,7 @@ function Export() { return ( <> Export - - {message.text && ( -
- {message.text} -
- )} + { const card = Object.values(cards)[0]; if (card == undefined || card.time < start || card.time > end) { diff --git a/web/src/views/history/DesktopTimelineView.tsx b/web/src/views/history/DesktopTimelineView.tsx index 2c3d1ae17..0cabd35ac 100644 --- a/web/src/views/history/DesktopTimelineView.tsx +++ b/web/src/views/history/DesktopTimelineView.tsx @@ -34,9 +34,6 @@ export default function DesktopTimelineView({ const controllerRef = useRef(undefined); const initialScrollRef = useRef(null); - const [selectedPlayback, setSelectedPlayback] = useState(initialPlayback); - const [timelineTime, setTimelineTime] = useState(0); - // handle scrolling to initial timeline item useEffect(() => { if (initialScrollRef.current != null) { @@ -50,17 +47,45 @@ export default function DesktopTimelineView({ }); }, []); + const [timelineTime, setTimelineTime] = useState(0); const timelineStack = useMemo( () => getTimelineHoursForDay( - selectedPlayback.camera, + initialPlayback.camera, timelineData, cameraPreviews, - selectedPlayback.range.start + 60 + initialPlayback.range.start + 60 ), [] ); + const [selectedPlaybackIdx, setSelectedPlaybackIdx] = useState( + timelineStack.playbackItems.findIndex((playback) => { + return ( + playback.range.start == initialPlayback.range.start && + playback.range.end == initialPlayback.range.end + ); + }) + ); + const selectedPlayback = useMemo( + () => timelineStack.playbackItems[selectedPlaybackIdx], + [selectedPlaybackIdx] + ); + + // handle moving to next clip + useEffect(() => { + if (!controllerRef.current) { + return; + } + + if (selectedPlaybackIdx > 0) { + controllerRef.current.onClipEndedEvent(() => { + console.log("setting to " + (selectedPlaybackIdx - 1)); + setSelectedPlaybackIdx(selectedPlaybackIdx - 1); + }); + } + }, [controllerRef, selectedPlaybackIdx]); + const { data: activity } = useSWR( [ `${initialPlayback.camera}/recording/hourly/activity`, @@ -148,12 +173,14 @@ export default function DesktopTimelineView({
- {timelineStack.playbackItems.map((timeline) => { + {timelineStack.playbackItems.map((timeline, tIdx) => { const isInitiallySelected = initialPlayback.range.start == timeline.range.start; const isSelected = timeline.range.start == selectedPlayback.range.start; const graphData = timelineGraphData[timeline.range.start]; + const start = new Date(timeline.range.start * 1000); + const end = new Date(timeline.range.end * 1000); return (
{ - setSelectedPlayback(timeline); + setSelectedPlaybackIdx(tIdx); let startTs; if (timeline.timelineItems.length > 0) {