better arrangement of thumbnail footer items on smaller screens

This commit is contained in:
Josh Hawkins 2024-10-16 23:12:34 -05:00
parent 1d00364a9d
commit 982f1e3719
4 changed files with 134 additions and 110 deletions

View File

@ -33,9 +33,11 @@ import { toast } from "sonner";
import { MdImageSearch } from "react-icons/md";
import { isMobileOnly } from "react-device-detect";
import { buttonVariants } from "../ui/button";
import { cn } from "@/lib/utils";
type SearchThumbnailProps = {
searchResult: SearchResult;
columns: number;
findSimilar: () => void;
refreshResults: () => void;
showObjectLifecycle: () => void;
@ -43,6 +45,7 @@ type SearchThumbnailProps = {
export default function SearchThumbnailFooter({
searchResult,
columns,
findSimilar,
refreshResults,
showObjectLifecycle,
@ -114,104 +117,112 @@ export default function SearchThumbnailFooter({
}}
/>
<div className="flex flex-col items-start text-xs text-primary-variant">
{searchResult.end_time ? (
<TimeAgo time={searchResult.start_time * 1000} dense />
) : (
<div>
<ActivityIndicator size={14} />
</div>
<div
className={cn(
"flex w-full flex-row items-center justify-between",
columns > 4 &&
"items-start sm:flex-col sm:gap-2 lg:flex-row lg:items-center lg:gap-1",
)}
{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 && (
>
<div className="flex flex-col items-start text-xs text-primary-variant">
{searchResult.end_time ? (
<TimeAgo time={searchResult.start_time * 1000} dense />
) : (
<div>
<ActivityIndicator size={14} />
</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>
<TooltipTrigger>
<FrigatePlusIcon
<MdImageSearch
className="size-5 cursor-pointer text-primary-variant hover:text-primary"
onClick={() => setShowFrigatePlus(true)}
onClick={findSimilar}
/>
</TooltipTrigger>
<TooltipContent>Submit to Frigate+</TooltipContent>
<TooltipContent>Find similar</TooltipContent>
</Tooltip>
)}
{config?.semantic_search?.enabled && (
<Tooltip>
<TooltipTrigger>
<MdImageSearch
className="size-5 cursor-pointer text-primary-variant hover:text-primary"
onClick={findSimilar}
/>
</TooltipTrigger>
<TooltipContent>Find similar</TooltipContent>
</Tooltip>
)}
<DropdownMenu>
<DropdownMenuTrigger>
<LuMoreVertical className="size-5 cursor-pointer text-primary-variant hover:text-primary" />
</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>
<DropdownMenu>
<DropdownMenuTrigger>
<LuMoreVertical className="size-5 cursor-pointer text-primary-variant hover:text-primary" />
</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>
)}
<DropdownMenuItem
className="cursor-pointer"
onClick={() => setDeleteDialogOpen(true)}
>
<LuTrash2 className="mr-2 size-4" />
<span>Delete</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{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
className="cursor-pointer"
onClick={() => setDeleteDialogOpen(true)}
>
<LuTrash2 className="mr-2 size-4" />
<span>Delete</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</>
);

View File

@ -1,6 +1,6 @@
import { Button } from "../ui/button";
import { useState } from "react";
import { isDesktop } from "react-device-detect";
import { isDesktop, isMobileOnly } from "react-device-detect";
import { cn } from "@/lib/utils";
import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog";
import { FaCog } from "react-icons/fa";
@ -68,26 +68,32 @@ export default function SearchSettings({
</SelectContent>
</Select>
</div>
<DropdownMenuSeparator />
<div className="flex w-full flex-col space-y-4">
<div className="space-y-0.5">
<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.
{!isMobileOnly && (
<>
<DropdownMenuSeparator />
<div className="flex w-full flex-col space-y-4">
<div className="space-y-0.5">
<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 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>
);

View File

@ -14,6 +14,7 @@ import { ModelState } from "@/types/ws";
import { formatSecondsToDuration } from "@/utils/dateUtil";
import SearchView from "@/views/search/SearchView";
import { useCallback, useEffect, useMemo, useState } from "react";
import { isMobileOnly } from "react-device-detect";
import { LuCheck, LuExternalLink, LuX } from "react-icons/lu";
import { TbExclamationCircle } from "react-icons/tb";
import { Link } from "react-router-dom";
@ -32,7 +33,12 @@ export default function Explore() {
// grid
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

View File

@ -387,7 +387,7 @@ export default function SearchView({
key={value.id}
ref={(item) => (itemRefs.current[index] = item)}
data-start={value.start_time}
className="review-item relative rounded-lg"
className="review-item relative flex flex-col rounded-lg"
>
<div
className={cn(
@ -402,9 +402,10 @@ export default function SearchView({
<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"}`}
/>
<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
searchResult={value}
columns={columns}
findSimilar={() => {
if (config?.semantic_search.enabled) {
setSimilaritySearch(value);