From 863f51363ade0697500c6d0741da7617e0303c37 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Wed, 11 Sep 2024 12:32:45 -0500 Subject: [PATCH] Explore UI tweaks (#13679) * Loading indicators and filter bar tweaks * remove unnecessary bits from search thumbnail * simplify * add video loading indicator * clean up --- web/src/components/card/SearchThumbnail.tsx | 78 +++--------------- .../overlay/detail/SearchDetailDialog.tsx | 26 +++--- web/src/views/explore/ExploreView.tsx | 79 ++++++++++++++----- web/src/views/search/SearchView.tsx | 21 +++-- 4 files changed, 93 insertions(+), 111 deletions(-) diff --git a/web/src/components/card/SearchThumbnail.tsx b/web/src/components/card/SearchThumbnail.tsx index 5ce653a4c..3f9a4a6a5 100644 --- a/web/src/components/card/SearchThumbnail.tsx +++ b/web/src/components/card/SearchThumbnail.tsx @@ -1,14 +1,13 @@ -import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback } from "react"; import { useApiHost } from "@/api"; import { getIconForLabel } from "@/utils/iconUtil"; import TimeAgo from "../dynamic/TimeAgo"; import useSWR from "swr"; import { FrigateConfig } from "@/types/frigateConfig"; -import { isIOS, isMobile, isSafari } from "react-device-detect"; +import { isIOS, isSafari } from "react-device-detect"; import Chip from "@/components/indicators/Chip"; import { useFormattedTimestamp } from "@/hooks/use-date-utils"; import useImageLoaded from "@/hooks/use-image-loaded"; -import { useSwipeable } from "react-swipeable"; import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; import ImageLoadingIndicator from "../indicators/ImageLoadingIndicator"; import ActivityIndicator from "../indicators/activity-indicator"; @@ -19,14 +18,12 @@ import { cn } from "@/lib/utils"; type SearchThumbnailProps = { searchResult: SearchResult; - scrollLock?: boolean; findSimilar: () => void; - onClick: (searchResult: SearchResult, detail: boolean) => void; + onClick: (searchResult: SearchResult) => void; }; export default function SearchThumbnail({ searchResult, - scrollLock = false, findSimilar, onClick, }: SearchThumbnailProps) { @@ -34,58 +31,11 @@ export default function SearchThumbnail({ const { data: config } = useSWR("config"); const [imgRef, imgLoaded, onImgLoad] = useImageLoaded(); - // interaction - - const swipeHandlers = useSwipeable({ - onSwipedLeft: () => setDetails(false), - onSwipedRight: () => setDetails(true), - preventScrollOnSwipe: true, - }); - useContextMenu(imgRef, findSimilar); - // Hover Details - - const [hoverTimeout, setHoverTimeout] = useState(); - const [details, setDetails] = useState(false); - const [tooltipHovering, setTooltipHovering] = useState(false); - const showingMoreDetail = useMemo( - () => details && !tooltipHovering, - [details, tooltipHovering], - ); - const [isHovered, setIsHovered] = useState(false); - - const handleOnClick = useCallback( - (e: React.MouseEvent) => { - if (!showingMoreDetail) { - onClick(searchResult, e.metaKey); - } - }, - [searchResult, showingMoreDetail, onClick], - ); - - useEffect(() => { - if (isHovered && scrollLock) { - return; - } - - if (isHovered && !tooltipHovering) { - setHoverTimeout( - setTimeout(() => { - setDetails(true); - setHoverTimeout(null); - }, 500), - ); - } else { - if (hoverTimeout) { - clearTimeout(hoverTimeout); - } - - setDetails(false); - } - // we know that these deps are correct - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isHovered, scrollLock, tooltipHovering]); + const handleOnClick = useCallback(() => { + onClick(searchResult); + }, [searchResult, onClick]); // date @@ -95,13 +45,7 @@ export default function SearchThumbnail({ ); return ( -
setIsHovered(true)} - onMouseLeave={isMobile ? undefined : () => setIsHovered(false)} - onClick={handleOnClick} - {...swipeHandlers} - > +
-
setTooltipHovering(true)} - onMouseLeave={() => setTooltipHovering(false)} - > +
{ <> onClick(searchResult, true)} + onClick={() => onClick(searchResult)} > {getIconForLabel( searchResult.label, diff --git a/web/src/components/overlay/detail/SearchDetailDialog.tsx b/web/src/components/overlay/detail/SearchDetailDialog.tsx index 754712933..89928c986 100644 --- a/web/src/components/overlay/detail/SearchDetailDialog.tsx +++ b/web/src/components/overlay/detail/SearchDetailDialog.tsx @@ -32,6 +32,7 @@ import { Event } from "@/types/event"; import HlsVideoPlayer from "@/components/player/HlsVideoPlayer"; import { baseUrl } from "@/api/baseUrl"; import { cn } from "@/lib/utils"; +import ActivityIndicator from "@/components/indicators/activity-indicator"; const SEARCH_TABS = ["details", "Frigate+", "video"] as const; type SearchTab = (typeof SEARCH_TABS)[number]; @@ -312,19 +313,26 @@ type VideoTabProps = { search: SearchResult; }; function VideoTab({ search }: VideoTabProps) { + const [isLoading, setIsLoading] = useState(true); const videoRef = useRef(null); const endTime = useMemo(() => search.end_time ?? Date.now() / 1000, [search]); return ( - + <> + {isLoading && ( + + )} + setIsLoading(false)} + /> + ); } diff --git a/web/src/views/explore/ExploreView.tsx b/web/src/views/explore/ExploreView.tsx index 8145312e3..3f9f7a3d7 100644 --- a/web/src/views/explore/ExploreView.tsx +++ b/web/src/views/explore/ExploreView.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo } from "react"; -import { isIOS, isMobileOnly } from "react-device-detect"; +import { isIOS, isMobileOnly, isSafari } from "react-device-detect"; import useSWR from "swr"; import { useApiHost } from "@/api"; import { cn } from "@/lib/utils"; @@ -12,9 +12,12 @@ import { } from "@/components/ui/tooltip"; import { TooltipPortal } from "@radix-ui/react-tooltip"; import { SearchResult } from "@/types/search"; +import ImageLoadingIndicator from "@/components/indicators/ImageLoadingIndicator"; +import useImageLoaded from "@/hooks/use-image-loaded"; +import ActivityIndicator from "@/components/indicators/activity-indicator"; type ExploreViewProps = { - onSelectSearch: (searchResult: SearchResult, detail: boolean) => void; + onSelectSearch: (searchResult: SearchResult) => void; }; export default function ExploreView({ onSelectSearch }: ExploreViewProps) { @@ -50,8 +53,14 @@ export default function ExploreView({ onSelectSearch }: ExploreViewProps) { }, {}); }, [events]); + if (!events) { + return ( + + ); + } + return ( -
+
{Object.entries(eventsByLabel).map(([label, filteredEvents]) => ( void; + onSelectSearch: (searchResult: SearchResult) => void; }; function ThumbnailRow({ @@ -75,7 +84,6 @@ function ThumbnailRow({ searchResults, onSelectSearch, }: ThumbnailRowType) { - const apiHost = useApiHost(); const navigate = useNavigate(); const handleSearch = (label: string) => { @@ -106,22 +114,9 @@ function ThumbnailRow({ key={event.id} className="relative aspect-square h-auto max-w-[20%] flex-grow md:max-w-[10%]" > - {`${objectType} onSelectSearch(event, true)} +
))} @@ -148,6 +143,48 @@ function ThumbnailRow({ ); } +type ExploreThumbnailImageProps = { + event: SearchResult; + onSelectSearch: (searchResult: SearchResult) => void; +}; +function ExploreThumbnailImage({ + event, + onSelectSearch, +}: ExploreThumbnailImageProps) { + const apiHost = useApiHost(); + const [imgRef, imgLoaded, onImgLoad] = useImageLoaded(); + + return ( + <> + + onSelectSearch(event)} + onLoad={() => { + onImgLoad(); + }} + /> + + ); +} + function ExploreMoreLink({ objectType }: { objectType: string }) { const formattedType = objectType.replaceAll("_", " "); const label = formattedType.endsWith("s") diff --git a/web/src/views/search/SearchView.tsx b/web/src/views/search/SearchView.tsx index 0cc0f99dc..366ea813e 100644 --- a/web/src/views/search/SearchView.tsx +++ b/web/src/views/search/SearchView.tsx @@ -65,12 +65,8 @@ export default function SearchView({ // search interaction - const onSelectSearch = useCallback((item: SearchResult, detail: boolean) => { - if (detail) { - setSearchDetail(item); - } else { - setSearchDetail(item); - } + const onSelectSearch = useCallback((item: SearchResult) => { + setSearchDetail(item); }, []); // confidence score - probably needs tweaking @@ -110,18 +106,18 @@ export default function SearchView({
{config?.semantic_search?.enabled && (
@@ -179,9 +177,8 @@ export default function SearchView({ > setSimilaritySearch(value)} - onClick={onSelectSearch} + onClick={() => onSelectSearch(value)} /> {(searchTerm || similaritySearch) && (