mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-09-26 19:41:29 +08:00
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:
@@ -2,7 +2,7 @@ import ActivityIndicator from "../indicators/activity-indicator";
|
|||||||
import { LuTrash } from "react-icons/lu";
|
import { LuTrash } from "react-icons/lu";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { isDesktop } from "react-device-detect";
|
import { isDesktop, isMobile } from "react-device-detect";
|
||||||
import { FaDownload, FaPlay, FaShareAlt } from "react-icons/fa";
|
import { FaDownload, FaPlay, FaShareAlt } from "react-icons/fa";
|
||||||
import Chip from "../indicators/Chip";
|
import Chip from "../indicators/Chip";
|
||||||
import { Skeleton } from "../ui/skeleton";
|
import { Skeleton } from "../ui/skeleton";
|
||||||
@@ -82,7 +82,13 @@ export default function ExportCard({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogContent>
|
<DialogContent
|
||||||
|
onOpenAutoFocus={(e) => {
|
||||||
|
if (isMobile) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
<DialogTitle>Rename Export</DialogTitle>
|
<DialogTitle>Rename Export</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Enter a new name for this export.
|
Enter a new name for this export.
|
||||||
@@ -90,7 +96,7 @@ export default function ExportCard({
|
|||||||
{editName && (
|
{editName && (
|
||||||
<>
|
<>
|
||||||
<Input
|
<Input
|
||||||
className="mt-3"
|
className="text-md mt-3"
|
||||||
type="search"
|
type="search"
|
||||||
placeholder={editName?.original}
|
placeholder={editName?.original}
|
||||||
value={
|
value={
|
||||||
|
@@ -710,7 +710,7 @@ export function CameraGroupEdit({
|
|||||||
<FormLabel>Name</FormLabel>
|
<FormLabel>Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
className="w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
|
className="text-md w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
|
||||||
placeholder="Enter a name..."
|
placeholder="Enter a name..."
|
||||||
disabled={editingGroup !== undefined}
|
disabled={editingGroup !== undefined}
|
||||||
{...field}
|
{...field}
|
||||||
|
@@ -74,7 +74,7 @@ export default function CreateUserDialog({
|
|||||||
<FormLabel>User</FormLabel>
|
<FormLabel>User</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
className="w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
|
className="text-md w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@@ -89,7 +89,7 @@ export default function CreateUserDialog({
|
|||||||
<FormLabel>Password</FormLabel>
|
<FormLabel>Password</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
className="w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
|
className="text-md w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
|
||||||
type="password"
|
type="password"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
|
@@ -273,7 +273,7 @@ export function ExportContent({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Input
|
<Input
|
||||||
className="my-6"
|
className="text-md my-6"
|
||||||
type="search"
|
type="search"
|
||||||
placeholder="Name the Export"
|
placeholder="Name the Export"
|
||||||
value={name}
|
value={name}
|
||||||
@@ -431,7 +431,7 @@ function CustomTimeSelector({
|
|||||||
/>
|
/>
|
||||||
<SelectSeparator className="bg-secondary" />
|
<SelectSeparator className="bg-secondary" />
|
||||||
<input
|
<input
|
||||||
className="mx-4 w-full border border-input bg-background p-1 text-secondary-foreground hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
|
className="text-md mx-4 w-full border border-input bg-background p-1 text-secondary-foreground hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
|
||||||
id="startTime"
|
id="startTime"
|
||||||
type="time"
|
type="time"
|
||||||
value={startClock}
|
value={startClock}
|
||||||
@@ -493,7 +493,7 @@ function CustomTimeSelector({
|
|||||||
/>
|
/>
|
||||||
<SelectSeparator className="bg-secondary" />
|
<SelectSeparator className="bg-secondary" />
|
||||||
<input
|
<input
|
||||||
className="mx-4 w-full border border-input bg-background p-1 text-secondary-foreground hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
|
className="text-md mx-4 w-full border border-input bg-background p-1 text-secondary-foreground hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
|
||||||
id="startTime"
|
id="startTime"
|
||||||
type="time"
|
type="time"
|
||||||
value={endClock}
|
value={endClock}
|
||||||
|
@@ -23,12 +23,12 @@ export default function SetPasswordDialog({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={show} onOpenChange={onCancel}>
|
<Dialog open={show} onOpenChange={onCancel}>
|
||||||
<DialogContent>
|
<DialogContent onOpenAutoFocus={(e) => e.preventDefault()}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Set Password</DialogTitle>
|
<DialogTitle>Set Password</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<Input
|
<Input
|
||||||
className="w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
|
className="text-md w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
|
||||||
type="password"
|
type="password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(event) => setPassword(event.target.value)}
|
onChange={(event) => setPassword(event.target.value)}
|
||||||
|
@@ -178,7 +178,7 @@ export function AnnotationSettingsPane({
|
|||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
className="w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
|
className="text-md w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
|
@@ -373,7 +373,7 @@ export default function ZoneEditPane({
|
|||||||
<FormLabel>Name</FormLabel>
|
<FormLabel>Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
className="w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
|
className="text-md w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
|
||||||
placeholder="Enter a name..."
|
placeholder="Enter a name..."
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
@@ -395,7 +395,7 @@ export default function ZoneEditPane({
|
|||||||
<FormLabel>Inertia</FormLabel>
|
<FormLabel>Inertia</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
className="w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
|
className="text-md w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
|
||||||
placeholder="3"
|
placeholder="3"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
@@ -417,7 +417,7 @@ export default function ZoneEditPane({
|
|||||||
<FormLabel>Loitering Time</FormLabel>
|
<FormLabel>Loitering Time</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
className="w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
|
className="text-md w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
|
@@ -175,7 +175,7 @@ function Exports() {
|
|||||||
{exports && (
|
{exports && (
|
||||||
<div className="flex w-full items-center justify-center p-2">
|
<div className="flex w-full items-center justify-center p-2">
|
||||||
<Input
|
<Input
|
||||||
className="w-full bg-muted md:w-1/3"
|
className="text-md w-full bg-muted md:w-1/3"
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
@@ -17,7 +17,7 @@ import useImageLoaded from "@/hooks/use-image-loaded";
|
|||||||
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||||
|
|
||||||
type ExploreViewProps = {
|
type ExploreViewProps = {
|
||||||
onSelectSearch: (searchResult: SearchResult) => void;
|
onSelectSearch: (searchResult: SearchResult, index: number) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ExploreView({ onSelectSearch }: ExploreViewProps) {
|
export default function ExploreView({ onSelectSearch }: ExploreViewProps) {
|
||||||
@@ -76,7 +76,7 @@ export default function ExploreView({ onSelectSearch }: ExploreViewProps) {
|
|||||||
type ThumbnailRowType = {
|
type ThumbnailRowType = {
|
||||||
objectType: string;
|
objectType: string;
|
||||||
searchResults?: SearchResult[];
|
searchResults?: SearchResult[];
|
||||||
onSelectSearch: (searchResult: SearchResult) => void;
|
onSelectSearch: (searchResult: SearchResult, index: number) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
function ThumbnailRow({
|
function ThumbnailRow({
|
||||||
@@ -145,7 +145,7 @@ function ThumbnailRow({
|
|||||||
|
|
||||||
type ExploreThumbnailImageProps = {
|
type ExploreThumbnailImageProps = {
|
||||||
event: SearchResult;
|
event: SearchResult;
|
||||||
onSelectSearch: (searchResult: SearchResult) => void;
|
onSelectSearch: (searchResult: SearchResult, index: number) => void;
|
||||||
};
|
};
|
||||||
function ExploreThumbnailImage({
|
function ExploreThumbnailImage({
|
||||||
event,
|
event,
|
||||||
@@ -176,7 +176,7 @@ function ExploreThumbnailImage({
|
|||||||
loading={isSafari ? "eager" : "lazy"}
|
loading={isSafari ? "eager" : "lazy"}
|
||||||
draggable={false}
|
draggable={false}
|
||||||
src={`${apiHost}api/events/${event.id}/thumbnail.jpg`}
|
src={`${apiHost}api/events/${event.id}/thumbnail.jpg`}
|
||||||
onClick={() => onSelectSearch(event)}
|
onClick={() => onSelectSearch(event, 0)}
|
||||||
onLoad={() => {
|
onLoad={() => {
|
||||||
onImgLoad();
|
onImgLoad();
|
||||||
}}
|
}}
|
||||||
|
@@ -13,11 +13,15 @@ import {
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
import { SearchFilter, SearchResult } from "@/types/search";
|
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 { isMobileOnly } from "react-device-detect";
|
||||||
import { LuImage, LuSearchX, LuText, LuXCircle } from "react-icons/lu";
|
import { LuImage, LuSearchX, LuText, LuXCircle } from "react-icons/lu";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import ExploreView from "../explore/ExploreView";
|
import ExploreView from "../explore/ExploreView";
|
||||||
|
import useKeyboardListener, {
|
||||||
|
KeyModifiers,
|
||||||
|
} from "@/hooks/use-keyboard-listener";
|
||||||
|
import scrollIntoView from "scroll-into-view-if-needed";
|
||||||
|
|
||||||
type SearchViewProps = {
|
type SearchViewProps = {
|
||||||
search: string;
|
search: string;
|
||||||
@@ -59,8 +63,12 @@ export default function SearchView({
|
|||||||
|
|
||||||
// search interaction
|
// 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);
|
setSearchDetail(item);
|
||||||
|
setSelectedIndex(index);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// confidence score - probably needs tweaking
|
// confidence score - probably needs tweaking
|
||||||
@@ -87,6 +95,56 @@ export default function SearchView({
|
|||||||
[searchResults, searchFilter],
|
[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 (
|
return (
|
||||||
<div className="flex size-full flex-col pt-2 md:py-2">
|
<div className="flex size-full flex-col pt-2 md:py-2">
|
||||||
<Toaster closeButton={true} />
|
<Toaster closeButton={true} />
|
||||||
@@ -156,12 +214,13 @@ export default function SearchView({
|
|||||||
{uniqueResults && (
|
{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">
|
<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 &&
|
||||||
uniqueResults.map((value) => {
|
uniqueResults.map((value, index) => {
|
||||||
const selected = false;
|
const selected = selectedIndex === index;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={value.id}
|
key={value.id}
|
||||||
|
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 rounded-lg"
|
||||||
>
|
>
|
||||||
@@ -173,7 +232,7 @@ export default function SearchView({
|
|||||||
<SearchThumbnail
|
<SearchThumbnail
|
||||||
searchResult={value}
|
searchResult={value}
|
||||||
findSimilar={() => setSimilaritySearch(value)}
|
findSimilar={() => setSimilaritySearch(value)}
|
||||||
onClick={() => onSelectSearch(value)}
|
onClick={() => onSelectSearch(value, index)}
|
||||||
/>
|
/>
|
||||||
{searchTerm && (
|
{searchTerm && (
|
||||||
<div className={cn("absolute right-2 top-2 z-40")}>
|
<div className={cn("absolute right-2 top-2 z-40")}>
|
||||||
@@ -207,7 +266,7 @@ export default function SearchView({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<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>
|
</div>
|
||||||
);
|
);
|
||||||
|
@@ -254,7 +254,7 @@ export default function NotificationView({
|
|||||||
<FormLabel>Email</FormLabel>
|
<FormLabel>Email</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<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"
|
placeholder="example@email.com"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
|
Reference in New Issue
Block a user