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" && (