diff --git a/web/package-lock.json b/web/package-lock.json index 15bd003f3..de42a1ac7 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -37,6 +37,7 @@ "copy-to-clipboard": "^3.3.3", "date-fns": "^3.6.0", "embla-carousel-react": "^8.2.0", + "framer-motion": "^11.5.4", "hls.js": "^1.5.14", "idb-keyval": "^6.2.1", "immer": "^10.1.1", @@ -4717,6 +4718,31 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/framer-motion": { + "version": "11.5.4", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.5.4.tgz", + "integrity": "sha512-E+tb3/G6SO69POkdJT+3EpdMuhmtCh9EWuK4I1DnIC23L7tFPrl8vxP+LSovwaw6uUr73rUbpb4FgK011wbRJQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", diff --git a/web/package.json b/web/package.json index 148d74919..bb15ea4b8 100644 --- a/web/package.json +++ b/web/package.json @@ -43,6 +43,7 @@ "copy-to-clipboard": "^3.3.3", "date-fns": "^3.6.0", "embla-carousel-react": "^8.2.0", + "framer-motion": "^11.5.4", "hls.js": "^1.5.14", "idb-keyval": "^6.2.1", "immer": "^10.1.1", diff --git a/web/src/components/mobile/MobilePage.tsx b/web/src/components/mobile/MobilePage.tsx new file mode 100644 index 000000000..8dbcc2f61 --- /dev/null +++ b/web/src/components/mobile/MobilePage.tsx @@ -0,0 +1,121 @@ +import { cn } from "@/lib/utils"; +import { isPWA } from "@/utils/isPWA"; +import { ReactNode, useEffect, useState } from "react"; +import { Button } from "../ui/button"; +import { IoMdArrowRoundBack } from "react-icons/io"; +import { motion, AnimatePresence } from "framer-motion"; + +type MobilePageProps = { + children: ReactNode; + open: boolean; + onOpenChange: (open: boolean) => void; +}; + +export function MobilePage({ children, open, onOpenChange }: MobilePageProps) { + const [isVisible, setIsVisible] = useState(open); + + useEffect(() => { + if (open) { + setIsVisible(true); + } + }, [open]); + + const handleAnimationComplete = () => { + if (!open) { + setIsVisible(false); + onOpenChange(false); + } + }; + + return ( + + {isVisible && ( + + {children} + + )} + + ); +} + +type MobileComponentProps = { + children: ReactNode; + className?: string; +}; + +export function MobilePageContent({ + children, + className, + ...props +}: MobileComponentProps) { + return ( +
+ {children} +
+ ); +} + +export function MobilePageDescription({ + children, + className, + ...props +}: MobileComponentProps) { + return ( +

+ {children} +

+ ); +} + +interface MobilePageHeaderProps extends React.HTMLAttributes { + onClose: () => void; +} + +export function MobilePageHeader({ + children, + className, + onClose, + ...props +}: MobilePageHeaderProps) { + return ( +
+ +
{children}
+
+ ); +} + +export function MobilePageTitle({ + children, + className, + ...props +}: MobileComponentProps) { + return ( +

+ {children} +

+ ); +} diff --git a/web/src/components/overlay/detail/ObjectLifecycle.tsx b/web/src/components/overlay/detail/ObjectLifecycle.tsx index ca8851b92..377710b09 100644 --- a/web/src/components/overlay/detail/ObjectLifecycle.tsx +++ b/web/src/components/overlay/detail/ObjectLifecycle.tsx @@ -230,7 +230,7 @@ export default function ObjectLifecycle({ {!fullscreen && (
)} -
+
)} -
+
(
(); const [pane, setPane] = useState("overview"); - const Overlay = isDesktop ? Sheet : Drawer; - const Content = isDesktop ? SheetContent : DrawerContent; - const Header = isDesktop ? SheetHeader : DrawerHeader; - const Title = isDesktop ? SheetTitle : DrawerTitle; - const Description = isDesktop ? SheetDescription : DrawerDescription; + // dialog and mobile page + + const [isOpen, setIsOpen] = useState(review != undefined); + + 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) { return; @@ -94,7 +102,7 @@ export default function ReviewDetailDialog({ return ( <> { if (!open) { setReview(undefined); @@ -115,19 +123,43 @@ export default function ReviewDetailDialog({ -
- Review Item Details - Review item details -
+
{pane == "overview" && ( -
+
setIsOpen(false)}> + Review Item Details + Review item details +
+ + + + + Share this review item + +
+
+ )} + {pane == "overview" && ( +
@@ -140,21 +172,11 @@ export default function ReviewDetailDialog({
Timestamp
{formattedDate}
-
Objects
-
+
{events?.map((event) => { return (
("details"); 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(() => { if (!config || !search) { return []; @@ -102,15 +110,15 @@ export default function SearchDetailDialog({ // content - const Overlay = isDesktop ? Dialog : Drawer; - const Content = isDesktop ? DialogContent : DrawerContent; - const Header = isDesktop ? DialogHeader : DrawerHeader; - const Title = isDesktop ? DialogTitle : DrawerTitle; - const Description = isDesktop ? DialogDescription : DrawerDescription; + const Overlay = isDesktop ? Dialog : MobilePage; + const Content = isDesktop ? DialogContent : MobilePageContent; + const Header = isDesktop ? DialogHeader : MobilePageHeader; + const Title = isDesktop ? DialogTitle : MobilePageTitle; + const Description = isDesktop ? DialogDescription : MobilePageDescription; return ( { if (!open) { setSearch(undefined); @@ -118,15 +126,16 @@ export default function SearchDetailDialog({ }} > -
+
setIsOpen(false)}> Tracked Object Details - Tracked object details + Tracked object details
+
@@ -303,7 +312,7 @@ function ObjectDetailsTab({
{formattedDate}
-
+
{ if (!config || !upload) { return ""; @@ -79,60 +83,74 @@ export function FrigatePlusDialog({ const content = ( - - Submit To Frigate+ - - Objects in locations you want to avoid are not false positives. - Submitting them as false positives will confuse the model. - - - - {upload?.id && ( - {`${upload?.label}`} - )} - +
+ + + Submit To Frigate+ + + + Objects in locations you want to avoid are not false positives. + Submitting them as false positives will confuse the model. + + + + {upload?.id && ( + {`${upload?.label}`} + )} + - - {state == "reviewing" && ( - <> - {dialog && } - - - - )} - {state == "uploading" && } - + + {state == "reviewing" && ( + <> + {dialog && } + + + + )} + {state == "uploading" && } + +
); diff --git a/web/src/utils/browserUtil.ts b/web/src/utils/browserUtil.ts index 78f740649..b6a82fb54 100644 --- a/web/src/utils/browserUtil.ts +++ b/web/src/utils/browserUtil.ts @@ -9,7 +9,7 @@ export function shareOrCopy(url: string, title?: string) { }); } else { copy(url); - toast.success("Copied to clipboard.", { + toast.success("Copied URL to clipboard.", { position: "top-center", }); } diff --git a/web/src/views/explore/ExploreView.tsx b/web/src/views/explore/ExploreView.tsx index 46c7e6bc3..b8ab51d80 100644 --- a/web/src/views/explore/ExploreView.tsx +++ b/web/src/views/explore/ExploreView.tsx @@ -60,7 +60,7 @@ export default function ExploreView({ onSelectSearch }: ExploreViewProps) { } return ( -
+
{Object.entries(eventsByLabel).map(([label, filteredEvents]) => ( )} {!uniqueResults && !isLoading && ( -
+
)}