forked from Github/frigate
UI tweaks (#13711)
* Fix dialog autofocus problems on mobile * set font size to prevent mobile zooming * Use arrow keys to navigate Explore view
This commit is contained in:
@@ -17,7 +17,7 @@ import useImageLoaded from "@/hooks/use-image-loaded";
|
||||
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||
|
||||
type ExploreViewProps = {
|
||||
onSelectSearch: (searchResult: SearchResult) => void;
|
||||
onSelectSearch: (searchResult: SearchResult, index: number) => void;
|
||||
};
|
||||
|
||||
export default function ExploreView({ onSelectSearch }: ExploreViewProps) {
|
||||
@@ -76,7 +76,7 @@ export default function ExploreView({ onSelectSearch }: ExploreViewProps) {
|
||||
type ThumbnailRowType = {
|
||||
objectType: string;
|
||||
searchResults?: SearchResult[];
|
||||
onSelectSearch: (searchResult: SearchResult) => void;
|
||||
onSelectSearch: (searchResult: SearchResult, index: number) => void;
|
||||
};
|
||||
|
||||
function ThumbnailRow({
|
||||
@@ -145,7 +145,7 @@ function ThumbnailRow({
|
||||
|
||||
type ExploreThumbnailImageProps = {
|
||||
event: SearchResult;
|
||||
onSelectSearch: (searchResult: SearchResult) => void;
|
||||
onSelectSearch: (searchResult: SearchResult, index: number) => void;
|
||||
};
|
||||
function ExploreThumbnailImage({
|
||||
event,
|
||||
@@ -176,7 +176,7 @@ function ExploreThumbnailImage({
|
||||
loading={isSafari ? "eager" : "lazy"}
|
||||
draggable={false}
|
||||
src={`${apiHost}api/events/${event.id}/thumbnail.jpg`}
|
||||
onClick={() => onSelectSearch(event)}
|
||||
onClick={() => onSelectSearch(event, 0)}
|
||||
onLoad={() => {
|
||||
onImgLoad();
|
||||
}}
|
||||
|
||||
@@ -13,11 +13,15 @@ import {
|
||||
import { cn } from "@/lib/utils";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import { SearchFilter, SearchResult } from "@/types/search";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { isMobileOnly } from "react-device-detect";
|
||||
import { LuImage, LuSearchX, LuText, LuXCircle } from "react-icons/lu";
|
||||
import useSWR from "swr";
|
||||
import ExploreView from "../explore/ExploreView";
|
||||
import useKeyboardListener, {
|
||||
KeyModifiers,
|
||||
} from "@/hooks/use-keyboard-listener";
|
||||
import scrollIntoView from "scroll-into-view-if-needed";
|
||||
|
||||
type SearchViewProps = {
|
||||
search: string;
|
||||
@@ -59,8 +63,12 @@ export default function SearchView({
|
||||
|
||||
// search interaction
|
||||
|
||||
const onSelectSearch = useCallback((item: SearchResult) => {
|
||||
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
|
||||
const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||
|
||||
const onSelectSearch = useCallback((item: SearchResult, index: number) => {
|
||||
setSearchDetail(item);
|
||||
setSelectedIndex(index);
|
||||
}, []);
|
||||
|
||||
// confidence score - probably needs tweaking
|
||||
@@ -87,6 +95,56 @@ export default function SearchView({
|
||||
[searchResults, searchFilter],
|
||||
);
|
||||
|
||||
// keyboard listener
|
||||
|
||||
const onKeyboardShortcut = useCallback(
|
||||
(key: string | null, modifiers: KeyModifiers) => {
|
||||
if (!modifiers.down || !uniqueResults) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case "ArrowLeft":
|
||||
setSelectedIndex((prevIndex) => {
|
||||
const newIndex =
|
||||
prevIndex === null
|
||||
? uniqueResults.length - 1
|
||||
: (prevIndex - 1 + uniqueResults.length) % uniqueResults.length;
|
||||
setSearchDetail(uniqueResults[newIndex]);
|
||||
return newIndex;
|
||||
});
|
||||
break;
|
||||
case "ArrowRight":
|
||||
setSelectedIndex((prevIndex) => {
|
||||
const newIndex =
|
||||
prevIndex === null ? 0 : (prevIndex + 1) % uniqueResults.length;
|
||||
setSearchDetail(uniqueResults[newIndex]);
|
||||
return newIndex;
|
||||
});
|
||||
break;
|
||||
}
|
||||
},
|
||||
[uniqueResults],
|
||||
);
|
||||
|
||||
useKeyboardListener(["ArrowLeft", "ArrowRight"], onKeyboardShortcut);
|
||||
|
||||
// scroll into view
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
selectedIndex !== null &&
|
||||
uniqueResults &&
|
||||
itemRefs.current?.[selectedIndex]
|
||||
) {
|
||||
scrollIntoView(itemRefs.current[selectedIndex], {
|
||||
block: "center",
|
||||
behavior: "smooth",
|
||||
scrollMode: "if-needed",
|
||||
});
|
||||
}
|
||||
}, [selectedIndex, uniqueResults]);
|
||||
|
||||
return (
|
||||
<div className="flex size-full flex-col pt-2 md:py-2">
|
||||
<Toaster closeButton={true} />
|
||||
@@ -156,12 +214,13 @@ export default function SearchView({
|
||||
{uniqueResults && (
|
||||
<div className="mt-2 grid w-full gap-2 px-1 sm:grid-cols-2 md:mx-2 md:grid-cols-4 md:gap-4 3xl:grid-cols-6">
|
||||
{uniqueResults &&
|
||||
uniqueResults.map((value) => {
|
||||
const selected = false;
|
||||
uniqueResults.map((value, index) => {
|
||||
const selected = selectedIndex === index;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={value.id}
|
||||
ref={(item) => (itemRefs.current[index] = item)}
|
||||
data-start={value.start_time}
|
||||
className="review-item relative rounded-lg"
|
||||
>
|
||||
@@ -173,7 +232,7 @@ export default function SearchView({
|
||||
<SearchThumbnail
|
||||
searchResult={value}
|
||||
findSimilar={() => setSimilaritySearch(value)}
|
||||
onClick={() => onSelectSearch(value)}
|
||||
onClick={() => onSelectSearch(value, index)}
|
||||
/>
|
||||
{searchTerm && (
|
||||
<div className={cn("absolute right-2 top-2 z-40")}>
|
||||
@@ -207,7 +266,7 @@ 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-severity_alert outline-severity_alert` : "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>
|
||||
);
|
||||
|
||||
@@ -254,7 +254,7 @@ export default function NotificationView({
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
className="w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark] md:w-72"
|
||||
className="text-md w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark] md:w-72"
|
||||
placeholder="example@email.com"
|
||||
{...field}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user