mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-04-11 17:47:37 +03:00
lazy loading
This commit is contained in:
parent
9924b89d2b
commit
074af52b81
@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import {
|
import {
|
||||||
@ -15,11 +15,13 @@ import { cn } from "@/lib/utils";
|
|||||||
import { Event } from "@/types/event";
|
import { Event } from "@/types/event";
|
||||||
import { useApiHost } from "@/api";
|
import { useApiHost } from "@/api";
|
||||||
import { isDesktop, isMobile } from "react-device-detect";
|
import { isDesktop, isMobile } from "react-device-detect";
|
||||||
|
import ActivityIndicator from "../indicators/activity-indicator";
|
||||||
|
|
||||||
type ImagePickerProps = {
|
type ImagePickerProps = {
|
||||||
selectedImageId?: string;
|
selectedImageId?: string;
|
||||||
setSelectedImageId?: (id: string) => void;
|
setSelectedImageId?: (id: string) => void;
|
||||||
camera: string;
|
camera: string;
|
||||||
|
limit?: number;
|
||||||
direct?: boolean;
|
direct?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
@ -28,6 +30,7 @@ export default function ImagePicker({
|
|||||||
selectedImageId,
|
selectedImageId,
|
||||||
setSelectedImageId,
|
setSelectedImageId,
|
||||||
camera,
|
camera,
|
||||||
|
limit = 100,
|
||||||
direct = false,
|
direct = false,
|
||||||
className,
|
className,
|
||||||
}: ImagePickerProps) {
|
}: ImagePickerProps) {
|
||||||
@ -35,9 +38,10 @@ export default function ImagePicker({
|
|||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [loadedImages, setLoadedImages] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
const { data: events } = useSWR<Event[]>(
|
const { data: events } = useSWR<Event[]>(
|
||||||
`events?camera=${camera}&limit=100`,
|
`events?camera=${camera}&limit=${limit}`,
|
||||||
{
|
{
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
},
|
},
|
||||||
@ -74,6 +78,10 @@ export default function ImagePicker({
|
|||||||
[setSelectedImageId, direct],
|
[setSelectedImageId, direct],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleImageLoad = useCallback((imageId: string) => {
|
||||||
|
setLoadedImages((prev) => new Set(prev).add(imageId));
|
||||||
|
}, []);
|
||||||
|
|
||||||
const renderSearchInput = () => (
|
const renderSearchInput = () => (
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
@ -95,7 +103,7 @@ export default function ImagePicker({
|
|||||||
<div
|
<div
|
||||||
key={image.id}
|
key={image.id}
|
||||||
className={cn(
|
className={cn(
|
||||||
"aspect-square cursor-pointer overflow-hidden rounded-lg border-2 bg-background transition-all",
|
"relative aspect-square cursor-pointer overflow-hidden rounded-lg border-2 bg-background transition-all",
|
||||||
selectedImageId === image.id &&
|
selectedImageId === image.id &&
|
||||||
"border-selected ring-2 ring-selected",
|
"border-selected ring-2 ring-selected",
|
||||||
)}
|
)}
|
||||||
@ -105,13 +113,25 @@ export default function ImagePicker({
|
|||||||
alt={image.label}
|
alt={image.label}
|
||||||
className="h-full w-full object-cover"
|
className="h-full w-full object-cover"
|
||||||
onClick={() => handleImageSelect(image.id)}
|
onClick={() => handleImageSelect(image.id)}
|
||||||
|
onLoad={() => handleImageLoad(image.id)}
|
||||||
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
|
{!loadedImages.has(image.id) && (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<ActivityIndicator />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Reset loaded images when images change
|
||||||
|
useEffect(() => {
|
||||||
|
setLoadedImages(new Set());
|
||||||
|
}, [images]);
|
||||||
|
|
||||||
if (direct) {
|
if (direct) {
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef} className={className}>
|
<div ref={containerRef} className={className}>
|
||||||
@ -141,6 +161,7 @@ export default function ImagePicker({
|
|||||||
<div className="hover:cursor-pointer">
|
<div className="hover:cursor-pointer">
|
||||||
<div className="my-3 flex w-full flex-row items-center justify-between gap-2">
|
<div className="my-3 flex w-full flex-row items-center justify-between gap-2">
|
||||||
<div className="flex flex-row items-center gap-2">
|
<div className="flex flex-row items-center gap-2">
|
||||||
|
<div className="relative h-8 w-8">
|
||||||
<img
|
<img
|
||||||
src={
|
src={
|
||||||
selectedImage
|
selectedImage
|
||||||
@ -149,7 +170,15 @@ export default function ImagePicker({
|
|||||||
}
|
}
|
||||||
alt={selectedImage?.label || "Selected image"}
|
alt={selectedImage?.label || "Selected image"}
|
||||||
className="h-8 w-8 rounded object-cover"
|
className="h-8 w-8 rounded object-cover"
|
||||||
|
onLoad={() => handleImageLoad(selectedImageId || "")}
|
||||||
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
|
{selectedImageId && !loadedImages.has(selectedImageId) && (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<ActivityIndicator />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className="text-sm smart-capitalize">
|
<div className="text-sm smart-capitalize">
|
||||||
{selectedImage?.label || selectedImageId}
|
{selectedImage?.label || selectedImageId}
|
||||||
{selectedImage?.sub_label
|
{selectedImage?.sub_label
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user