mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-10 13:15:25 +03:00
Add intermediary mobile bottom sheet
This commit is contained in:
parent
9599ad166a
commit
fae5fcc037
@ -371,15 +371,15 @@ function CalendarFilterButton({
|
||||
type GeneralFilterButtonProps = {
|
||||
allLabels: string[];
|
||||
selectedLabels: string[] | undefined;
|
||||
updateLabelFilter: (labels: string[] | undefined) => void;
|
||||
showReviewed?: 0 | 1;
|
||||
updateLabelFilter: (labels: string[] | undefined) => void;
|
||||
setShowReviewed: (reviewed?: 0 | 1) => void;
|
||||
};
|
||||
function GeneralFilterButton({
|
||||
allLabels,
|
||||
selectedLabels,
|
||||
updateLabelFilter,
|
||||
showReviewed,
|
||||
updateLabelFilter,
|
||||
setShowReviewed,
|
||||
}: GeneralFilterButtonProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
@ -395,6 +395,84 @@ function GeneralFilterButton({
|
||||
</Button>
|
||||
);
|
||||
const content = (
|
||||
<GeneralFilterContent
|
||||
allLabels={allLabels}
|
||||
selectedLabels={selectedLabels}
|
||||
currentLabels={currentLabels}
|
||||
showReviewed={showReviewed}
|
||||
reviewed={reviewed}
|
||||
updateLabelFilter={updateLabelFilter}
|
||||
setShowReviewed={setShowReviewed}
|
||||
setCurrentLabels={setCurrentLabels}
|
||||
setReviewed={setReviewed}
|
||||
onClose={() => setOpen(false)}
|
||||
/>
|
||||
);
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Drawer
|
||||
open={open}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setReviewed(showReviewed ?? 0);
|
||||
setCurrentLabels(selectedLabels);
|
||||
}
|
||||
|
||||
setOpen(open);
|
||||
}}
|
||||
>
|
||||
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
|
||||
<DrawerContent className="max-h-[75dvh] overflow-hidden">
|
||||
{content}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={open}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setReviewed(showReviewed ?? 0);
|
||||
setCurrentLabels(selectedLabels);
|
||||
}
|
||||
|
||||
setOpen(open);
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
|
||||
<PopoverContent side="left">{content}</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
type GeneralFilterContentProps = {
|
||||
allLabels: string[];
|
||||
selectedLabels: string[] | undefined;
|
||||
currentLabels: string[] | undefined;
|
||||
showReviewed?: 0 | 1;
|
||||
reviewed: 0 | 1;
|
||||
updateLabelFilter: (labels: string[] | undefined) => void;
|
||||
setCurrentLabels: (labels: string[] | undefined) => void;
|
||||
setShowReviewed: (reviewed?: 0 | 1) => void;
|
||||
setReviewed: (reviewed: 0 | 1) => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
export function GeneralFilterContent({
|
||||
allLabels,
|
||||
selectedLabels,
|
||||
currentLabels,
|
||||
showReviewed,
|
||||
reviewed,
|
||||
updateLabelFilter,
|
||||
setCurrentLabels,
|
||||
setShowReviewed,
|
||||
setReviewed,
|
||||
onClose,
|
||||
}: GeneralFilterContentProps) {
|
||||
return (
|
||||
<>
|
||||
<div className="flex p-2 justify-start items-center">
|
||||
<Switch
|
||||
@ -459,7 +537,7 @@ function GeneralFilterButton({
|
||||
updateLabelFilter(currentLabels);
|
||||
}
|
||||
|
||||
setOpen(false);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
Apply
|
||||
@ -478,44 +556,6 @@ function GeneralFilterButton({
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Drawer
|
||||
open={open}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setReviewed(showReviewed ?? 0);
|
||||
setCurrentLabels(selectedLabels);
|
||||
}
|
||||
|
||||
setOpen(open);
|
||||
}}
|
||||
>
|
||||
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
|
||||
<DrawerContent className="max-h-[75dvh] overflow-hidden">
|
||||
{content}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={open}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setReviewed(showReviewed ?? 0);
|
||||
setCurrentLabels(selectedLabels);
|
||||
}
|
||||
|
||||
setOpen(open);
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
|
||||
<PopoverContent side="left">{content}</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
type ShowMotionOnlyButtonProps = {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
@ -55,6 +55,72 @@ export default function ExportDialog({
|
||||
setRange,
|
||||
setMode,
|
||||
}: ExportDialogProps) {
|
||||
const Overlay = isDesktop ? Dialog : Drawer;
|
||||
const Trigger = isDesktop ? DialogTrigger : DrawerTrigger;
|
||||
const Content = isDesktop ? DialogContent : DrawerContent;
|
||||
|
||||
return (
|
||||
<Overlay
|
||||
open={mode == "select"}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setMode("none");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Trigger asChild>
|
||||
<Button
|
||||
className="flex items-center gap-2"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (mode == "none") {
|
||||
setMode("select");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FaArrowDown className="p-1 fill-secondary bg-muted-foreground rounded-md" />
|
||||
{isDesktop ? (mode != "timeline" ? "Export" : "Save") : null}
|
||||
</Button>
|
||||
</Trigger>
|
||||
<Content
|
||||
className={isDesktop ? "sm:rounded-2xl" : "px-4 pb-4 mx-4 rounded-2xl"}
|
||||
>
|
||||
<ExportContent
|
||||
camera={camera}
|
||||
latestTime={latestTime}
|
||||
currentTime={currentTime}
|
||||
range={range}
|
||||
mode={mode}
|
||||
setRange={setRange}
|
||||
setMode={setMode}
|
||||
onCancel={() => setMode("none")}
|
||||
/>
|
||||
</Content>
|
||||
</Overlay>
|
||||
);
|
||||
}
|
||||
|
||||
type ExportContentProps = {
|
||||
camera: string;
|
||||
latestTime: number;
|
||||
currentTime: number;
|
||||
range?: TimeRange;
|
||||
mode: ExportMode;
|
||||
setRange: (range: TimeRange | undefined) => void;
|
||||
setMode: (mode: ExportMode) => void;
|
||||
onCancel: () => void;
|
||||
};
|
||||
export function ExportContent({
|
||||
camera,
|
||||
latestTime,
|
||||
currentTime,
|
||||
range,
|
||||
mode,
|
||||
setRange,
|
||||
setMode,
|
||||
onCancel,
|
||||
}: ExportContentProps) {
|
||||
const [selectedOption, setSelectedOption] = useState<ExportOption>("1");
|
||||
const [name, setName] = useState("");
|
||||
|
||||
@ -131,112 +197,90 @@ export default function ExportDialog({
|
||||
});
|
||||
}, [camera, name, range, setRange]);
|
||||
|
||||
const Overlay = isDesktop ? Dialog : Drawer;
|
||||
const Trigger = isDesktop ? DialogTrigger : DrawerTrigger;
|
||||
const Content = isDesktop ? DialogContent : DrawerContent;
|
||||
useEffect(() => {
|
||||
if (mode != "timeline_save") {
|
||||
return;
|
||||
}
|
||||
|
||||
onStartExport();
|
||||
setMode("none");
|
||||
}, [mode, onStartExport, setMode]);
|
||||
|
||||
return (
|
||||
<Overlay
|
||||
open={mode == "select"}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setMode("none");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Trigger asChild>
|
||||
<>
|
||||
{isDesktop && (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Export</DialogTitle>
|
||||
</DialogHeader>
|
||||
<SelectSeparator className="bg-secondary" />
|
||||
</>
|
||||
)}
|
||||
<RadioGroup
|
||||
className={`flex flex-col gap-3 ${isDesktop ? "" : "mt-4"}`}
|
||||
onValueChange={(value) => onSelectTime(value as ExportOption)}
|
||||
>
|
||||
{EXPORT_OPTIONS.map((opt) => {
|
||||
return (
|
||||
<div key={opt} className="flex items-center gap-2">
|
||||
<RadioGroupItem
|
||||
className={
|
||||
opt == selectedOption
|
||||
? "from-selected/50 to-selected/90 text-selected bg-selected"
|
||||
: "from-secondary/50 to-secondary/90 text-secondary bg-secondary"
|
||||
}
|
||||
id={opt}
|
||||
value={opt}
|
||||
/>
|
||||
<Label className="cursor-pointer capitalize" htmlFor={opt}>
|
||||
{isNaN(parseInt(opt))
|
||||
? opt == "timeline"
|
||||
? "Select from Timeline"
|
||||
: `${opt}`
|
||||
: `Last ${opt > "1" ? `${opt} Hours` : "Hour"}`}
|
||||
</Label>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</RadioGroup>
|
||||
{selectedOption == "custom" && (
|
||||
<CustomTimeSelector
|
||||
latestTime={latestTime}
|
||||
range={range}
|
||||
setRange={setRange}
|
||||
/>
|
||||
)}
|
||||
<Input
|
||||
className="mt-3"
|
||||
type="search"
|
||||
placeholder="Name the Export"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
{isDesktop && <SelectSeparator className="bg-secondary" />}
|
||||
<DialogFooter
|
||||
className={isDesktop ? "" : "mt-3 flex flex-col-reverse gap-4"}
|
||||
>
|
||||
<div className="p-2 cursor-pointer" onClick={onCancel}>
|
||||
Cancel
|
||||
</div>
|
||||
<Button
|
||||
className="flex items-center gap-2"
|
||||
variant="secondary"
|
||||
variant="select"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (mode == "none") {
|
||||
setMode("select");
|
||||
} else if (mode == "timeline") {
|
||||
if (selectedOption == "timeline") {
|
||||
setRange({ before: currentTime + 30, after: currentTime - 30 });
|
||||
setMode("timeline");
|
||||
} else {
|
||||
onStartExport();
|
||||
setMode("none");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FaArrowDown className="p-1 fill-secondary bg-muted-foreground rounded-md" />
|
||||
{isDesktop ? (mode != "timeline" ? "Export" : "Save") : null}
|
||||
{selectedOption == "timeline" ? "Select" : "Export"}
|
||||
</Button>
|
||||
</Trigger>
|
||||
<Content
|
||||
className={isDesktop ? "sm:rounded-2xl" : "px-4 pb-4 mx-4 rounded-2xl"}
|
||||
>
|
||||
{isDesktop && (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Export</DialogTitle>
|
||||
</DialogHeader>
|
||||
<SelectSeparator className="bg-secondary" />
|
||||
</>
|
||||
)}
|
||||
<RadioGroup
|
||||
className={`flex flex-col gap-3 ${isDesktop ? "" : "mt-4"}`}
|
||||
onValueChange={(value) => onSelectTime(value as ExportOption)}
|
||||
>
|
||||
{EXPORT_OPTIONS.map((opt) => {
|
||||
return (
|
||||
<div key={opt} className="flex items-center gap-2">
|
||||
<RadioGroupItem
|
||||
className={
|
||||
opt == selectedOption
|
||||
? "from-selected/50 to-selected/90 text-selected bg-selected"
|
||||
: "from-secondary/50 to-secondary/90 text-secondary bg-secondary"
|
||||
}
|
||||
id={opt}
|
||||
value={opt}
|
||||
/>
|
||||
<Label className="cursor-pointer capitalize" htmlFor={opt}>
|
||||
{isNaN(parseInt(opt))
|
||||
? opt == "timeline"
|
||||
? "Select from Timeline"
|
||||
: `${opt}`
|
||||
: `Last ${opt > "1" ? `${opt} Hours` : "Hour"}`}
|
||||
</Label>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</RadioGroup>
|
||||
{selectedOption == "custom" && (
|
||||
<CustomTimeSelector
|
||||
latestTime={latestTime}
|
||||
range={range}
|
||||
setRange={setRange}
|
||||
/>
|
||||
)}
|
||||
<Input
|
||||
className="mt-3"
|
||||
type="search"
|
||||
placeholder="Name the Export"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
{isDesktop && <SelectSeparator className="bg-secondary" />}
|
||||
<DialogFooter
|
||||
className={isDesktop ? "" : "mt-3 flex flex-col-reverse gap-4"}
|
||||
>
|
||||
<DialogClose onClick={() => setMode("none")}>Cancel</DialogClose>
|
||||
<Button
|
||||
variant="select"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (selectedOption == "timeline") {
|
||||
setRange({ before: currentTime + 30, after: currentTime - 30 });
|
||||
setMode("timeline");
|
||||
} else {
|
||||
onStartExport();
|
||||
setMode("none");
|
||||
}
|
||||
}}
|
||||
>
|
||||
{selectedOption == "timeline" ? "Select" : "Export"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Content>
|
||||
</Overlay>
|
||||
</DialogFooter>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
231
web/src/components/overlay/MobileReviewSettingsDrawer.tsx
Normal file
231
web/src/components/overlay/MobileReviewSettingsDrawer.tsx
Normal file
@ -0,0 +1,231 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
|
||||
import { Button } from "../ui/button";
|
||||
import { FaArrowDown, FaCalendarAlt, FaCog, FaFilter } from "react-icons/fa";
|
||||
import { TimeRange } from "@/types/timeline";
|
||||
import { ExportContent } from "./ExportDialog";
|
||||
import { ExportMode } from "@/types/filter";
|
||||
import ReviewActivityCalendar from "./ReviewActivityCalendar";
|
||||
import { SelectSeparator } from "../ui/select";
|
||||
import { ReviewFilter } from "@/types/review";
|
||||
import { getEndOfDayTimestamp } from "@/utils/dateUtil";
|
||||
import { GeneralFilterContent } from "../filter/ReviewFilterGroup";
|
||||
import useSWR from "swr";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
|
||||
const ATTRIBUTES = ["amazon", "face", "fedex", "license_plate", "ups"];
|
||||
type DrawerMode = "none" | "select" | "export" | "calendar" | "filter";
|
||||
|
||||
type MobileReviewSettingsDrawerProps = {
|
||||
camera: string;
|
||||
filter?: ReviewFilter;
|
||||
latestTime: number;
|
||||
currentTime: number;
|
||||
range?: TimeRange;
|
||||
mode: ExportMode;
|
||||
onUpdateFilter: (filter: ReviewFilter | undefined) => void;
|
||||
setRange: (range: TimeRange | undefined) => void;
|
||||
setMode: (mode: ExportMode) => void;
|
||||
};
|
||||
export default function MobileReviewSettingsDrawer({
|
||||
camera,
|
||||
filter,
|
||||
latestTime,
|
||||
currentTime,
|
||||
range,
|
||||
mode,
|
||||
onUpdateFilter,
|
||||
setRange,
|
||||
setMode,
|
||||
}: MobileReviewSettingsDrawerProps) {
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
const [drawerMode, setDrawerMode] = useState<DrawerMode>("none");
|
||||
|
||||
const allLabels = useMemo<string[]>(() => {
|
||||
if (!config) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const labels = new Set<string>();
|
||||
const cameras = filter?.cameras || Object.keys(config.cameras);
|
||||
|
||||
cameras.forEach((camera) => {
|
||||
const cameraConfig = config.cameras[camera];
|
||||
cameraConfig.objects.track.forEach((label) => {
|
||||
if (!ATTRIBUTES.includes(label)) {
|
||||
labels.add(label);
|
||||
}
|
||||
});
|
||||
|
||||
if (cameraConfig.audio.enabled_in_config) {
|
||||
cameraConfig.audio.listen.forEach((label) => {
|
||||
labels.add(label);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return [...labels].sort();
|
||||
}, [config, filter]);
|
||||
const [currentLabels, setCurrentLabels] = useState<string[] | undefined>(
|
||||
filter?.labels,
|
||||
);
|
||||
|
||||
let content;
|
||||
if (drawerMode == "select") {
|
||||
content = (
|
||||
<div className="w-full p-4 flex flex-col gap-2">
|
||||
<Button
|
||||
className="w-full flex justify-center items-center gap-2"
|
||||
onClick={() => setDrawerMode("export")}
|
||||
>
|
||||
<FaArrowDown className="p-1 fill-secondary bg-muted-foreground rounded-md" />
|
||||
Export
|
||||
</Button>
|
||||
<Button
|
||||
className="w-full flex justify-center items-center gap-2"
|
||||
onClick={() => setDrawerMode("calendar")}
|
||||
>
|
||||
<FaCalendarAlt className="fill-muted-foreground" />
|
||||
Calendar
|
||||
</Button>
|
||||
<Button
|
||||
className="w-full flex justify-center items-center gap-2"
|
||||
onClick={() => setDrawerMode("filter")}
|
||||
>
|
||||
<FaFilter className="fill-muted-foreground" />
|
||||
Filter
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
} else if (drawerMode == "export") {
|
||||
content = (
|
||||
<ExportContent
|
||||
camera={camera}
|
||||
latestTime={latestTime}
|
||||
currentTime={currentTime}
|
||||
range={range}
|
||||
mode={mode}
|
||||
setRange={setRange}
|
||||
setMode={(mode) => {
|
||||
setMode(mode);
|
||||
|
||||
if (mode == "timeline") {
|
||||
setDrawerMode("none");
|
||||
}
|
||||
}}
|
||||
onCancel={() => {
|
||||
setMode("none");
|
||||
setRange(undefined);
|
||||
setDrawerMode("select");
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else if (drawerMode == "calendar") {
|
||||
content = (
|
||||
<div className="w-full flex flex-col">
|
||||
<div className="w-full h-8 relative">
|
||||
<div
|
||||
className="absolute left-0 text-selected"
|
||||
onClick={() => setDrawerMode("select")}
|
||||
>
|
||||
Back
|
||||
</div>
|
||||
<div className="absolute left-1/2 -translate-x-1/2 text-muted-foreground">
|
||||
Calendar
|
||||
</div>
|
||||
</div>
|
||||
<ReviewActivityCalendar
|
||||
selectedDay={
|
||||
filter?.after == undefined
|
||||
? undefined
|
||||
: new Date(filter.after * 1000)
|
||||
}
|
||||
onSelect={(day) => {
|
||||
onUpdateFilter({
|
||||
...filter,
|
||||
after: day == undefined ? undefined : day.getTime() / 1000,
|
||||
before: day == undefined ? undefined : getEndOfDayTimestamp(day),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<SelectSeparator />
|
||||
<div className="p-2 flex justify-center items-center">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
onUpdateFilter({
|
||||
...filter,
|
||||
after: undefined,
|
||||
before: undefined,
|
||||
});
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (drawerMode == "filter") {
|
||||
content = (
|
||||
<div className="w-full flex flex-col">
|
||||
<div className="w-full h-8 relative">
|
||||
<div
|
||||
className="absolute left-0 text-selected"
|
||||
onClick={() => setDrawerMode("select")}
|
||||
>
|
||||
Back
|
||||
</div>
|
||||
<div className="absolute left-1/2 -translate-x-1/2 text-muted-foreground">
|
||||
Filter
|
||||
</div>
|
||||
</div>
|
||||
<GeneralFilterContent
|
||||
allLabels={allLabels}
|
||||
selectedLabels={filter?.labels}
|
||||
currentLabels={currentLabels}
|
||||
showReviewed={0}
|
||||
reviewed={0}
|
||||
setCurrentLabels={setCurrentLabels}
|
||||
updateLabelFilter={(newLabels) =>
|
||||
onUpdateFilter({ ...filter, labels: newLabels })
|
||||
}
|
||||
setShowReviewed={() => {}}
|
||||
setReviewed={() => {}}
|
||||
onClose={() => setDrawerMode("select")}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
open={drawerMode != "none"}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setDrawerMode("none");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DrawerTrigger asChild>
|
||||
<Button
|
||||
className="rounded-lg capitalize"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => setDrawerMode("select")}
|
||||
>
|
||||
<FaCog className="text-muted-foreground" />
|
||||
</Button>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent className="max-h-[75dvh] overflow-hidden flex flex-col items-center gap-2 px-4 pb-4 mx-4 rounded-2xl">
|
||||
{content}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* <MobileTimelineDrawer
|
||||
selected={timelineType ?? "timeline"}
|
||||
onSelect={setTimelineType}
|
||||
/>
|
||||
*/
|
||||
46
web/src/components/overlay/MobileTimelineDrawer.tsx
Normal file
46
web/src/components/overlay/MobileTimelineDrawer.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import { useState } from "react";
|
||||
import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
|
||||
import { Button } from "../ui/button";
|
||||
import { FaFlag } from "react-icons/fa";
|
||||
import { TimelineType } from "@/types/timeline";
|
||||
|
||||
type MobileTimelineDrawerProps = {
|
||||
selected: TimelineType;
|
||||
onSelect: (timeline: TimelineType) => void;
|
||||
};
|
||||
export default function MobileTimelineDrawer({
|
||||
selected,
|
||||
onSelect,
|
||||
}: MobileTimelineDrawerProps) {
|
||||
const [drawer, setDrawer] = useState(false);
|
||||
|
||||
return (
|
||||
<Drawer open={drawer} onOpenChange={setDrawer}>
|
||||
<DrawerTrigger asChild>
|
||||
<Button className="rounded-lg capitalize" size="sm" variant="secondary">
|
||||
<FaFlag className="text-muted-foreground" />
|
||||
</Button>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent className="max-h-[75dvh] overflow-hidden flex flex-col items-center gap-2">
|
||||
<div
|
||||
className={`w-full mx-4 py-2 text-center capitalize ${selected == "timeline" ? "bg-secondary rounded-lg" : ""}`}
|
||||
onClick={() => {
|
||||
onSelect("timeline");
|
||||
setDrawer(false);
|
||||
}}
|
||||
>
|
||||
Timeline
|
||||
</div>
|
||||
<div
|
||||
className={`w-full mx-4 py-2 text-center capitalize ${selected == "events" ? "bg-secondary rounded-lg" : ""}`}
|
||||
onClick={() => {
|
||||
onSelect("events");
|
||||
setDrawer(false);
|
||||
}}
|
||||
>
|
||||
Events
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
@ -2,4 +2,4 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type FilterType = { [searchKey: string]: any };
|
||||
|
||||
export type ExportMode = "select" | "timeline" | "none";
|
||||
export type ExportMode = "select" | "timeline" | "timeline_save" | "none";
|
||||
|
||||
@ -24,3 +24,5 @@ export type Timeline = {
|
||||
};
|
||||
|
||||
export type TimeRange = { before: number; after: number };
|
||||
|
||||
export type TimelineType = "timeline" | "events";
|
||||
|
||||
@ -33,11 +33,13 @@ import { IoMdArrowRoundBack } from "react-icons/io";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import useSWR from "swr";
|
||||
import { TimeRange } from "@/types/timeline";
|
||||
import { TimeRange, TimelineType } from "@/types/timeline";
|
||||
import MobileCameraDrawer from "@/components/overlay/MobileCameraDrawer";
|
||||
import MobileTimelineDrawer from "@/components/overlay/MobileTimelineDrawer";
|
||||
import MobileReviewSettingsDrawer from "@/components/overlay/MobileReviewSettingsDrawer";
|
||||
import Logo from "@/components/Logo";
|
||||
|
||||
const SEGMENT_DURATION = 30;
|
||||
type TimelineType = "timeline" | "events";
|
||||
|
||||
type RecordingViewProps = {
|
||||
startCamera: string;
|
||||
@ -218,7 +220,12 @@ export function RecordingView({
|
||||
return (
|
||||
<div ref={contentRef} className="size-full flex flex-col">
|
||||
<Toaster />
|
||||
<div className={`w-full h-10 px-1 flex items-center justify-between`}>
|
||||
<div
|
||||
className={`w-full h-10 px-2 relative flex items-center justify-between`}
|
||||
>
|
||||
{isMobile && (
|
||||
<Logo className="absolute top-1 inset-x-1/2 -translate-x-1/2 h-8" />
|
||||
)}
|
||||
<Button
|
||||
className="flex items-center gap-2 rounded-lg"
|
||||
onClick={() => navigate(-1)}
|
||||
@ -237,24 +244,28 @@ export function RecordingView({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<ExportDialog
|
||||
camera={mainCamera}
|
||||
currentTime={currentTime}
|
||||
latestTime={timeRange.end}
|
||||
mode={exportMode}
|
||||
range={exportRange}
|
||||
setRange={setExportRange}
|
||||
setMode={setExportMode}
|
||||
/>
|
||||
<ReviewFilterGroup
|
||||
filters={["date", "general"]}
|
||||
reviewSummary={reviewSummary}
|
||||
filter={filter}
|
||||
onUpdateFilter={updateFilter}
|
||||
motionOnly={false}
|
||||
setMotionOnly={() => {}}
|
||||
/>
|
||||
{isDesktop && (
|
||||
<ExportDialog
|
||||
camera={mainCamera}
|
||||
currentTime={currentTime}
|
||||
latestTime={timeRange.end}
|
||||
mode={exportMode}
|
||||
range={exportRange}
|
||||
setRange={setExportRange}
|
||||
setMode={setExportMode}
|
||||
/>
|
||||
)}
|
||||
{isDesktop && (
|
||||
<ReviewFilterGroup
|
||||
filters={["date", "general"]}
|
||||
reviewSummary={reviewSummary}
|
||||
filter={filter}
|
||||
onUpdateFilter={updateFilter}
|
||||
motionOnly={false}
|
||||
setMotionOnly={() => {}}
|
||||
/>
|
||||
)}
|
||||
{isDesktop ? (
|
||||
<ToggleGroup
|
||||
className="*:px-3 *:py-4 *:rounded-md"
|
||||
type="single"
|
||||
@ -279,6 +290,24 @@ export function RecordingView({
|
||||
<div className="">Events</div>
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
) : (
|
||||
<MobileTimelineDrawer
|
||||
selected={timelineType ?? "timeline"}
|
||||
onSelect={setTimelineType}
|
||||
/>
|
||||
)}
|
||||
{isMobile && (
|
||||
<MobileReviewSettingsDrawer
|
||||
camera={mainCamera}
|
||||
filter={filter}
|
||||
currentTime={currentTime}
|
||||
latestTime={timeRange.end}
|
||||
mode={exportMode}
|
||||
range={exportRange}
|
||||
onUpdateFilter={updateFilter}
|
||||
setRange={setExportRange}
|
||||
setMode={setExportMode}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@ -351,32 +380,6 @@ export function RecordingView({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{isMobile && (
|
||||
<ToggleGroup
|
||||
className="py-2 *:px-3 *:py-4 *:rounded-md"
|
||||
type="single"
|
||||
size="sm"
|
||||
value={timelineType}
|
||||
onValueChange={(value: TimelineType) =>
|
||||
value ? setTimelineType(value) : null
|
||||
} // don't allow the severity to be unselected
|
||||
>
|
||||
<ToggleGroupItem
|
||||
className={`${timelineType == "timeline" ? "" : "text-gray-500"}`}
|
||||
value="timeline"
|
||||
aria-label="Select timeline"
|
||||
>
|
||||
<div className="">Timeline</div>
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem
|
||||
className={`${timelineType == "events" ? "" : "text-gray-500"}`}
|
||||
value="events"
|
||||
aria-label="Select events"
|
||||
>
|
||||
<div className="">Events</div>
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
)}
|
||||
<Timeline
|
||||
contentRef={contentRef}
|
||||
mainCamera={mainCamera}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user