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 { 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>
</> </>
); );

View File

@ -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>
); );

View File

@ -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

View File

@ -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);