import { h, Fragment } from 'preact';
import { route } from 'preact-router';
import ActivityIndicator from '../components/ActivityIndicator';
import Heading from '../components/Heading';
import { useApiHost } from '../api';
import useSWR from 'swr';
import useSWRInfinite from 'swr/infinite';
import axios from 'axios';
import { useState, useRef, useCallback, useMemo } from 'preact/hooks';
import VideoPlayer from '../components/VideoPlayer';
import { StarRecording } from '../icons/StarRecording';
import { Snapshot } from '../icons/Snapshot';
import { UploadPlus } from '../icons/UploadPlus';
import { Clip } from '../icons/Clip';
import { Zone } from '../icons/Zone';
import { Camera } from '../icons/Camera';
import { Delete } from '../icons/Delete';
import { Download } from '../icons/Download';
import Menu, { MenuItem } from '../components/Menu';
import CalendarIcon from '../icons/Calendar';
import Calendar from '../components/Calendar';
import Button from '../components/Button';
import Dialog from '../components/Dialog';
const API_LIMIT = 25;
const daysAgo = (num) => {
let date = new Date();
date.setDate(date.getDate() - num);
return new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime() / 1000;
};
const monthsAgo = (num) => {
let date = new Date();
date.setMonth(date.getMonth() - num);
return new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime() / 1000;
};
export default function Events({ path, ...props }) {
const apiHost = useApiHost();
const [searchParams, setSearchParams] = useState({
before: null,
after: null,
camera: props.camera ?? 'all',
label: props.label ?? 'all',
zone: props.zone ?? 'all',
});
const [state, setState] = useState({
showDownloadMenu: false,
showDatePicker: false,
showCalendar: false,
showPlusConfig: false,
});
const [uploading, setUploading] = useState([]);
const [viewEvent, setViewEvent] = useState();
const [downloadEvent, setDownloadEvent] = useState({
id: null,
has_clip: false,
has_snapshot: false,
plus_id: undefined,
});
const eventsFetcher = useCallback((path, params) => {
params = { ...params, include_thumbnails: 0, limit: API_LIMIT };
return axios.get(path, { params }).then((res) => res.data);
}, []);
const getKey = useCallback(
(index, prevData) => {
if (index > 0) {
const lastDate = prevData[prevData.length - 1].start_time;
const pagedParams = { ...searchParams, before: lastDate };
return ['events', pagedParams];
}
return ['events', searchParams];
},
[searchParams]
);
const { data: eventPages, mutate, size, setSize, isValidating } = useSWRInfinite(getKey, eventsFetcher);
const { data: config } = useSWR('config');
const filterValues = useMemo(
() => ({
cameras: Object.keys(config?.cameras || {}),
zones: Object.values(config?.cameras || {})
.reduce((memo, camera) => {
memo = memo.concat(Object.keys(camera?.zones || {}));
return memo;
}, [])
.filter((value, i, self) => self.indexOf(value) === i),
labels: Object.values(config?.cameras || {})
.reduce((memo, camera) => {
memo = memo.concat(camera?.objects?.track || []);
return memo;
}, config?.objects?.track || [])
.filter((value, i, self) => self.indexOf(value) === i),
}),
[config]
);
const onSave = async (e, eventId, save) => {
e.stopPropagation();
let response;
if (save) {
response = await axios.post(`events/${eventId}/retain`);
} else {
response = await axios.delete(`events/${eventId}/retain`);
}
if (response.status === 200) {
mutate();
}
};
const onDelete = async (e, eventId) => {
e.stopPropagation();
const response = await axios.delete(`events/${eventId}`);
if (response.status === 200) {
mutate();
}
};
const datePicker = useRef();
const downloadButton = useRef();
const onDownloadClick = (e, event) => {
e.stopPropagation();
setDownloadEvent((_prev) => ({
id: event.id,
has_clip: event.has_clip,
has_snapshot: event.has_snapshot,
plus_id: event.plus_id,
}));
downloadButton.current = e.target;
setState({ ...state, showDownloadMenu: true });
};
const handleSelectDateRange = useCallback(
(dates) => {
setSearchParams({ ...searchParams, before: dates.before, after: dates.after });
setState({ ...state, showDatePicker: false });
},
[searchParams, setSearchParams, state, setState]
);
const onFilter = useCallback(
(name, value) => {
const updatedParams = { ...searchParams, [name]: value };
setSearchParams(updatedParams);
const queryString = Object.keys(updatedParams)
.map((key) => {
if (updatedParams[key] && updatedParams[key] != 'all') {
return `${key}=${updatedParams[key]}`;
}
return null;
})
.filter((val) => val)
.join('&');
route(`${path}?${queryString}`);
},
[path, searchParams, setSearchParams]
);
const isDone = (eventPages?.[eventPages.length - 1]?.length ?? 0) < API_LIMIT;
// hooks for infinite scroll
const observer = useRef();
const lastEventRef = useCallback(
(node) => {
if (isValidating) return;
if (observer.current) observer.current.disconnect();
observer.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && !isDone) {
setSize(size + 1);
}
});
if (node) observer.current.observe(node);
},
[size, setSize, isValidating, isDone]
);
const onSendToPlus = async (id, e) => {
if (e) {
e.stopPropagation();
}
if (uploading.includes(id)) {
return;
}
if (!config.plus.enabled) {
setState({ ...state, showDownloadMenu: false, showPlusConfig: true });
return;
}
setUploading((prev) => [...prev, id]);
const response = await axios.post(`events/${id}/plus`);
if (response.status === 200) {
mutate(
(pages) =>
pages.map((page) =>
page.map((event) => {
if (event.id === id) {
return { ...event, plus_id: response.data.plus_id };
}
return event;
})
),
false
);
}
setUploading((prev) => prev.filter((i) => i !== id));
if (state.showDownloadMenu && downloadEvent.id === id) {
setState({ ...state, showDownloadMenu: false });
}
};
if (!config) {
return