mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01:00
fix date picker
This commit is contained in:
parent
1d8f1b24a9
commit
deb3536cb2
@ -15,7 +15,8 @@
|
|||||||
"rules": {
|
"rules": {
|
||||||
"indent": ["error", 2, { "SwitchCase": 1 }],
|
"indent": ["error", 2, { "SwitchCase": 1 }],
|
||||||
"comma-dangle": ["error", { "objects": "always-multiline", "arrays": "always-multiline" }],
|
"comma-dangle": ["error", { "objects": "always-multiline", "arrays": "always-multiline" }],
|
||||||
"no-unused-vars": ["error", { "argsIgnorePattern": "^_" }]
|
"no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
|
||||||
|
"no-console": "error"
|
||||||
},
|
},
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
|
@ -5,7 +5,7 @@ import ArrowRightDouble from '../icons/ArrowRightDouble';
|
|||||||
|
|
||||||
const todayTimestamp = new Date().setHours(0, 0, 0, 0).valueOf();
|
const todayTimestamp = new Date().setHours(0, 0, 0, 0).valueOf();
|
||||||
|
|
||||||
const Calendar = ({ onChange, calendarRef, close }) => {
|
const Calendar = ({ onChange, calendarRef, close, dateRange }) => {
|
||||||
const keyRef = useRef([]);
|
const keyRef = useRef([]);
|
||||||
|
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
@ -36,7 +36,7 @@ const Calendar = ({ onChange, calendarRef, close }) => {
|
|||||||
year,
|
year,
|
||||||
month,
|
month,
|
||||||
selectedDay: null,
|
selectedDay: null,
|
||||||
timeRange: { before: null, after: null },
|
timeRange: dateRange,
|
||||||
monthDetails: null,
|
monthDetails: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -42,53 +42,55 @@ export default function Events({ path, ...props }) {
|
|||||||
label: props.label ?? 'all',
|
label: props.label ?? 'all',
|
||||||
zone: props.zone ?? 'all',
|
zone: props.zone ?? 'all',
|
||||||
});
|
});
|
||||||
|
const [state, setState] = useState({
|
||||||
|
showDownloadMenu: null,
|
||||||
|
showDatePicker: null,
|
||||||
|
showCalendar: null,
|
||||||
|
});
|
||||||
const [viewEvent, setViewEvent] = useState();
|
const [viewEvent, setViewEvent] = useState();
|
||||||
const [downloadEvent, setDownloadEvent] = useState({ id: null, has_clip: false, has_snapshot: false });
|
const [downloadEvent, setDownloadEvent] = useState({ id: null, has_clip: false, has_snapshot: false });
|
||||||
const [showDownloadMenu, setShowDownloadMenu] = useState();
|
|
||||||
const [showDatePicker, setShowDatePicker] = useState();
|
|
||||||
const [showCalendar, setShowCalendar] = useState();
|
|
||||||
|
|
||||||
const eventsFetcher = (path, params) => {
|
const eventsFetcher = useCallback((path, params) => {
|
||||||
params = { ...params, include_thumbnails: 0, limit: API_LIMIT };
|
params = { ...params, include_thumbnails: 0, limit: API_LIMIT };
|
||||||
return axios.get(path, { params }).then((res) => res.data);
|
return axios.get(path, { params }).then((res) => res.data);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const getKey = (index, prevData) => {
|
const getKey = useCallback(
|
||||||
if (index > 0) {
|
(index, prevData) => {
|
||||||
const lastDate = prevData[prevData.length - 1].start_time;
|
if (index > 0) {
|
||||||
const pagedParams = { ...searchParams, before: lastDate };
|
const lastDate = prevData[prevData.length - 1].start_time;
|
||||||
return ['events', pagedParams];
|
const pagedParams = { ...searchParams, before: lastDate };
|
||||||
}
|
return ['events', pagedParams];
|
||||||
|
}
|
||||||
|
|
||||||
return ['events', searchParams];
|
return ['events', searchParams];
|
||||||
};
|
},
|
||||||
|
[searchParams]
|
||||||
|
);
|
||||||
|
|
||||||
const { data: eventPages, mutate, size, setSize, isValidating } = useSWRInfinite(getKey, eventsFetcher);
|
const { data: eventPages, mutate, size, setSize, isValidating } = useSWRInfinite(getKey, eventsFetcher);
|
||||||
|
|
||||||
const { data: config } = useSWR('config');
|
const { data: config } = useSWR('config');
|
||||||
|
|
||||||
const cameras = useMemo(() => Object.keys(config?.cameras || {}), [config]);
|
const filterValues = useMemo(
|
||||||
|
() => ({
|
||||||
const zones = useMemo(
|
cameras: Object.keys(config?.cameras || {}),
|
||||||
() =>
|
zones: Object.values(config?.cameras || {})
|
||||||
Object.values(config?.cameras || {})
|
|
||||||
.reduce((memo, camera) => {
|
.reduce((memo, camera) => {
|
||||||
memo = memo.concat(Object.keys(camera?.zones || {}));
|
memo = memo.concat(Object.keys(camera?.zones || {}));
|
||||||
return memo;
|
return memo;
|
||||||
}, [])
|
}, [])
|
||||||
.filter((value, i, self) => self.indexOf(value) === i),
|
.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]
|
[config]
|
||||||
);
|
);
|
||||||
|
|
||||||
const labels = useMemo(() => {
|
|
||||||
return 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) => {
|
const onSave = async (e, eventId, save) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
let response;
|
let response;
|
||||||
@ -118,16 +120,15 @@ export default function Events({ path, ...props }) {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setDownloadEvent((_prev) => ({ id: event.id, has_clip: event.has_clip, has_snapshot: event.has_snapshot }));
|
setDownloadEvent((_prev) => ({ id: event.id, has_clip: event.has_clip, has_snapshot: event.has_snapshot }));
|
||||||
downloadButton.current = e.target;
|
downloadButton.current = e.target;
|
||||||
setShowDownloadMenu(true);
|
setState({ ...state, showDownloadMenu: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectDateRange = useCallback(
|
const handleSelectDateRange = useCallback(
|
||||||
(dates) => {
|
(dates) => {
|
||||||
console.log(dates);
|
|
||||||
setSearchParams({ ...searchParams, before: dates.before, after: dates.after });
|
setSearchParams({ ...searchParams, before: dates.before, after: dates.after });
|
||||||
setShowDatePicker(false);
|
setState({ ...state, showDatePicker: false });
|
||||||
},
|
},
|
||||||
[searchParams, setSearchParams, setShowDatePicker]
|
[searchParams, setSearchParams, state, setState]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onFilter = useCallback(
|
const onFilter = useCallback(
|
||||||
@ -166,7 +167,7 @@ export default function Events({ path, ...props }) {
|
|||||||
[size, setSize, isValidating, isDone]
|
[size, setSize, isValidating, isDone]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!eventPages || !config) {
|
if (!config) {
|
||||||
return <ActivityIndicator />;
|
return <ActivityIndicator />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,7 +181,7 @@ export default function Events({ path, ...props }) {
|
|||||||
onChange={(e) => onFilter('camera', e.target.value)}
|
onChange={(e) => onFilter('camera', e.target.value)}
|
||||||
>
|
>
|
||||||
<option value="all">all</option>
|
<option value="all">all</option>
|
||||||
{cameras.map((item) => (
|
{filterValues.cameras.map((item) => (
|
||||||
<option key={item} value={item}>
|
<option key={item} value={item}>
|
||||||
{item}
|
{item}
|
||||||
</option>
|
</option>
|
||||||
@ -192,7 +193,7 @@ export default function Events({ path, ...props }) {
|
|||||||
onChange={(e) => onFilter('label', e.target.value)}
|
onChange={(e) => onFilter('label', e.target.value)}
|
||||||
>
|
>
|
||||||
<option value="all">all</option>
|
<option value="all">all</option>
|
||||||
{labels.map((item) => (
|
{filterValues.labels.map((item) => (
|
||||||
<option key={item} value={item}>
|
<option key={item} value={item}>
|
||||||
{item}
|
{item}
|
||||||
</option>
|
</option>
|
||||||
@ -204,18 +205,21 @@ export default function Events({ path, ...props }) {
|
|||||||
onChange={(e) => onFilter('zone', e.target.value)}
|
onChange={(e) => onFilter('zone', e.target.value)}
|
||||||
>
|
>
|
||||||
<option value="all">all</option>
|
<option value="all">all</option>
|
||||||
{zones.map((item) => (
|
{filterValues.zones.map((item) => (
|
||||||
<option key={item} value={item}>
|
<option key={item} value={item}>
|
||||||
{item}
|
{item}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
<div ref={datePicker} className="ml-auto">
|
<div ref={datePicker} className="ml-auto">
|
||||||
<CalendarIcon className="h-8 w-8 cursor-pointer" onClick={() => setShowDatePicker(true)} />
|
<CalendarIcon
|
||||||
|
className="h-8 w-8 cursor-pointer"
|
||||||
|
onClick={() => setState({ ...state, showDatePicker: true })}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{showDownloadMenu && (
|
{state.showDownloadMenu && (
|
||||||
<Menu onDismiss={() => setShowDownloadMenu(false)} relativeTo={downloadButton}>
|
<Menu onDismiss={() => setState({ ...state, showDownloadMenu: false })} relativeTo={downloadButton}>
|
||||||
{downloadEvent.has_snapshot && (
|
{downloadEvent.has_snapshot && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={Snapshot}
|
icon={Snapshot}
|
||||||
@ -236,8 +240,12 @@ export default function Events({ path, ...props }) {
|
|||||||
)}
|
)}
|
||||||
</Menu>
|
</Menu>
|
||||||
)}
|
)}
|
||||||
{showDatePicker && (
|
{state.showDatePicker && (
|
||||||
<Menu className="rounded-t-none" onDismiss={() => setShowDatePicker(false)} relativeTo={datePicker}>
|
<Menu
|
||||||
|
className="rounded-t-none"
|
||||||
|
onDismiss={() => setState({ ...state, setShowDatePicker: false })}
|
||||||
|
relativeTo={datePicker}
|
||||||
|
>
|
||||||
<MenuItem label="All" value={{ before: null, after: null }} onSelect={handleSelectDateRange} />
|
<MenuItem label="All" value={{ before: null, after: null }} onSelect={handleSelectDateRange} />
|
||||||
<MenuItem label="Today" value={{ before: null, after: daysAgo(0) }} onSelect={handleSelectDateRange} />
|
<MenuItem label="Today" value={{ before: null, after: daysAgo(0) }} onSelect={handleSelectDateRange} />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
@ -256,119 +264,130 @@ export default function Events({ path, ...props }) {
|
|||||||
label="Custom Range"
|
label="Custom Range"
|
||||||
value="custom"
|
value="custom"
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
setShowCalendar(true);
|
setState({ ...state, showCalendar: true, showDatePicker: false });
|
||||||
setShowDatePicker(false);
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Menu>
|
</Menu>
|
||||||
)}
|
)}
|
||||||
{showCalendar && (
|
{state.showCalendar && (
|
||||||
<Menu className="rounded-t-none" onDismiss={() => setShowCalendar(false)} relativeTo={datePicker}>
|
<Menu
|
||||||
<Calendar onChange={handleSelectDateRange} close={() => setShowCalendar(false)} />
|
className="rounded-t-none"
|
||||||
|
onDismiss={() => setState({ ...state, showCalendar: false })}
|
||||||
|
relativeTo={datePicker}
|
||||||
|
>
|
||||||
|
<Calendar
|
||||||
|
onChange={handleSelectDateRange}
|
||||||
|
dateRange={{ before: searchParams.before * 1000 || null, after: searchParams.after * 1000 || null }}
|
||||||
|
close={() => setState({ ...state, showCalendar: false })}
|
||||||
|
/>
|
||||||
</Menu>
|
</Menu>
|
||||||
)}
|
)}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{eventPages.map((page, i) => {
|
{eventPages ? (
|
||||||
const lastPage = eventPages.length === i + 1;
|
eventPages.map((page, i) => {
|
||||||
return page.map((event, j) => {
|
const lastPage = eventPages.length === i + 1;
|
||||||
const lastEvent = lastPage && page.length === j + 1;
|
return page.map((event, j) => {
|
||||||
return (
|
const lastEvent = lastPage && page.length === j + 1;
|
||||||
<Fragment key={event.id}>
|
return (
|
||||||
<div
|
<Fragment key={event.id}>
|
||||||
ref={lastEvent ? lastEventRef : false}
|
|
||||||
className="flex bg-slate-100 dark:bg-slate-800 rounded cursor-pointer min-w-[330px]"
|
|
||||||
onClick={() => (viewEvent === event.id ? setViewEvent(null) : setViewEvent(event.id))}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className="relative rounded-l flex-initial min-w-[125px] h-[125px] bg-contain"
|
ref={lastEvent ? lastEventRef : false}
|
||||||
style={{
|
className="flex bg-slate-100 dark:bg-slate-800 rounded cursor-pointer min-w-[330px]"
|
||||||
'background-image': `url(${apiHost}/api/events/${event.id}/thumbnail.jpg)`,
|
onClick={() => (viewEvent === event.id ? setViewEvent(null) : setViewEvent(event.id))}
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<StarRecording
|
<div
|
||||||
className="h-6 w-6 text-yellow-300 absolute top-1 right-1 cursor-pointer"
|
className="relative rounded-l flex-initial min-w-[125px] h-[125px] bg-contain"
|
||||||
onClick={(e) => onSave(e, event.id, !event.retain_indefinitely)}
|
style={{
|
||||||
fill={event.retain_indefinitely ? 'currentColor' : 'none'}
|
'background-image': `url(${apiHost}/api/events/${event.id}/thumbnail.jpg)`,
|
||||||
/>
|
}}
|
||||||
{event.end_time ? null : (
|
>
|
||||||
<div className="bg-slate-300 dark:bg-slate-700 absolute bottom-0 text-center w-full uppercase text-sm rounded-bl">
|
<StarRecording
|
||||||
In progress
|
className="h-6 w-6 text-yellow-300 absolute top-1 right-1 cursor-pointer"
|
||||||
</div>
|
onClick={(e) => onSave(e, event.id, !event.retain_indefinitely)}
|
||||||
)}
|
fill={event.retain_indefinitely ? 'currentColor' : 'none'}
|
||||||
</div>
|
|
||||||
<div className="m-2 flex grow">
|
|
||||||
<div className="flex flex-col grow">
|
|
||||||
<div className="capitalize text-lg font-bold">
|
|
||||||
{event.label} ({(event.top_score * 100).toFixed(0)}%)
|
|
||||||
</div>
|
|
||||||
<div className="text-sm">
|
|
||||||
{new Date(event.start_time * 1000).toLocaleDateString()}{' '}
|
|
||||||
{new Date(event.start_time * 1000).toLocaleTimeString()}
|
|
||||||
</div>
|
|
||||||
<div className="capitalize text-sm flex align-center mt-1">
|
|
||||||
<Camera className="h-5 w-5 mr-2 inline" />
|
|
||||||
{event.camera}
|
|
||||||
</div>
|
|
||||||
<div className="capitalize text-sm flex align-center">
|
|
||||||
<Zone className="w-5 h-5 mr-2 inline" />
|
|
||||||
{event.zones.join(',')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<Delete className="cursor-pointer" stroke="#f87171" onClick={(e) => onDelete(e, event.id)} />
|
|
||||||
|
|
||||||
<Download
|
|
||||||
className="h-6 w-6 mt-auto"
|
|
||||||
stroke={event.has_clip || event.has_snapshot ? '#3b82f6' : '#cbd5e1'}
|
|
||||||
onClick={(e) => onDownloadClick(e, event)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
{event.end_time ? null : (
|
||||||
</div>
|
<div className="bg-slate-300 dark:bg-slate-700 absolute bottom-0 text-center w-full uppercase text-sm rounded-bl">
|
||||||
</div>
|
In progress
|
||||||
{viewEvent !== event.id ? null : (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="mx-auto">
|
|
||||||
{event.has_clip ? (
|
|
||||||
<>
|
|
||||||
<Heading size="lg">Clip</Heading>
|
|
||||||
<VideoPlayer
|
|
||||||
options={{
|
|
||||||
preload: 'auto',
|
|
||||||
autoplay: true,
|
|
||||||
sources: [
|
|
||||||
{
|
|
||||||
src: `${apiHost}/vod/event/${event.id}/index.m3u8`,
|
|
||||||
type: 'application/vnd.apple.mpegurl',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}}
|
|
||||||
seekOptions={{ forward: 10, back: 5 }}
|
|
||||||
onReady={() => {}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="flex justify-center">
|
|
||||||
<div>
|
|
||||||
<Heading size="sm">{event.has_snapshot ? 'Best Image' : 'Thumbnail'}</Heading>
|
|
||||||
<img
|
|
||||||
className="flex-grow-0"
|
|
||||||
src={
|
|
||||||
event.has_snapshot
|
|
||||||
? `${apiHost}/api/events/${event.id}/snapshot.jpg`
|
|
||||||
: `data:image/jpeg;base64,${event.thumbnail}`
|
|
||||||
}
|
|
||||||
alt={`${event.label} at ${(event.top_score * 100).toFixed(0)}% confidence`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="m-2 flex grow">
|
||||||
|
<div className="flex flex-col grow">
|
||||||
|
<div className="capitalize text-lg font-bold">
|
||||||
|
{event.label} ({(event.top_score * 100).toFixed(0)}%)
|
||||||
|
</div>
|
||||||
|
<div className="text-sm">
|
||||||
|
{new Date(event.start_time * 1000).toLocaleDateString()}{' '}
|
||||||
|
{new Date(event.start_time * 1000).toLocaleTimeString()}
|
||||||
|
</div>
|
||||||
|
<div className="capitalize text-sm flex align-center mt-1">
|
||||||
|
<Camera className="h-5 w-5 mr-2 inline" />
|
||||||
|
{event.camera}
|
||||||
|
</div>
|
||||||
|
<div className="capitalize text-sm flex align-center">
|
||||||
|
<Zone className="w-5 h-5 mr-2 inline" />
|
||||||
|
{event.zones.join(',')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<Delete className="cursor-pointer" stroke="#f87171" onClick={(e) => onDelete(e, event.id)} />
|
||||||
|
|
||||||
|
<Download
|
||||||
|
className="h-6 w-6 mt-auto"
|
||||||
|
stroke={event.has_clip || event.has_snapshot ? '#3b82f6' : '#cbd5e1'}
|
||||||
|
onClick={(e) => onDownloadClick(e, event)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
{viewEvent !== event.id ? null : (
|
||||||
</Fragment>
|
<div className="space-y-4">
|
||||||
);
|
<div className="mx-auto">
|
||||||
});
|
{event.has_clip ? (
|
||||||
})}
|
<>
|
||||||
|
<Heading size="lg">Clip</Heading>
|
||||||
|
<VideoPlayer
|
||||||
|
options={{
|
||||||
|
preload: 'auto',
|
||||||
|
autoplay: true,
|
||||||
|
sources: [
|
||||||
|
{
|
||||||
|
src: `${apiHost}/vod/event/${event.id}/index.m3u8`,
|
||||||
|
type: 'application/vnd.apple.mpegurl',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
seekOptions={{ forward: 10, back: 5 }}
|
||||||
|
onReady={() => {}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<div>
|
||||||
|
<Heading size="sm">{event.has_snapshot ? 'Best Image' : 'Thumbnail'}</Heading>
|
||||||
|
<img
|
||||||
|
className="flex-grow-0"
|
||||||
|
src={
|
||||||
|
event.has_snapshot
|
||||||
|
? `${apiHost}/api/events/${event.id}/snapshot.jpg`
|
||||||
|
: `data:image/jpeg;base64,${event.thumbnail}`
|
||||||
|
}
|
||||||
|
alt={`${event.label} at ${(event.top_score * 100).toFixed(0)}% confidence`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<ActivityIndicator />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>{isDone ? null : <ActivityIndicator />}</div>
|
<div>{isDone ? null : <ActivityIndicator />}</div>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user