mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-16 08:05:22 +03:00
better arrangement of thumbnail footer items on smaller screens
This commit is contained in:
parent
1d00364a9d
commit
982f1e3719
@ -33,9 +33,11 @@ import { toast } from "sonner";
|
|||||||
import { MdImageSearch } from "react-icons/md";
|
import { MdImageSearch } from "react-icons/md";
|
||||||
import { isMobileOnly } from "react-device-detect";
|
import { isMobileOnly } from "react-device-detect";
|
||||||
import { buttonVariants } from "../ui/button";
|
import { buttonVariants } from "../ui/button";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type SearchThumbnailProps = {
|
type SearchThumbnailProps = {
|
||||||
searchResult: SearchResult;
|
searchResult: SearchResult;
|
||||||
|
columns: number;
|
||||||
findSimilar: () => void;
|
findSimilar: () => void;
|
||||||
refreshResults: () => void;
|
refreshResults: () => void;
|
||||||
showObjectLifecycle: () => void;
|
showObjectLifecycle: () => void;
|
||||||
@ -43,6 +45,7 @@ type SearchThumbnailProps = {
|
|||||||
|
|
||||||
export default function SearchThumbnailFooter({
|
export default function SearchThumbnailFooter({
|
||||||
searchResult,
|
searchResult,
|
||||||
|
columns,
|
||||||
findSimilar,
|
findSimilar,
|
||||||
refreshResults,
|
refreshResults,
|
||||||
showObjectLifecycle,
|
showObjectLifecycle,
|
||||||
@ -114,104 +117,112 @@ export default function SearchThumbnailFooter({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex flex-col items-start text-xs text-primary-variant">
|
<div
|
||||||
{searchResult.end_time ? (
|
className={cn(
|
||||||
<TimeAgo time={searchResult.start_time * 1000} dense />
|
"flex w-full flex-row items-center justify-between",
|
||||||
) : (
|
columns > 4 &&
|
||||||
<div>
|
"items-start sm:flex-col sm:gap-2 lg:flex-row lg:items-center lg:gap-1",
|
||||||
<ActivityIndicator size={14} />
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
{formattedDate}
|
>
|
||||||
</div>
|
<div className="flex flex-col items-start text-xs text-primary-variant">
|
||||||
<div className="flex flex-row items-center justify-end gap-6 md:gap-4">
|
{searchResult.end_time ? (
|
||||||
{!isMobileOnly &&
|
<TimeAgo time={searchResult.start_time * 1000} dense />
|
||||||
config?.plus?.enabled &&
|
) : (
|
||||||
searchResult.has_snapshot &&
|
<div>
|
||||||
searchResult.end_time &&
|
<ActivityIndicator size={14} />
|
||||||
!searchResult.plus_id && (
|
</div>
|
||||||
|
)}
|
||||||
|
{formattedDate}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row items-center justify-end gap-6 md:gap-4">
|
||||||
|
{!isMobileOnly &&
|
||||||
|
config?.plus?.enabled &&
|
||||||
|
searchResult.has_snapshot &&
|
||||||
|
searchResult.end_time &&
|
||||||
|
!searchResult.plus_id && (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<FrigatePlusIcon
|
||||||
|
className="size-5 cursor-pointer text-primary-variant hover:text-primary"
|
||||||
|
onClick={() => setShowFrigatePlus(true)}
|
||||||
|
/>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Submit to Frigate+</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{config?.semantic_search?.enabled && (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<FrigatePlusIcon
|
<MdImageSearch
|
||||||
className="size-5 cursor-pointer text-primary-variant hover:text-primary"
|
className="size-5 cursor-pointer text-primary-variant hover:text-primary"
|
||||||
onClick={() => setShowFrigatePlus(true)}
|
onClick={findSimilar}
|
||||||
/>
|
/>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>Submit to Frigate+</TooltipContent>
|
<TooltipContent>Find similar</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{config?.semantic_search?.enabled && (
|
<DropdownMenu>
|
||||||
<Tooltip>
|
<DropdownMenuTrigger>
|
||||||
<TooltipTrigger>
|
<LuMoreVertical className="size-5 cursor-pointer text-primary-variant hover:text-primary" />
|
||||||
<MdImageSearch
|
</DropdownMenuTrigger>
|
||||||
className="size-5 cursor-pointer text-primary-variant hover:text-primary"
|
<DropdownMenuContent align={"end"}>
|
||||||
onClick={findSimilar}
|
{searchResult.has_clip && (
|
||||||
/>
|
<DropdownMenuItem>
|
||||||
</TooltipTrigger>
|
<a
|
||||||
<TooltipContent>Find similar</TooltipContent>
|
className="justify_start flex items-center"
|
||||||
</Tooltip>
|
href={`${baseUrl}api/events/${searchResult.id}/clip.mp4`}
|
||||||
)}
|
download={`${searchResult.camera}_${searchResult.label}.mp4`}
|
||||||
|
>
|
||||||
<DropdownMenu>
|
<LuDownload className="mr-2 size-4" />
|
||||||
<DropdownMenuTrigger>
|
<span>Download video</span>
|
||||||
<LuMoreVertical className="size-5 cursor-pointer text-primary-variant hover:text-primary" />
|
</a>
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align={"end"}>
|
|
||||||
{searchResult.has_clip && (
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<a
|
|
||||||
className="justify_start flex items-center"
|
|
||||||
href={`${baseUrl}api/events/${searchResult.id}/clip.mp4`}
|
|
||||||
download={`${searchResult.camera}_${searchResult.label}.mp4`}
|
|
||||||
>
|
|
||||||
<LuDownload className="mr-2 size-4" />
|
|
||||||
<span>Download video</span>
|
|
||||||
</a>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
{searchResult.has_snapshot && (
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<a
|
|
||||||
className="justify_start flex items-center"
|
|
||||||
href={`${baseUrl}api/events/${searchResult.id}/snapshot.jpg`}
|
|
||||||
download={`${searchResult.camera}_${searchResult.label}.jpg`}
|
|
||||||
>
|
|
||||||
<LuCamera className="mr-2 size-4" />
|
|
||||||
<span>Download snapshot</span>
|
|
||||||
</a>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={showObjectLifecycle}
|
|
||||||
>
|
|
||||||
<FaArrowsRotate className="mr-2 size-4" />
|
|
||||||
<span>View object lifecycle</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
|
|
||||||
{isMobileOnly &&
|
|
||||||
config?.plus?.enabled &&
|
|
||||||
searchResult.has_snapshot &&
|
|
||||||
searchResult.end_time &&
|
|
||||||
!searchResult.plus_id && (
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => setShowFrigatePlus(true)}
|
|
||||||
>
|
|
||||||
<FrigatePlusIcon className="mr-2 size-4 cursor-pointer text-primary" />
|
|
||||||
<span>Submit to Frigate+</span>
|
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
<DropdownMenuItem
|
{searchResult.has_snapshot && (
|
||||||
className="cursor-pointer"
|
<DropdownMenuItem>
|
||||||
onClick={() => setDeleteDialogOpen(true)}
|
<a
|
||||||
>
|
className="justify_start flex items-center"
|
||||||
<LuTrash2 className="mr-2 size-4" />
|
href={`${baseUrl}api/events/${searchResult.id}/snapshot.jpg`}
|
||||||
<span>Delete</span>
|
download={`${searchResult.camera}_${searchResult.label}.jpg`}
|
||||||
</DropdownMenuItem>
|
>
|
||||||
</DropdownMenuContent>
|
<LuCamera className="mr-2 size-4" />
|
||||||
</DropdownMenu>
|
<span>Download snapshot</span>
|
||||||
|
</a>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={showObjectLifecycle}
|
||||||
|
>
|
||||||
|
<FaArrowsRotate className="mr-2 size-4" />
|
||||||
|
<span>View object lifecycle</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
{isMobileOnly &&
|
||||||
|
config?.plus?.enabled &&
|
||||||
|
searchResult.has_snapshot &&
|
||||||
|
searchResult.end_time &&
|
||||||
|
!searchResult.plus_id && (
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => setShowFrigatePlus(true)}
|
||||||
|
>
|
||||||
|
<FrigatePlusIcon className="mr-2 size-4 cursor-pointer text-primary" />
|
||||||
|
<span>Submit to Frigate+</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => setDeleteDialogOpen(true)}
|
||||||
|
>
|
||||||
|
<LuTrash2 className="mr-2 size-4" />
|
||||||
|
<span>Delete</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { isDesktop } from "react-device-detect";
|
import { isDesktop, isMobileOnly } from "react-device-detect";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog";
|
import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog";
|
||||||
import { FaCog } from "react-icons/fa";
|
import { FaCog } from "react-icons/fa";
|
||||||
@ -68,26 +68,32 @@ export default function SearchSettings({
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<DropdownMenuSeparator />
|
{!isMobileOnly && (
|
||||||
<div className="flex w-full flex-col space-y-4">
|
<>
|
||||||
<div className="space-y-0.5">
|
<DropdownMenuSeparator />
|
||||||
<div className="text-md">Grid Columns</div>
|
<div className="flex w-full flex-col space-y-4">
|
||||||
<div className="space-y-1 text-xs text-muted-foreground">
|
<div className="space-y-0.5">
|
||||||
Select the number of columns in the grid view.
|
<div className="text-md">Grid Columns</div>
|
||||||
|
<div className="space-y-1 text-xs text-muted-foreground">
|
||||||
|
Select the number of columns in the grid view.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<Slider
|
||||||
|
value={[columns]}
|
||||||
|
onValueChange={([value]) => setColumns(value)}
|
||||||
|
max={6}
|
||||||
|
min={2}
|
||||||
|
step={1}
|
||||||
|
className="flex-grow"
|
||||||
|
/>
|
||||||
|
<span className="w-9 text-center text-sm font-medium">
|
||||||
|
{columns}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
<div className="flex items-center space-x-4">
|
)}
|
||||||
<Slider
|
|
||||||
value={[columns]}
|
|
||||||
onValueChange={([value]) => setColumns(value)}
|
|
||||||
max={6}
|
|
||||||
min={2}
|
|
||||||
step={1}
|
|
||||||
className="flex-grow"
|
|
||||||
/>
|
|
||||||
<span className="w-9 text-center text-sm font-medium">{columns}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import { ModelState } from "@/types/ws";
|
|||||||
import { formatSecondsToDuration } from "@/utils/dateUtil";
|
import { formatSecondsToDuration } from "@/utils/dateUtil";
|
||||||
import SearchView from "@/views/search/SearchView";
|
import SearchView from "@/views/search/SearchView";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import { isMobileOnly } from "react-device-detect";
|
||||||
import { LuCheck, LuExternalLink, LuX } from "react-icons/lu";
|
import { LuCheck, LuExternalLink, LuX } from "react-icons/lu";
|
||||||
import { TbExclamationCircle } from "react-icons/tb";
|
import { TbExclamationCircle } from "react-icons/tb";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
@ -32,7 +33,12 @@ export default function Explore() {
|
|||||||
// grid
|
// grid
|
||||||
|
|
||||||
const [columnCount, setColumnCount] = usePersistence("exploreGridColumns", 4);
|
const [columnCount, setColumnCount] = usePersistence("exploreGridColumns", 4);
|
||||||
const gridColumns = useMemo(() => columnCount ?? 4, [columnCount]);
|
const gridColumns = useMemo(() => {
|
||||||
|
if (isMobileOnly) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
return columnCount ?? 4;
|
||||||
|
}, [columnCount]);
|
||||||
|
|
||||||
// default layout
|
// default layout
|
||||||
|
|
||||||
|
|||||||
@ -387,7 +387,7 @@ export default function SearchView({
|
|||||||
key={value.id}
|
key={value.id}
|
||||||
ref={(item) => (itemRefs.current[index] = item)}
|
ref={(item) => (itemRefs.current[index] = item)}
|
||||||
data-start={value.start_time}
|
data-start={value.start_time}
|
||||||
className="review-item relative rounded-lg"
|
className="review-item relative flex flex-col rounded-lg"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -402,9 +402,10 @@ export default function SearchView({
|
|||||||
<div
|
<div
|
||||||
className={`review-item-ring pointer-events-none absolute inset-0 z-10 size-full rounded-lg outline outline-[3px] -outline-offset-[2.8px] ${selected ? `shadow-selected outline-selected` : "outline-transparent duration-500"}`}
|
className={`review-item-ring pointer-events-none absolute inset-0 z-10 size-full rounded-lg outline outline-[3px] -outline-offset-[2.8px] ${selected ? `shadow-selected outline-selected` : "outline-transparent duration-500"}`}
|
||||||
/>
|
/>
|
||||||
<div className="flex w-full items-center justify-between rounded-b-lg border border-t-0 bg-card p-3 text-card-foreground">
|
<div className="flex w-full grow items-center justify-between rounded-b-lg border border-t-0 bg-card p-3 text-card-foreground">
|
||||||
<SearchThumbnailFooter
|
<SearchThumbnailFooter
|
||||||
searchResult={value}
|
searchResult={value}
|
||||||
|
columns={columns}
|
||||||
findSimilar={() => {
|
findSimilar={() => {
|
||||||
if (config?.semantic_search.enabled) {
|
if (config?.semantic_search.enabled) {
|
||||||
setSimilaritySearch(value);
|
setSimilaritySearch(value);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user