mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-01-22 20:18:30 +03:00
Exports Improvements (#21521)
* Add images to case folder view * Add ability to select case in export dialog * Add to mobile review too
This commit is contained in:
parent
aa0b082184
commit
26744efb1e
@ -48,6 +48,10 @@
|
||||
"name": {
|
||||
"placeholder": "Name the Export"
|
||||
},
|
||||
"case": {
|
||||
"label": "Case",
|
||||
"placeholder": "Select a case"
|
||||
},
|
||||
"select": "Select",
|
||||
"export": "Export",
|
||||
"selectOrExport": "Select or Export",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import ActivityIndicator from "../indicators/activity-indicator";
|
||||
import { Button } from "../ui/button";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import { FiMoreVertical } from "react-icons/fi";
|
||||
import { Skeleton } from "../ui/skeleton";
|
||||
@ -32,18 +32,37 @@ import { FaFolder } from "react-icons/fa";
|
||||
type CaseCardProps = {
|
||||
className: string;
|
||||
exportCase: ExportCase;
|
||||
exports: Export[];
|
||||
onSelect: () => void;
|
||||
};
|
||||
export function CaseCard({ className, exportCase, onSelect }: CaseCardProps) {
|
||||
export function CaseCard({
|
||||
className,
|
||||
exportCase,
|
||||
exports,
|
||||
onSelect,
|
||||
}: CaseCardProps) {
|
||||
const firstExport = useMemo(
|
||||
() => exports.find((exp) => exp.thumb_path && exp.thumb_path.length > 0),
|
||||
[exports],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex aspect-video size-full cursor-pointer items-center justify-center rounded-lg bg-secondary md:rounded-2xl",
|
||||
"relative flex aspect-video size-full cursor-pointer items-center justify-center overflow-hidden rounded-lg bg-secondary md:rounded-2xl",
|
||||
className,
|
||||
)}
|
||||
onClick={() => onSelect()}
|
||||
>
|
||||
<div className="absolute bottom-2 left-2 flex items-center justify-start gap-2">
|
||||
{firstExport && (
|
||||
<img
|
||||
className="absolute inset-0 size-full object-cover"
|
||||
src={`${baseUrl}${firstExport.thumb_path.replace("/media/frigate/", "")}`}
|
||||
alt=""
|
||||
/>
|
||||
)}
|
||||
<div className="pointer-events-none absolute inset-x-0 bottom-0 z-10 h-16 bg-gradient-to-t from-black/60 to-transparent" />
|
||||
<div className="absolute bottom-2 left-2 z-20 flex items-center justify-start gap-2 text-white">
|
||||
<FaFolder />
|
||||
<div className="capitalize">{exportCase.name}</div>
|
||||
</div>
|
||||
|
||||
@ -22,7 +22,14 @@ import useSWR from "swr";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
|
||||
import { TimezoneAwareCalendar } from "./ReviewActivityCalendar";
|
||||
import { SelectSeparator } from "../ui/select";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../ui/select";
|
||||
import { isDesktop, isIOS, isMobile } from "react-device-detect";
|
||||
import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
|
||||
import SaveExportOverlay from "./SaveExportOverlay";
|
||||
@ -31,6 +38,7 @@ import { baseUrl } from "@/api/baseUrl";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { GenericVideoPlayer } from "../player/GenericVideoPlayer";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ExportCase } from "@/types/export";
|
||||
|
||||
const EXPORT_OPTIONS = [
|
||||
"1",
|
||||
@ -67,6 +75,9 @@ export default function ExportDialog({
|
||||
}: ExportDialogProps) {
|
||||
const { t } = useTranslation(["components/dialog"]);
|
||||
const [name, setName] = useState("");
|
||||
const [selectedCaseId, setSelectedCaseId] = useState<string | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
const onStartExport = useCallback(() => {
|
||||
if (!range) {
|
||||
@ -89,6 +100,7 @@ export default function ExportDialog({
|
||||
{
|
||||
playback: "realtime",
|
||||
name,
|
||||
export_case_id: selectedCaseId || undefined,
|
||||
},
|
||||
)
|
||||
.then((response) => {
|
||||
@ -102,6 +114,7 @@ export default function ExportDialog({
|
||||
),
|
||||
});
|
||||
setName("");
|
||||
setSelectedCaseId(undefined);
|
||||
setRange(undefined);
|
||||
setMode("none");
|
||||
}
|
||||
@ -118,10 +131,11 @@ export default function ExportDialog({
|
||||
{ position: "top-center" },
|
||||
);
|
||||
});
|
||||
}, [camera, name, range, setRange, setName, setMode, t]);
|
||||
}, [camera, name, range, selectedCaseId, setRange, setName, setMode, t]);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
setName("");
|
||||
setSelectedCaseId(undefined);
|
||||
setMode("none");
|
||||
setRange(undefined);
|
||||
}, [setMode, setRange]);
|
||||
@ -190,8 +204,10 @@ export default function ExportDialog({
|
||||
currentTime={currentTime}
|
||||
range={range}
|
||||
name={name}
|
||||
selectedCaseId={selectedCaseId}
|
||||
onStartExport={onStartExport}
|
||||
setName={setName}
|
||||
setSelectedCaseId={setSelectedCaseId}
|
||||
setRange={setRange}
|
||||
setMode={setMode}
|
||||
onCancel={handleCancel}
|
||||
@ -207,8 +223,10 @@ type ExportContentProps = {
|
||||
currentTime: number;
|
||||
range?: TimeRange;
|
||||
name: string;
|
||||
selectedCaseId?: string;
|
||||
onStartExport: () => void;
|
||||
setName: (name: string) => void;
|
||||
setSelectedCaseId: (caseId: string | undefined) => void;
|
||||
setRange: (range: TimeRange | undefined) => void;
|
||||
setMode: (mode: ExportMode) => void;
|
||||
onCancel: () => void;
|
||||
@ -218,14 +236,17 @@ export function ExportContent({
|
||||
currentTime,
|
||||
range,
|
||||
name,
|
||||
selectedCaseId,
|
||||
onStartExport,
|
||||
setName,
|
||||
setSelectedCaseId,
|
||||
setRange,
|
||||
setMode,
|
||||
onCancel,
|
||||
}: ExportContentProps) {
|
||||
const { t } = useTranslation(["components/dialog"]);
|
||||
const [selectedOption, setSelectedOption] = useState<ExportOption>("1");
|
||||
const { data: cases } = useSWR<ExportCase[]>("cases");
|
||||
|
||||
const onSelectTime = useCallback(
|
||||
(option: ExportOption) => {
|
||||
@ -320,6 +341,44 @@ export function ExportContent({
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
<div className="my-4">
|
||||
<Label className="text-sm text-secondary-foreground">
|
||||
{t("export.case.label", { defaultValue: "Case (optional)" })}
|
||||
</Label>
|
||||
<Select
|
||||
value={selectedCaseId || "none"}
|
||||
onValueChange={(value) =>
|
||||
setSelectedCaseId(value === "none" ? undefined : value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="mt-2">
|
||||
<SelectValue
|
||||
placeholder={t("export.case.placeholder", {
|
||||
defaultValue: "Select a case (optional)",
|
||||
})}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
value="none"
|
||||
className="cursor-pointer hover:bg-accent hover:text-accent-foreground"
|
||||
>
|
||||
{t("label.none", { ns: "common" })}
|
||||
</SelectItem>
|
||||
{cases
|
||||
?.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((caseItem) => (
|
||||
<SelectItem
|
||||
key={caseItem.id}
|
||||
value={caseItem.id}
|
||||
className="cursor-pointer hover:bg-accent hover:text-accent-foreground"
|
||||
>
|
||||
{caseItem.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{isDesktop && <SelectSeparator className="my-4 bg-secondary" />}
|
||||
<DialogFooter
|
||||
className={isDesktop ? "" : "mt-3 flex flex-col-reverse gap-4"}
|
||||
|
||||
@ -75,6 +75,9 @@ export default function MobileReviewSettingsDrawer({
|
||||
// exports
|
||||
|
||||
const [name, setName] = useState("");
|
||||
const [selectedCaseId, setSelectedCaseId] = useState<string | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const onStartExport = useCallback(() => {
|
||||
if (!range) {
|
||||
toast.error(t("toast.error.noValidTimeSelected"), {
|
||||
@ -96,6 +99,7 @@ export default function MobileReviewSettingsDrawer({
|
||||
{
|
||||
playback: "realtime",
|
||||
name,
|
||||
export_case_id: selectedCaseId || undefined,
|
||||
},
|
||||
)
|
||||
.then((response) => {
|
||||
@ -114,6 +118,7 @@ export default function MobileReviewSettingsDrawer({
|
||||
},
|
||||
);
|
||||
setName("");
|
||||
setSelectedCaseId(undefined);
|
||||
setRange(undefined);
|
||||
setMode("none");
|
||||
}
|
||||
@ -133,7 +138,7 @@ export default function MobileReviewSettingsDrawer({
|
||||
},
|
||||
);
|
||||
});
|
||||
}, [camera, name, range, setRange, setName, setMode, t]);
|
||||
}, [camera, name, range, selectedCaseId, setRange, setName, setMode, t]);
|
||||
|
||||
// filters
|
||||
|
||||
@ -200,8 +205,10 @@ export default function MobileReviewSettingsDrawer({
|
||||
currentTime={currentTime}
|
||||
range={range}
|
||||
name={name}
|
||||
selectedCaseId={selectedCaseId}
|
||||
onStartExport={onStartExport}
|
||||
setName={setName}
|
||||
setSelectedCaseId={setSelectedCaseId}
|
||||
setRange={setRange}
|
||||
setMode={(mode) => {
|
||||
setMode(mode);
|
||||
@ -213,6 +220,7 @@ export default function MobileReviewSettingsDrawer({
|
||||
onCancel={() => {
|
||||
setMode("none");
|
||||
setRange(undefined);
|
||||
setSelectedCaseId(undefined);
|
||||
setDrawerMode("select");
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -321,6 +321,7 @@ function Exports() {
|
||||
search={search}
|
||||
cases={filteredCases}
|
||||
exports={exports}
|
||||
exportsByCase={exportsByCase}
|
||||
setSelectedCaseId={setSelectedCaseId}
|
||||
setSelected={setSelected}
|
||||
renameClip={onHandleRename}
|
||||
@ -337,6 +338,7 @@ type AllExportsViewProps = {
|
||||
search: string;
|
||||
cases?: ExportCase[];
|
||||
exports: Export[];
|
||||
exportsByCase: { [caseId: string]: Export[] };
|
||||
setSelectedCaseId: (id: string) => void;
|
||||
setSelected: (e: Export) => void;
|
||||
renameClip: (id: string, update: string) => void;
|
||||
@ -348,6 +350,7 @@ function AllExportsView({
|
||||
search,
|
||||
cases,
|
||||
exports,
|
||||
exportsByCase,
|
||||
setSelectedCaseId,
|
||||
setSelected,
|
||||
renameClip,
|
||||
@ -404,6 +407,7 @@ function AllExportsView({
|
||||
: "hidden"
|
||||
}
|
||||
exportCase={item}
|
||||
exports={exportsByCase[item.id] || []}
|
||||
onSelect={() => {
|
||||
setSelectedCaseId(item.id);
|
||||
}}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user