Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions web/public/locales/en/components/dialog.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@
"name": {
"placeholder": "Name the Export"
},
"case": {
"label": "Case",
"placeholder": "Select a case"
},
"select": "Select",
"export": "Export",
"selectOrExport": "Select or Export",
Expand Down
27 changes: 23 additions & 4 deletions web/src/components/card/ExportCard.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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>
Expand Down
63 changes: 61 additions & 2 deletions web/src/components/overlay/ExportDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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",
Expand Down Expand Up @@ -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) {
Expand All @@ -89,6 +100,7 @@ export default function ExportDialog({
{
playback: "realtime",
name,
export_case_id: selectedCaseId || undefined,
},
)
.then((response) => {
Expand All @@ -102,6 +114,7 @@ export default function ExportDialog({
),
});
setName("");
setSelectedCaseId(undefined);
setRange(undefined);
setMode("none");
}
Expand All @@ -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]);
Expand Down Expand Up @@ -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}
Expand All @@ -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;
Expand All @@ -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) => {
Expand Down Expand Up @@ -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"}
Expand Down
10 changes: 9 additions & 1 deletion web/src/components/overlay/MobileReviewSettingsDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"), {
Expand All @@ -96,6 +99,7 @@ export default function MobileReviewSettingsDrawer({
{
playback: "realtime",
name,
export_case_id: selectedCaseId || undefined,
},
)
.then((response) => {
Expand All @@ -114,6 +118,7 @@ export default function MobileReviewSettingsDrawer({
},
);
setName("");
setSelectedCaseId(undefined);
setRange(undefined);
setMode("none");
}
Expand All @@ -133,7 +138,7 @@ export default function MobileReviewSettingsDrawer({
},
);
});
}, [camera, name, range, setRange, setName, setMode, t]);
}, [camera, name, range, selectedCaseId, setRange, setName, setMode, t]);

// filters

Expand Down Expand Up @@ -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);
Expand All @@ -213,6 +220,7 @@ export default function MobileReviewSettingsDrawer({
onCancel={() => {
setMode("none");
setRange(undefined);
setSelectedCaseId(undefined);
setDrawerMode("select");
}}
/>
Expand Down
4 changes: 4 additions & 0 deletions web/src/pages/Exports.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ function Exports() {
search={search}
cases={filteredCases}
exports={exports}
exportsByCase={exportsByCase}
setSelectedCaseId={setSelectedCaseId}
setSelected={setSelected}
renameClip={onHandleRename}
Expand All @@ -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;
Expand All @@ -348,6 +350,7 @@ function AllExportsView({
search,
cases,
exports,
exportsByCase,
setSelectedCaseId,
setSelected,
renameClip,
Expand Down Expand Up @@ -404,6 +407,7 @@ function AllExportsView({
: "hidden"
}
exportCase={item}
exports={exportsByCase[item.id] || []}
onSelect={() => {
setSelectedCaseId(item.id);
}}
Expand Down