diff --git a/web/src/components/card/SearchThumbnailFooter.tsx b/web/src/components/card/SearchThumbnailFooter.tsx index af7606b37..f66aca516 100644 --- a/web/src/components/card/SearchThumbnailFooter.tsx +++ b/web/src/components/card/SearchThumbnailFooter.tsx @@ -118,7 +118,7 @@ export default function SearchThumbnailFooter({ ) : (
- +
)} {formattedDate} diff --git a/web/src/components/overlay/detail/ObjectLifecycle.tsx b/web/src/components/overlay/detail/ObjectLifecycle.tsx index fb27966df..1bd55d9c8 100644 --- a/web/src/components/overlay/detail/ObjectLifecycle.tsx +++ b/web/src/components/overlay/detail/ObjectLifecycle.tsx @@ -383,7 +383,7 @@ export default function ObjectLifecycle({ {eventSequence.map((item, index) => ( - +
- void; + setDefaultView: (view: string) => void; +}; +export default function SearchSettings({ + className, + columns, + setColumns, + defaultView, + setDefaultView, +}: SearchSettingsProps) { + const [open, setOpen] = useState(false); + + const trigger = ( + + ); + const content = ( +
+
+
+
Default Search View
+
+ When no filters are selected, display a summary of the most recent + tracked objects per label, or display an unfiltered grid. +
+
+ +
+ +
+
+
Grid Columns
+
+ Select the number of columns in the results grid. +
+
+
+ setColumns(value)} + max={6} + min={2} + step={1} + className="flex-grow" + /> + {columns} +
+
+
+ ); + + return ( + { + setOpen(open); + }} + /> + ); +} diff --git a/web/src/pages/Explore.tsx b/web/src/pages/Explore.tsx index ffbef1060..770b45cb8 100644 --- a/web/src/pages/Explore.tsx +++ b/web/src/pages/Explore.tsx @@ -7,6 +7,7 @@ import ActivityIndicator from "@/components/indicators/activity-indicator"; import AnimatedCircularProgressBar from "@/components/ui/circular-progress-bar"; import { useApiFilterArgs } from "@/hooks/use-api-filter"; import { useTimezone } from "@/hooks/use-date-utils"; +import { usePersistence } from "@/hooks/use-persistence"; import { FrigateConfig } from "@/types/frigateConfig"; import { SearchFilter, SearchQuery, SearchResult } from "@/types/search"; import { ModelState } from "@/types/ws"; @@ -28,6 +29,18 @@ export default function Explore() { revalidateOnFocus: false, }); + // grid + + const [columnCount, setColumnCount] = usePersistence("exploreGridColumns", 4); + const gridColumns = useMemo(() => columnCount ?? 4, [columnCount]); + + // default layout + + const [defaultView, setDefaultView] = usePersistence( + "exploreDefaultView", + "summary", + ); + const timezone = useTimezone(config); const [search, setSearch] = useState(""); @@ -65,7 +78,11 @@ export default function Explore() { const searchQuery: SearchQuery = useMemo(() => { // no search parameters if (searchSearchParams && Object.keys(searchSearchParams).length === 0) { - return null; + if (defaultView == "grid") { + return ["events", {}]; + } else { + return null; + } } // parameters, but no search term and not similarity @@ -117,7 +134,7 @@ export default function Explore() { include_thumbnails: 0, }, ]; - }, [searchTerm, searchSearchParams, similaritySearch, timezone]); + }, [searchTerm, searchSearchParams, similaritySearch, timezone, defaultView]); // paging @@ -385,6 +402,8 @@ export default function Explore() { searchResults={searchResults} isLoading={(isLoadingInitialData || isLoadingMore) ?? true} hasMore={!isReachingEnd} + columns={gridColumns} + defaultView={defaultView} setSearch={setSearch} setSimilaritySearch={(search) => { setSearchFilter({ @@ -395,6 +414,8 @@ export default function Explore() { }} setSearchFilter={setSearchFilter} onUpdateFilter={setSearchFilter} + setColumns={setColumnCount} + setDefaultView={setDefaultView} loadMore={loadMore} refresh={mutate} /> diff --git a/web/src/views/explore/ExploreView.tsx b/web/src/views/explore/ExploreView.tsx index e2c8e63bc..3a0b9cc7b 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, isSafari } from "react-device-detect"; +import { isDesktop, isIOS, isMobileOnly, isSafari } from "react-device-detect"; import useSWR from "swr"; import { useApiHost } from "@/api"; import { cn } from "@/lib/utils"; @@ -17,6 +17,7 @@ import useImageLoaded from "@/hooks/use-image-loaded"; import ActivityIndicator from "@/components/indicators/activity-indicator"; import { useEventUpdate } from "@/api/ws"; import { isEqual } from "lodash"; +import TimeAgo from "@/components/dynamic/TimeAgo"; type ExploreViewProps = { searchDetail: SearchResult | undefined; @@ -197,6 +198,7 @@ function ExploreThumbnailImage({ className="absolute inset-0" imgLoaded={imgLoaded} /> + + {isDesktop && ( +
+ {event.end_time ? ( + + ) : ( +
+ +
+ )} +
+ )} ); } diff --git a/web/src/views/search/SearchView.tsx b/web/src/views/search/SearchView.tsx index 9427cdcff..b22a5248a 100644 --- a/web/src/views/search/SearchView.tsx +++ b/web/src/views/search/SearchView.tsx @@ -5,17 +5,12 @@ import SearchDetailDialog, { SearchTab, } from "@/components/overlay/detail/SearchDetailDialog"; import { Toaster } from "@/components/ui/sonner"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import { FrigateConfig } from "@/types/frigateConfig"; import { SearchFilter, SearchResult, SearchSource } from "@/types/search"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { isDesktop, isMobileOnly } from "react-device-detect"; -import { LuColumns, LuSearchX } from "react-icons/lu"; +import { isMobileOnly } from "react-device-detect"; +import { LuSearchX } from "react-icons/lu"; import useSWR from "swr"; import ExploreView from "../explore/ExploreView"; import useKeyboardListener, { @@ -26,14 +21,8 @@ import InputWithTags from "@/components/input/InputWithTags"; import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; import { isEqual } from "lodash"; import { formatDateToLocaleString } from "@/utils/dateUtil"; -import { Slider } from "@/components/ui/slider"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; -import { usePersistence } from "@/hooks/use-persistence"; import SearchThumbnailFooter from "@/components/card/SearchThumbnailFooter"; +import SearchSettings from "@/components/settings/SearchSettings"; type SearchViewProps = { search: string; @@ -42,12 +31,16 @@ type SearchViewProps = { searchResults?: SearchResult[]; isLoading: boolean; hasMore: boolean; + columns: number; + defaultView?: string; setSearch: (search: string) => void; setSimilaritySearch: (search: SearchResult) => void; setSearchFilter: (filter: SearchFilter) => void; onUpdateFilter: (filter: SearchFilter) => void; loadMore: () => void; refresh: () => void; + setColumns: (columns: number) => void; + setDefaultView: (name: string) => void; }; export default function SearchView({ search, @@ -56,12 +49,16 @@ export default function SearchView({ searchResults, isLoading, hasMore, + columns, + defaultView = "summary", setSearch, setSimilaritySearch, setSearchFilter, onUpdateFilter, loadMore, refresh, + setColumns, + setDefaultView, }: SearchViewProps) { const contentRef = useRef(null); const { data: config } = useSWR("config", { @@ -70,18 +67,15 @@ export default function SearchView({ // grid - const [columnCount, setColumnCount] = usePersistence("exploreGridColumns", 4); - const effectiveColumnCount = useMemo(() => columnCount ?? 4, [columnCount]); - const gridClassName = cn( "grid w-full gap-2 px-1 gap-2 lg:gap-4 md:mx-2", isMobileOnly && "grid-cols-2", { - "sm:grid-cols-2": effectiveColumnCount <= 2, - "sm:grid-cols-3": effectiveColumnCount === 3, - "sm:grid-cols-4": effectiveColumnCount === 4, - "sm:grid-cols-5": effectiveColumnCount === 5, - "sm:grid-cols-6": effectiveColumnCount === 6, + "sm:grid-cols-2": columns <= 2, + "sm:grid-cols-3": columns === 3, + "sm:grid-cols-4": columns === 4, + "sm:grid-cols-5": columns === 5, + "sm:grid-cols-6": columns === 6, }, ); @@ -342,7 +336,7 @@ export default function SearchView({ {hasExistingSearch && ( -
+
+
@@ -425,53 +425,13 @@ export default function SearchView({
{hasMore && isLoading && }
- - {isDesktop && columnCount && ( -
- - - - -
- -
-
-
- Adjust Grid Columns -
- -
-
- Grid Columns -
-
- setColumnCount(value)} - max={6} - min={2} - step={1} - className="flex-grow" - /> - - {effectiveColumnCount} - -
-
-
-
-
- )} )}
{searchFilter && Object.keys(searchFilter).length === 0 && - !searchTerm && ( + !searchTerm && + defaultView == "summary" && (