mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-14 23:25:25 +03:00
use mobile page component for review and search detail
This commit is contained in:
parent
73b3daedee
commit
3cdaccb6af
@ -6,13 +6,6 @@ import {
|
|||||||
SheetHeader,
|
SheetHeader,
|
||||||
SheetTitle,
|
SheetTitle,
|
||||||
} from "../../ui/sheet";
|
} from "../../ui/sheet";
|
||||||
import {
|
|
||||||
Drawer,
|
|
||||||
DrawerContent,
|
|
||||||
DrawerDescription,
|
|
||||||
DrawerHeader,
|
|
||||||
DrawerTitle,
|
|
||||||
} from "../../ui/drawer";
|
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
import { useFormattedTimestamp } from "@/hooks/use-date-utils";
|
import { useFormattedTimestamp } from "@/hooks/use-date-utils";
|
||||||
@ -20,7 +13,7 @@ import { getIconForLabel } from "@/utils/iconUtil";
|
|||||||
import { useApiHost } from "@/api";
|
import { useApiHost } from "@/api";
|
||||||
import { ReviewDetailPaneType, ReviewSegment } from "@/types/review";
|
import { ReviewDetailPaneType, ReviewSegment } from "@/types/review";
|
||||||
import { Event } from "@/types/event";
|
import { Event } from "@/types/event";
|
||||||
import { useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { FrigatePlusDialog } from "../dialog/FrigatePlusDialog";
|
import { FrigatePlusDialog } from "../dialog/FrigatePlusDialog";
|
||||||
import ObjectLifecycle from "./ObjectLifecycle";
|
import ObjectLifecycle from "./ObjectLifecycle";
|
||||||
@ -37,6 +30,13 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { baseUrl } from "@/api/baseUrl";
|
import { baseUrl } from "@/api/baseUrl";
|
||||||
import { shareOrCopy } from "@/utils/browserUtil";
|
import { shareOrCopy } from "@/utils/browserUtil";
|
||||||
|
import {
|
||||||
|
MobilePage,
|
||||||
|
MobilePageContent,
|
||||||
|
MobilePageDescription,
|
||||||
|
MobilePageHeader,
|
||||||
|
MobilePageTitle,
|
||||||
|
} from "@/components/mobile/MobilePage";
|
||||||
|
|
||||||
type ReviewDetailDialogProps = {
|
type ReviewDetailDialogProps = {
|
||||||
review?: ReviewSegment;
|
review?: ReviewSegment;
|
||||||
@ -80,11 +80,19 @@ export default function ReviewDetailDialog({
|
|||||||
const [selectedEvent, setSelectedEvent] = useState<Event>();
|
const [selectedEvent, setSelectedEvent] = useState<Event>();
|
||||||
const [pane, setPane] = useState<ReviewDetailPaneType>("overview");
|
const [pane, setPane] = useState<ReviewDetailPaneType>("overview");
|
||||||
|
|
||||||
const Overlay = isDesktop ? Sheet : Drawer;
|
// dialog and mobile page
|
||||||
const Content = isDesktop ? SheetContent : DrawerContent;
|
|
||||||
const Header = isDesktop ? SheetHeader : DrawerHeader;
|
const [isOpen, setIsOpen] = useState(review != undefined);
|
||||||
const Title = isDesktop ? SheetTitle : DrawerTitle;
|
|
||||||
const Description = isDesktop ? SheetDescription : DrawerDescription;
|
useEffect(() => {
|
||||||
|
setIsOpen(review != undefined);
|
||||||
|
}, [review]);
|
||||||
|
|
||||||
|
const Overlay = isDesktop ? Sheet : MobilePage;
|
||||||
|
const Content = isDesktop ? SheetContent : MobilePageContent;
|
||||||
|
const Header = isDesktop ? SheetHeader : MobilePageHeader;
|
||||||
|
const Title = isDesktop ? SheetTitle : MobilePageTitle;
|
||||||
|
const Description = isDesktop ? SheetDescription : MobilePageDescription;
|
||||||
|
|
||||||
if (!review) {
|
if (!review) {
|
||||||
return;
|
return;
|
||||||
@ -93,7 +101,7 @@ export default function ReviewDetailDialog({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Overlay
|
<Overlay
|
||||||
open={review != undefined}
|
open={isOpen}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
setReview(undefined);
|
setReview(undefined);
|
||||||
@ -114,19 +122,43 @@ export default function ReviewDetailDialog({
|
|||||||
|
|
||||||
<Content
|
<Content
|
||||||
className={cn(
|
className={cn(
|
||||||
isDesktop
|
"scrollbar-container overflow-y-auto",
|
||||||
? pane == "overview"
|
isDesktop && pane == "overview"
|
||||||
? "sm:max-w-xl"
|
? "sm:max-w-xl"
|
||||||
: "pt-2 sm:max-w-4xl"
|
: "pt-2 sm:max-w-4xl",
|
||||||
: "max-h-[80dvh] overflow-hidden p-2 pb-4",
|
isMobile && "px-4",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Header className="sr-only">
|
<span tabIndex={0} className="sr-only" />
|
||||||
<Title>Review Item Details</Title>
|
|
||||||
<Description>Review item details</Description>
|
|
||||||
</Header>
|
|
||||||
{pane == "overview" && (
|
{pane == "overview" && (
|
||||||
<div className="scrollbar-container mt-3 flex size-full flex-col gap-5 overflow-y-auto">
|
<Header className="justify-center" onClose={() => setIsOpen(false)}>
|
||||||
|
<Title>Review Item Details</Title>
|
||||||
|
<Description className="sr-only">Review item details</Description>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute",
|
||||||
|
isDesktop && "right-1 top-8",
|
||||||
|
isMobile && "right-0 top-3",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
shareOrCopy(`${baseUrl}review?id=${review.id}`)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FaShareAlt className="size-4 text-secondary-foreground" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Share this review item</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</Header>
|
||||||
|
)}
|
||||||
|
{pane == "overview" && (
|
||||||
|
<div className="flex flex-col gap-5 md:mt-3">
|
||||||
<div className="flex w-full flex-row">
|
<div className="flex w-full flex-row">
|
||||||
<div className="flex w-full flex-col gap-3">
|
<div className="flex w-full flex-col gap-3">
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
@ -139,21 +171,11 @@ export default function ReviewDetailDialog({
|
|||||||
<div className="text-sm text-primary/40">Timestamp</div>
|
<div className="text-sm text-primary/40">Timestamp</div>
|
||||||
<div className="text-sm">{formattedDate}</div>
|
<div className="text-sm">{formattedDate}</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
|
||||||
className="flex max-w-24 gap-2"
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
onClick={() =>
|
|
||||||
shareOrCopy(`${baseUrl}review?id=${review.id}`)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FaShareAlt className="size-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full flex-col items-center gap-2">
|
<div className="flex w-full flex-col items-center gap-2">
|
||||||
<div className="flex w-full flex-col gap-1.5">
|
<div className="flex w-full flex-col gap-1.5">
|
||||||
<div className="text-sm text-primary/40">Objects</div>
|
<div className="text-sm text-primary/40">Objects</div>
|
||||||
<div className="scrollbar-container flex max-h-32 flex-col items-start gap-2 overflow-y-scroll text-sm capitalize">
|
<div className="scrollbar-container flex max-h-32 flex-col items-start gap-2 overflow-y-auto text-sm capitalize">
|
||||||
{events?.map((event) => {
|
{events?.map((event) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -1,11 +1,4 @@
|
|||||||
import { isDesktop, isIOS, isMobile } from "react-device-detect";
|
import { isDesktop, isIOS, isMobile } from "react-device-detect";
|
||||||
import {
|
|
||||||
Drawer,
|
|
||||||
DrawerContent,
|
|
||||||
DrawerDescription,
|
|
||||||
DrawerHeader,
|
|
||||||
DrawerTitle,
|
|
||||||
} from "../../ui/drawer";
|
|
||||||
import { SearchResult } from "@/types/search";
|
import { SearchResult } from "@/types/search";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
@ -37,6 +30,13 @@ import { ASPECT_VERTICAL_LAYOUT, ASPECT_WIDE_LAYOUT } from "@/types/record";
|
|||||||
import { FaImage, FaRegListAlt, FaVideo } from "react-icons/fa";
|
import { FaImage, FaRegListAlt, FaVideo } from "react-icons/fa";
|
||||||
import { FaRotate } from "react-icons/fa6";
|
import { FaRotate } from "react-icons/fa6";
|
||||||
import ObjectLifecycle from "./ObjectLifecycle";
|
import ObjectLifecycle from "./ObjectLifecycle";
|
||||||
|
import {
|
||||||
|
MobilePage,
|
||||||
|
MobilePageContent,
|
||||||
|
MobilePageDescription,
|
||||||
|
MobilePageHeader,
|
||||||
|
MobilePageTitle,
|
||||||
|
} from "@/components/mobile/MobilePage";
|
||||||
|
|
||||||
const SEARCH_TABS = [
|
const SEARCH_TABS = [
|
||||||
"details",
|
"details",
|
||||||
@ -65,6 +65,14 @@ export default function SearchDetailDialog({
|
|||||||
const [page, setPage] = useState<SearchTab>("details");
|
const [page, setPage] = useState<SearchTab>("details");
|
||||||
const [pageToggle, setPageToggle] = useOptimisticState(page, setPage, 100);
|
const [pageToggle, setPageToggle] = useOptimisticState(page, setPage, 100);
|
||||||
|
|
||||||
|
// dialog and mobile page
|
||||||
|
|
||||||
|
const [isOpen, setIsOpen] = useState(search != undefined);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsOpen(search != undefined);
|
||||||
|
}, [search]);
|
||||||
|
|
||||||
const searchTabs = useMemo(() => {
|
const searchTabs = useMemo(() => {
|
||||||
if (!config || !search) {
|
if (!config || !search) {
|
||||||
return [];
|
return [];
|
||||||
@ -102,15 +110,15 @@ export default function SearchDetailDialog({
|
|||||||
|
|
||||||
// content
|
// content
|
||||||
|
|
||||||
const Overlay = isDesktop ? Dialog : Drawer;
|
const Overlay = isDesktop ? Dialog : MobilePage;
|
||||||
const Content = isDesktop ? DialogContent : DrawerContent;
|
const Content = isDesktop ? DialogContent : MobilePageContent;
|
||||||
const Header = isDesktop ? DialogHeader : DrawerHeader;
|
const Header = isDesktop ? DialogHeader : MobilePageHeader;
|
||||||
const Title = isDesktop ? DialogTitle : DrawerTitle;
|
const Title = isDesktop ? DialogTitle : MobilePageTitle;
|
||||||
const Description = isDesktop ? DialogDescription : DrawerDescription;
|
const Description = isDesktop ? DialogDescription : MobilePageDescription;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Overlay
|
<Overlay
|
||||||
open={search != undefined}
|
open={isOpen}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
setSearch(undefined);
|
setSearch(undefined);
|
||||||
@ -118,15 +126,16 @@ export default function SearchDetailDialog({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Content
|
<Content
|
||||||
className={
|
className={cn(
|
||||||
isDesktop
|
"scrollbar-container overflow-y-auto",
|
||||||
? "sm:max-w-xl md:max-w-4xl lg:max-w-4xl xl:max-w-7xl"
|
isDesktop &&
|
||||||
: "max-h-[75dvh] overflow-hidden px-2 pb-4"
|
"max-h-[95dvh] sm:max-w-xl md:max-w-4xl lg:max-w-4xl xl:max-w-7xl",
|
||||||
}
|
isMobile && "px-4",
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<Header className="sr-only">
|
<Header onClose={() => setIsOpen(false)}>
|
||||||
<Title>Tracked Object Details</Title>
|
<Title>Tracked Object Details</Title>
|
||||||
<Description>Tracked object details</Description>
|
<Description className="sr-only">Tracked object details</Description>
|
||||||
</Header>
|
</Header>
|
||||||
<ScrollArea
|
<ScrollArea
|
||||||
className={cn("w-full whitespace-nowrap", isMobile && "my-2")}
|
className={cn("w-full whitespace-nowrap", isMobile && "my-2")}
|
||||||
@ -274,7 +283,7 @@ function ObjectDetailsTab({
|
|||||||
}, [desc, search]);
|
}, [desc, search]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-3 flex size-full flex-col gap-5 md:mt-0">
|
<div className="flex flex-col gap-5">
|
||||||
<div className="flex w-full flex-row">
|
<div className="flex w-full flex-row">
|
||||||
<div className="flex w-full flex-col gap-3">
|
<div className="flex w-full flex-col gap-3">
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
@ -302,7 +311,7 @@ function ObjectDetailsTab({
|
|||||||
<div className="text-sm">{formattedDate}</div>
|
<div className="text-sm">{formattedDate}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full flex-col gap-2 px-6">
|
<div className="flex w-full flex-col gap-2 pl-6">
|
||||||
<img
|
<img
|
||||||
className="aspect-video select-none rounded-lg object-contain transition-opacity"
|
className="aspect-video select-none rounded-lg object-contain transition-opacity"
|
||||||
style={
|
style={
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user