+
+
+
+ );
+}
+
+export default memo(Close);
diff --git a/web/src/index.css b/web/src/index.css
index 1ccb2fad7..2278ef964 100644
--- a/web/src/index.css
+++ b/web/src/index.css
@@ -29,3 +29,12 @@
.jsmpeg canvas {
position: static !important;
}
+
+/*
+Event.js
+Maintain aspect ratio and scale down the video container
+Could not find a proper tailwind css.
+*/
+.outer-max-width {
+ max-width: 60%;
+}
diff --git a/web/src/routes/Event.jsx b/web/src/routes/Event.jsx
index 3cbe4e60f..06025d75e 100644
--- a/web/src/routes/Event.jsx
+++ b/web/src/routes/Event.jsx
@@ -1,25 +1,32 @@
import { h, Fragment } from 'preact';
-import { useCallback, useState } from 'preact/hooks';
-import { route } from 'preact-router';
+import { useCallback, useState, useEffect } from 'preact/hooks';
import ActivityIndicator from '../components/ActivityIndicator';
import Button from '../components/Button';
import Clip from '../icons/Clip';
+import Close from '../icons/Close';
import Delete from '../icons/Delete';
import Snapshot from '../icons/Snapshot';
import Dialog from '../components/Dialog';
import Heading from '../components/Heading';
-import Link from '../components/Link';
import VideoPlayer from '../components/VideoPlayer';
import { FetchStatus, useApiHost, useEvent, useDelete } from '../api';
-import { Table, Thead, Tbody, Th, Tr, Td } from '../components/Table';
-export default function Event({ eventId }) {
+export default function Event({ eventId, close, scrollRef }) {
const apiHost = useApiHost();
const { data, status } = useEvent(eventId);
const [showDialog, setShowDialog] = useState(false);
+ const [shouldScroll, setShouldScroll] = useState(true);
const [deleteStatus, setDeleteStatus] = useState(FetchStatus.NONE);
const setDeleteEvent = useDelete();
+ useEffect(() => {
+ // Scroll event into view when component has been mounted.
+ if (shouldScroll && scrollRef && scrollRef[eventId]) {
+ scrollRef[eventId].scrollIntoView();
+ setShouldScroll(false);
+ }
+ }, [data, scrollRef, eventId, shouldScroll]);
+
const handleClickDelete = () => {
setShowDialog(true);
};
@@ -40,7 +47,6 @@ export default function Event({ eventId }) {
if (success) {
setDeleteStatus(FetchStatus.LOADED);
setShowDialog(false);
- route('/events', true);
}
}, [eventId, setShowDialog, setDeleteEvent]);
@@ -48,18 +54,25 @@ export default function Event({ eventId }) {
return
;
}
- const startime = new Date(data.start_time * 1000);
- const endtime = new Date(data.end_time * 1000);
-
return (
-
-
- {data.camera} {data.label} {startime.toLocaleString()}
-
-
+
+
+
+
+
+
+
+
+
{showDialog ? (
) : null}
-
-
-
- Key |
- Value |
-
-
-
- Camera |
-
- {data.camera}
- |
-
-
- Timeframe |
-
- {startime.toLocaleString()} – {endtime.toLocaleString()}
- |
-
-
- Score |
- {(data.top_score * 100).toFixed(2)}% |
-
-
- Zones |
- {data.zones.join(', ')} |
-
-
-
-
- {data.has_clip ? (
-
- Clip
- {}}
- />
-
-
-
-
-
- ) : (
-
- {data.has_snapshot ? 'Best Image' : 'Thumbnail'}
-
-
- )}
+
+
+ {data.has_clip ? (
+
+ Clip
+ {}}
+ />
+
+ ) : (
+
+ {data.has_snapshot ? 'Best Image' : 'Thumbnail'}
+
+
+ )}
+
+
);
}
diff --git a/web/src/routes/Events.jsx b/web/src/routes/Events.jsx
index e74bbadc4..4db9413df 100644
--- a/web/src/routes/Events.jsx
+++ b/web/src/routes/Events.jsx
@@ -1,10 +1,11 @@
-import { h } from 'preact';
+import { h, Fragment } from 'preact';
import ActivityIndicator from '../components/ActivityIndicator';
import Heading from '../components/Heading';
import Link from '../components/Link';
import Select from '../components/Select';
import produce from 'immer';
import { route } from 'preact-router';
+import Event from './Event';
import { useIntersectionObserver } from '../hooks';
import { FetchStatus, useApiHost, useConfig, useEvents } from '../api';
import { Table, Thead, Tbody, Tfoot, Th, Tr, Td } from '../components/Table';
@@ -12,9 +13,20 @@ import { useCallback, useEffect, useMemo, useReducer, useState } from 'preact/ho
const API_LIMIT = 25;
-const initialState = Object.freeze({ events: [], reachedEnd: false, searchStrings: {} });
+const initialState = Object.freeze({ events: [], reachedEnd: false, searchStrings: {}, deleted: 0 });
const reducer = (state = initialState, action) => {
switch (action.type) {
+ case 'DELETE_EVENT': {
+ const { deletedId } = action;
+
+ return produce(state, (draftState) => {
+ const idx = draftState.events.findIndex((e) => e.id === deletedId);
+ if (idx === -1) return state;
+
+ draftState.events.splice(idx, 1);
+ draftState.deleted++;
+ });
+ }
case 'APPEND_EVENTS': {
const {
meta: { searchString },
@@ -24,6 +36,7 @@ const reducer = (state = initialState, action) => {
return produce(state, (draftState) => {
draftState.searchStrings[searchString] = true;
draftState.events.push(...payload);
+ draftState.deleted = 0;
});
}
@@ -54,11 +67,13 @@ function removeDefaultSearchKeys(searchParams) {
export default function Events({ path: pathname, limit = API_LIMIT } = {}) {
const apiHost = useApiHost();
- const [{ events, reachedEnd, searchStrings }, dispatch] = useReducer(reducer, initialState);
+ const [{ events, reachedEnd, searchStrings, deleted }, dispatch] = useReducer(reducer, initialState);
const { searchParams: initialSearchParams } = new URL(window.location);
+ const [viewEvent, setViewEvent] = useState(null);
const [searchString, setSearchString] = useState(`${defaultSearchString(limit)}&${initialSearchParams.toString()}`);
- const { data, status, deleted } = useEvents(searchString);
+ const { data, status, deletedId } = useEvents(searchString);
+ const scrollToRef = {};
useEffect(() => {
if (data && !(searchString in searchStrings)) {
dispatch({ type: 'APPEND_EVENTS', payload: data, meta: { searchString } });
@@ -67,7 +82,11 @@ export default function Events({ path: pathname, limit = API_LIMIT } = {}) {
if (data && Array.isArray(data) && data.length + deleted < limit) {
dispatch({ type: 'REACHED_END', meta: { searchString } });
}
- }, [data, limit, searchString, searchStrings, deleted]);
+
+ if (deletedId) {
+ dispatch({ type: 'DELETE_EVENT', deletedId });
+ }
+ }, [data, limit, searchString, searchStrings, deleted, deletedId]);
const [entry, setIntersectNode] = useIntersectionObserver();
@@ -100,7 +119,16 @@ export default function Events({ path: pathname, limit = API_LIMIT } = {}) {
[limit, pathname, setSearchString]
);
+ const viewEventHandler = (id) => {
+ //Toggle event view
+ if (viewEvent === id) return setViewEvent(null);
+
+ //Set event id to be rendered.
+ setViewEvent(id);
+ };
+
const searchParams = useMemo(() => new URLSearchParams(searchString), [searchString]);
+
return (
Events
@@ -123,70 +151,83 @@ export default function Events({ path: pathname, limit = API_LIMIT } = {}) {
{events.map(
- (
- { camera, id, label, start_time: startTime, end_time: endTime, thumbnail, top_score: score, zones },
- i
- ) => {
+ ({ camera, id, label, start_time: startTime, end_time: endTime, top_score: score, zones }, i) => {
const start = new Date(parseInt(startTime * 1000, 10));
const end = new Date(parseInt(endTime * 1000, 10));
const ref = i === events.length - 1 ? lastCellRef : undefined;
return (
-
-
-
-
+
+
+ viewEventHandler(id)}
+ ref={ref}
+ data-start-time={startTime}
+ data-reached-end={reachedEnd}
+ >
+ (scrollToRef[id] = el)}
+ width="150"
+ height="150"
+ className="cursor-pointer"
+ style="min-height: 48px; min-width: 48px;"
+ src={`${apiHost}/api/events/${id}/thumbnail.jpg`}
+ />
+
+ |
+
+
-
- |
-
-
- |
-
-
- |
- {(score * 100).toFixed(2)}% |
-
-
- {zones.map((zone) => (
- -
-
-
- ))}
-
- |
- {start.toLocaleDateString()} |
- {start.toLocaleTimeString()} |
- {end.toLocaleTimeString()} |
-
+ |
+
+
+ |
+ {(score * 100).toFixed(2)}% |
+
+
+ {zones.map((zone) => (
+ -
+
+
+ ))}
+
+ |
+ {start.toLocaleDateString()} |
+ {start.toLocaleTimeString()} |
+ {end.toLocaleTimeString()} |
+
+ {viewEvent === id ? (
+
+
+ setViewEvent(null)} scrollRef={scrollToRef} />
+ |
+
+ ) : null}
+
);
}
)}
-
+ |
{status === FetchStatus.LOADING ? : reachedEnd ? 'No more events' : null}
|