mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2024-12-27 07:35:56 -05:00
add some URL parameters for the list view
This is still missing the time range, which is more complex than the others. Small steps.
This commit is contained in:
parent
bcc59e9109
commit
782eb2f0d8
@ -99,7 +99,7 @@ function App() {
|
|||||||
path=""
|
path=""
|
||||||
element={
|
element={
|
||||||
<ListActivity
|
<ListActivity
|
||||||
cameras={toplevel.cameras}
|
toplevel={toplevel}
|
||||||
showSelectors={showListSelectors}
|
showSelectors={showListSelectors}
|
||||||
timeZoneName={timeZoneName!}
|
timeZoneName={timeZoneName!}
|
||||||
/>
|
/>
|
||||||
|
@ -11,8 +11,8 @@ import { ToplevelResponse } from "../api";
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
toplevel: ToplevelResponse;
|
toplevel: ToplevelResponse;
|
||||||
selected: Set<Stream>;
|
selected: Set<number>;
|
||||||
setSelected: (selected: Set<Stream>) => void;
|
setSelected: (selected: Set<number>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
const useStyles = makeStyles({
|
||||||
@ -42,25 +42,29 @@ const StreamMultiSelector = ({ toplevel, selected, setSelected }: Props) => {
|
|||||||
const setStream = (s: Stream, checked: boolean) => {
|
const setStream = (s: Stream, checked: boolean) => {
|
||||||
const updated = new Set(selected);
|
const updated = new Set(selected);
|
||||||
if (checked) {
|
if (checked) {
|
||||||
updated.add(s);
|
updated.add(s.id);
|
||||||
} else {
|
} else {
|
||||||
updated.delete(s);
|
updated.delete(s.id);
|
||||||
}
|
}
|
||||||
setSelected(updated);
|
setSelected(updated);
|
||||||
};
|
};
|
||||||
const toggleType = (st: StreamType) => {
|
const toggleType = (st: StreamType) => {
|
||||||
let updated = new Set(selected);
|
let updated = new Set(selected);
|
||||||
let foundAny = false;
|
let foundAny = false;
|
||||||
for (const s of selected) {
|
for (const sid of selected) {
|
||||||
|
const s = toplevel.streams.get(sid);
|
||||||
|
if (s === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (s.streamType === st) {
|
if (s.streamType === st) {
|
||||||
updated.delete(s);
|
updated.delete(s.id);
|
||||||
foundAny = true;
|
foundAny = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!foundAny) {
|
if (!foundAny) {
|
||||||
for (const c of toplevel.cameras) {
|
for (const c of toplevel.cameras) {
|
||||||
if (c.streams[st] !== undefined) {
|
if (c.streams[st] !== undefined) {
|
||||||
updated.add(c.streams[st as StreamType]!);
|
updated.add(c.streams[st as StreamType]!.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -71,14 +75,14 @@ const StreamMultiSelector = ({ toplevel, selected, setSelected }: Props) => {
|
|||||||
let foundAny = false;
|
let foundAny = false;
|
||||||
for (const st in c.streams) {
|
for (const st in c.streams) {
|
||||||
const s = c.streams[st as StreamType]!;
|
const s = c.streams[st as StreamType]!;
|
||||||
if (selected.has(s)) {
|
if (selected.has(s.id)) {
|
||||||
updated.delete(s);
|
updated.delete(s.id);
|
||||||
foundAny = true;
|
foundAny = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!foundAny) {
|
if (!foundAny) {
|
||||||
for (const st in c.streams) {
|
for (const st in c.streams) {
|
||||||
updated.add(c.streams[st as StreamType]!);
|
updated.add(c.streams[st as StreamType]!.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setSelected(updated);
|
setSelected(updated);
|
||||||
@ -96,7 +100,7 @@ const StreamMultiSelector = ({ toplevel, selected, setSelected }: Props) => {
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
className={classes.check}
|
className={classes.check}
|
||||||
size="small"
|
size="small"
|
||||||
checked={selected.has(s)}
|
checked={selected.has(s.id)}
|
||||||
color="secondary"
|
color="secondary"
|
||||||
onChange={(event) => setStream(s, event.target.checked)}
|
onChange={(event) => setStream(s, event.target.checked)}
|
||||||
/>
|
/>
|
||||||
|
@ -21,6 +21,7 @@ const TEST_CAMERA: Camera = {
|
|||||||
|
|
||||||
const TEST_STREAM: Stream = {
|
const TEST_STREAM: Stream = {
|
||||||
camera: TEST_CAMERA,
|
camera: TEST_CAMERA,
|
||||||
|
id: 1,
|
||||||
streamType: "main",
|
streamType: "main",
|
||||||
retainBytes: 0,
|
retainBytes: 0,
|
||||||
minStartTime90k: 0,
|
minStartTime90k: 0,
|
||||||
|
@ -21,6 +21,7 @@ import VideoList from "./VideoList";
|
|||||||
import { useLayoutEffect } from "react";
|
import { useLayoutEffect } from "react";
|
||||||
import { fillAspect } from "../aspect";
|
import { fillAspect } from "../aspect";
|
||||||
import useResizeObserver from "@react-hook/resize-observer";
|
import useResizeObserver from "@react-hook/resize-observer";
|
||||||
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) => ({
|
const useStyles = makeStyles((theme: Theme) => ({
|
||||||
root: {
|
root: {
|
||||||
@ -100,25 +101,151 @@ interface Props {
|
|||||||
showSelectors: boolean;
|
showSelectors: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parsed URL search parameters.
|
||||||
|
interface ParsedSearchParams {
|
||||||
|
selectedStreamIds: Set<number>;
|
||||||
|
split90k: number | undefined;
|
||||||
|
trimStartAndEnd: boolean;
|
||||||
|
timestampTrack: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <tt>ParsedSearchParams</tt> plus <tt>useState</tt>-like setters.
|
||||||
|
interface ParsedSearchParamsAndSetters extends ParsedSearchParams {
|
||||||
|
setSelectedStreamIds: (selectedStreamIds: Set<number>) => void;
|
||||||
|
setSplit90k: (split90k: number | undefined) => void;
|
||||||
|
setTrimStartAndEnd: (trimStartAndEnd: boolean) => void;
|
||||||
|
setTimestampTrack: (timestampTrack: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseSearchParams = (raw: URLSearchParams): ParsedSearchParams => {
|
||||||
|
let selectedStreamIds = new Set<number>();
|
||||||
|
let split90k = DEFAULT_DURATION;
|
||||||
|
let trimStartAndEnd = true;
|
||||||
|
let timestampTrack = false;
|
||||||
|
for (const [key, value] of raw) {
|
||||||
|
switch (key) {
|
||||||
|
case "s":
|
||||||
|
selectedStreamIds.add(Number.parseInt(value, 10));
|
||||||
|
break;
|
||||||
|
case "split":
|
||||||
|
split90k = value === "inf" ? undefined : Number.parseInt(value, 10);
|
||||||
|
break;
|
||||||
|
case "trim":
|
||||||
|
trimStartAndEnd = value === "true";
|
||||||
|
break;
|
||||||
|
case "ts":
|
||||||
|
timestampTrack = value === "true";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
selectedStreamIds,
|
||||||
|
split90k,
|
||||||
|
trimStartAndEnd,
|
||||||
|
timestampTrack,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const useParsedSearchParams = (): ParsedSearchParamsAndSetters => {
|
||||||
|
const [search, setSearch] = useSearchParams();
|
||||||
|
|
||||||
|
// This useMemo is necessary to avoid a re-rendering loop caused by each
|
||||||
|
// call's selectedStreamIds set having different identity.
|
||||||
|
const { selectedStreamIds, split90k, trimStartAndEnd, timestampTrack } =
|
||||||
|
useMemo(() => parseSearchParams(search), [search]);
|
||||||
|
|
||||||
|
const setSelectedStreamIds = (newSelectedStreamIds: Set<number>) => {
|
||||||
|
// TODO: check if it's worth suppressing no-ops here.
|
||||||
|
search.delete("s");
|
||||||
|
for (const id of newSelectedStreamIds) {
|
||||||
|
search.append("s", id.toString());
|
||||||
|
}
|
||||||
|
setSearch(search);
|
||||||
|
};
|
||||||
|
const setSplit90k = (newSplit90k: number | undefined) => {
|
||||||
|
if (newSplit90k === split90k) {
|
||||||
|
return;
|
||||||
|
} else if (newSplit90k === DEFAULT_DURATION) {
|
||||||
|
search.delete("split");
|
||||||
|
} else if (newSplit90k === undefined) {
|
||||||
|
search.set("split", "inf");
|
||||||
|
} else {
|
||||||
|
search.set("split", newSplit90k.toString());
|
||||||
|
}
|
||||||
|
setSearch(search);
|
||||||
|
};
|
||||||
|
const setTrimStartAndEnd = (newTrimStartAndEnd: boolean) => {
|
||||||
|
if (newTrimStartAndEnd === trimStartAndEnd) {
|
||||||
|
return;
|
||||||
|
} else if (newTrimStartAndEnd === true) {
|
||||||
|
search.delete("trim"); // default
|
||||||
|
} else {
|
||||||
|
search.set("trim", "false");
|
||||||
|
}
|
||||||
|
setSearch(search);
|
||||||
|
};
|
||||||
|
const setTimestampTrack = (newTimestampTrack: boolean) => {
|
||||||
|
if (newTimestampTrack === timestampTrack) {
|
||||||
|
return;
|
||||||
|
} else if (newTimestampTrack === false) {
|
||||||
|
search.delete("ts"); // default
|
||||||
|
} else {
|
||||||
|
search.set("ts", "true");
|
||||||
|
}
|
||||||
|
setSearch(search);
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
selectedStreamIds,
|
||||||
|
setSelectedStreamIds,
|
||||||
|
split90k,
|
||||||
|
setSplit90k,
|
||||||
|
trimStartAndEnd,
|
||||||
|
setTrimStartAndEnd,
|
||||||
|
timestampTrack,
|
||||||
|
setTimestampTrack,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const calcSelectedStreams = (
|
||||||
|
toplevel: api.ToplevelResponse,
|
||||||
|
ids: Set<number>
|
||||||
|
): Set<Stream> => {
|
||||||
|
let streams = new Set<Stream>();
|
||||||
|
for (const id of ids) {
|
||||||
|
const s = toplevel.streams.get(id);
|
||||||
|
if (s === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
streams.add(s);
|
||||||
|
}
|
||||||
|
return streams;
|
||||||
|
};
|
||||||
|
|
||||||
const Main = ({ toplevel, timeZoneName, showSelectors }: Props) => {
|
const Main = ({ toplevel, timeZoneName, showSelectors }: Props) => {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
|
|
||||||
/**
|
const {
|
||||||
* Selected streams to display and use for selecting time ranges.
|
selectedStreamIds,
|
||||||
* This currently uses the <tt>Stream</tt> object, which means it will
|
setSelectedStreamIds,
|
||||||
* not be stable across top-level API fetches. Maybe an id would be better.
|
split90k,
|
||||||
*/
|
setSplit90k,
|
||||||
const [selectedStreams, setSelectedStreams] = useState<Set<Stream>>(
|
trimStartAndEnd,
|
||||||
new Set()
|
setTrimStartAndEnd,
|
||||||
);
|
timestampTrack,
|
||||||
|
setTimestampTrack,
|
||||||
|
} = useParsedSearchParams();
|
||||||
|
|
||||||
/** Selected time range. */
|
// The time range to examine, or null if one hasn't yet been selected. Note
|
||||||
|
// this is derived from state held within TimerangeSelector.
|
||||||
const [range90k, setRange90k] = useState<[number, number] | null>(null);
|
const [range90k, setRange90k] = useState<[number, number] | null>(null);
|
||||||
|
|
||||||
const [split90k, setSplit90k] = useState(DEFAULT_DURATION);
|
// TimerangeSelector currently expects a Set<Stream>. Memoize one; otherwise
|
||||||
|
// we'd get an infinite rerendering loop because the Set identity changes
|
||||||
const [trimStartAndEnd, setTrimStartAndEnd] = useState(true);
|
// each time.
|
||||||
const [timestampTrack, setTimestampTrack] = useState(false);
|
const selectedStreams = useMemo(
|
||||||
|
() => calcSelectedStreams(toplevel, selectedStreamIds),
|
||||||
|
[toplevel, selectedStreamIds]
|
||||||
|
);
|
||||||
|
|
||||||
const [activeRecording, setActiveRecording] = useState<
|
const [activeRecording, setActiveRecording] = useState<
|
||||||
[Stream, api.Recording, api.VideoSampleEntry] | null
|
[Stream, api.Recording, api.VideoSampleEntry] | null
|
||||||
@ -161,9 +288,9 @@ const Main = ({ toplevel, timeZoneName, showSelectors }: Props) => {
|
|||||||
sx={{ display: showSelectors ? "block" : "none" }}
|
sx={{ display: showSelectors ? "block" : "none" }}
|
||||||
>
|
>
|
||||||
<StreamMultiSelector
|
<StreamMultiSelector
|
||||||
cameras={toplevel.cameras}
|
toplevel={toplevel}
|
||||||
selected={selectedStreams}
|
selected={selectedStreamIds}
|
||||||
setSelected={setSelectedStreams}
|
setSelected={setSelectedStreamIds}
|
||||||
/>
|
/>
|
||||||
<TimerangeSelector
|
<TimerangeSelector
|
||||||
selectedStreams={selectedStreams}
|
selectedStreams={selectedStreams}
|
||||||
|
Loading…
Reference in New Issue
Block a user