incrementally load filtered cameras
fix visible camera card image loading refactoring update version 1.8
This commit is contained in:
parent
302f117b44
commit
ed65fd20f8
@ -1,6 +1,6 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
# Build commands:
|
||||
# - $VERSION=1.7
|
||||
# - $VERSION=1.8
|
||||
# - rm build -r -Force ; rm ./node_modules/.cache/babel-loader -r -Force ; yarn build
|
||||
# - docker build --pull --rm -t oncharterliz/multi-frigate:latest -t oncharterliz/multi-frigate:$VERSION "."
|
||||
# - docker image push oncharterliz/multi-frigate:$VERSION ; docker image push oncharterliz/multi-frigate:latest
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "multi-frigate",
|
||||
"version": "0.1.7",
|
||||
"version": "0.1.8",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@cycjimmy/jsmpeg-player": "^6.0.5",
|
||||
@ -48,6 +48,7 @@
|
||||
"react-device-detect": "^2.2.3",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^14.1.0",
|
||||
"react-intersection-observer": "^9.13.1",
|
||||
"react-konva": "^18.2.10",
|
||||
"react-router-dom": "^6.14.1",
|
||||
"react-scripts": "5.0.1",
|
||||
|
||||
52
src/hooks/useOnWhenVisible.ts
Normal file
52
src/hooks/useOnWhenVisible.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
const hasIntersectionObserver: boolean =
|
||||
"IntersectionObserver" in window &&
|
||||
"IntersectionObserverEntry" in window &&
|
||||
"isIntersecting" in window.IntersectionObserverEntry.prototype;
|
||||
|
||||
interface UseOnWhenVisibleOptions {
|
||||
alwaysVisible?: boolean;
|
||||
threshold?: number | number[];
|
||||
}
|
||||
|
||||
export function useOnWhenVisible({
|
||||
alwaysVisible = false,
|
||||
threshold = 0.1,
|
||||
}: UseOnWhenVisibleOptions): [React.RefCallback<HTMLDivElement>, boolean] {
|
||||
const [ref, setRef] = useState<HTMLDivElement | null>(null);
|
||||
const [isVisible, setIsVisible] = useState<boolean>(
|
||||
alwaysVisible || !hasIntersectionObserver
|
||||
);
|
||||
|
||||
if (!hasIntersectionObserver) {
|
||||
console.warn("IntersectionObserver is not supported in this browser.");
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (ref == null || isVisible || !hasIntersectionObserver) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
const observer = new IntersectionObserver(([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setIsVisible(true);
|
||||
}
|
||||
}, { threshold });
|
||||
|
||||
observer.observe(ref);
|
||||
const rect = ref.getBoundingClientRect();
|
||||
const isCurrentlyVisible =
|
||||
rect.top < window.innerHeight && rect.bottom > 0;
|
||||
if (isCurrentlyVisible) {
|
||||
setIsVisible(true);
|
||||
}
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [ref, isVisible, threshold]);
|
||||
|
||||
return [setRef, isVisible];
|
||||
}
|
||||
@ -13,10 +13,11 @@ import { GetCameraWHostWConfig } from '../services/frigate.proxy/frigate.schema'
|
||||
import ClearableTextInput from '../shared/components/inputs/ClearableTextInput';
|
||||
import CenterLoader from '../shared/components/loaders/CenterLoader';
|
||||
import { isProduction } from '../shared/env.const';
|
||||
import CameraCard from '../widgets/CameraCard';
|
||||
import MainFiltersRightSide from '../widgets/sidebars/MainFiltersRightSide';
|
||||
import { SideBarContext } from '../widgets/sidebars/SideBarContext';
|
||||
import RetryErrorPage from './RetryErrorPage';
|
||||
import CameraCard from '../widgets/card/CameraCard';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
|
||||
export const mainPageParams = {
|
||||
hostId: 'hostId',
|
||||
@ -32,7 +33,10 @@ const MainPage = () => {
|
||||
|
||||
const { setRightChildren } = useContext(SideBarContext)
|
||||
const { hostId: selectedHostId, selectedTags, searchQuery } = mainStore.filters
|
||||
const [filteredCameras, setFilteredCameras] = useState<GetCameraWHostWConfig[]>()
|
||||
const [filteredCameras, setFilteredCameras] = useState<GetCameraWHostWConfig[]>([])
|
||||
|
||||
const { ref, inView } = useInView({ threshold: 0.5 })
|
||||
const [visibleCameras, setVisibleCameras] = useState<GetCameraWHostWConfig[]>([])
|
||||
|
||||
const realmUser = useRealmUser()
|
||||
if (!isProduction) console.log('Realmuser:', realmUser)
|
||||
@ -42,6 +46,23 @@ const MainPage = () => {
|
||||
queryFn: frigateApi.getCamerasWHost
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const pageSize = 20;
|
||||
if (inView && filteredCameras.length > visibleCameras.length) {
|
||||
const nextBatch = filteredCameras.slice(visibleCameras.length, visibleCameras.length + pageSize);
|
||||
setVisibleCameras(prev => [...prev, ...nextBatch]);
|
||||
}
|
||||
}, [inView, filteredCameras, visibleCameras]);
|
||||
|
||||
useEffect(() => {
|
||||
const hostId = searchParams.get(mainPageParams.hostId) || ''
|
||||
const searchQuery = searchParams.get(mainPageParams.searchQuery) || ''
|
||||
const selectedTags = mainStore.getArrayParam(mainPageParams.selectedTags)
|
||||
mainStore.setHostId(hostId, navigate)
|
||||
mainStore.setSearchQuery(searchQuery, navigate)
|
||||
mainStore.setSelectedTags(selectedTags, navigate)
|
||||
}, [searchParams])
|
||||
|
||||
useEffect(() => {
|
||||
const deSerializedTags = mainStore.getArrayParam(mainPageParams.selectedTags)
|
||||
mainStore.loadFiltersFromPage({
|
||||
@ -56,7 +77,7 @@ const MainPage = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!cameras) {
|
||||
setFilteredCameras(undefined)
|
||||
setFilteredCameras([])
|
||||
return
|
||||
}
|
||||
|
||||
@ -68,32 +89,33 @@ const MainPage = () => {
|
||||
}
|
||||
|
||||
setFilteredCameras(cameras.filter(filterCameras))
|
||||
setVisibleCameras([])
|
||||
}, [searchQuery, cameras, selectedHostId, selectedTags])
|
||||
|
||||
|
||||
const cards = useMemo(() => {
|
||||
if (filteredCameras)
|
||||
return filteredCameras.filter(camera => {
|
||||
if (camera.frigateHost && !camera.frigateHost.enabled) return false
|
||||
return true
|
||||
}).map(camera => (
|
||||
<CameraCard
|
||||
key={camera.id}
|
||||
camera={camera}
|
||||
/>)
|
||||
)
|
||||
else if (cameras)
|
||||
return cameras.filter(camera => {
|
||||
if (camera.frigateHost && !camera.frigateHost.enabled) return false
|
||||
return true
|
||||
}).map(camera => (
|
||||
<CameraCard
|
||||
key={camera.id}
|
||||
camera={camera}
|
||||
/>)
|
||||
)
|
||||
else return []
|
||||
}, [cameras, filteredCameras])
|
||||
// const cards = useMemo(() => {
|
||||
// if (filteredCameras)
|
||||
// return filteredCameras.filter(camera => {
|
||||
// if (camera.frigateHost && !camera.frigateHost.enabled) return false
|
||||
// return true
|
||||
// }).map(camera => (
|
||||
// <CameraCard
|
||||
// key={camera.id}
|
||||
// camera={camera}
|
||||
// />)
|
||||
// ).slice(0,5)
|
||||
// else if (cameras)
|
||||
// return cameras.filter(camera => {
|
||||
// if (camera.frigateHost && !camera.frigateHost.enabled) return false
|
||||
// return true
|
||||
// }).map(camera => (
|
||||
// <CameraCard
|
||||
// key={camera.id}
|
||||
// camera={camera}
|
||||
// />)
|
||||
// ).slice(0,5)
|
||||
// else return []
|
||||
// }, [cameras, filteredCameras])
|
||||
|
||||
const debouncedHandleSearchQuery = useDebounce((value: string) => {
|
||||
mainStore.setSearchQuery(value, navigate);
|
||||
@ -126,8 +148,11 @@ const MainPage = () => {
|
||||
</Flex>
|
||||
<Flex justify='center' h='100%' direction='column' w='100%' >
|
||||
<Grid mt='sm' justify="center" mb='sm' align='stretch'>
|
||||
{cards}
|
||||
{visibleCameras.map(camera => (
|
||||
<CameraCard key={camera.id} camera={camera} />
|
||||
))}
|
||||
</Grid>
|
||||
<div ref={ref} style={{ height: '50px' }} /> {/* trigger point */}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@ -26,7 +26,7 @@ export class MainStore {
|
||||
const params = new URLSearchParams();
|
||||
if (this.filters.hostId) params.set(mainPageParams.hostId, this.filters.hostId);
|
||||
if (this.filters.searchQuery) params.set(mainPageParams.searchQuery, this.filters.searchQuery);
|
||||
if (this.filters.selectedTags) {
|
||||
if (this.filters.selectedTags && this.filters.selectedTags.length > 0) {
|
||||
const serializedTags = this.filters.selectedTags.join(",")
|
||||
params.set(mainPageParams.selectedTags, serializedTags);
|
||||
}
|
||||
|
||||
@ -1,16 +1,15 @@
|
||||
import { Button, Card, Flex, Grid, Group, Text, createStyles } from '@mantine/core';
|
||||
import { useIntersection } from '@mantine/hooks';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAdminRole } from '../hooks/useAdminRole';
|
||||
import { recordingsPageQuery } from '../pages/RecordingsPage';
|
||||
import { routesPath } from '../router/routes.path';
|
||||
import { mapHostToHostname, proxyApi } from '../services/frigate.proxy/frigate.api';
|
||||
import { GetCameraWHostWConfig } from '../services/frigate.proxy/frigate.schema';
|
||||
import AutoUpdatedImage from '../shared/components/images/AutoUpdatedImage';
|
||||
import CameraTagsList from './CameraTagsList';
|
||||
import { eventsQueryParams } from '../pages/EventsPage';
|
||||
import { useAdminRole } from '../../hooks/useAdminRole';
|
||||
import { useOnWhenVisible } from '../../hooks/useOnWhenVisible';
|
||||
import { eventsQueryParams } from '../../pages/EventsPage';
|
||||
import { recordingsPageQuery } from '../../pages/RecordingsPage';
|
||||
import { routesPath } from '../../router/routes.path';
|
||||
import { mapHostToHostname, proxyApi } from '../../services/frigate.proxy/frigate.api';
|
||||
import { GetCameraWHostWConfig } from '../../services/frigate.proxy/frigate.schema';
|
||||
import AutoUpdatedImage from '../../shared/components/images/AutoUpdatedImage';
|
||||
import CameraTagsList from '../CameraTagsList';
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
mainCard: {
|
||||
@ -45,18 +44,13 @@ const CameraCard = ({
|
||||
camera
|
||||
}: CameraCardProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [renderImage, setRenderImage] = useState<boolean>(false)
|
||||
const { classes } = useStyles();
|
||||
const { ref, entry } = useIntersection({ threshold: 0.5, })
|
||||
const [ref, isVisible] = useOnWhenVisible({ threshold: 0.5 })
|
||||
const navigate = useNavigate()
|
||||
const hostName = mapHostToHostname(camera.frigateHost)
|
||||
const imageUrl = hostName ? proxyApi.cameraImageURL(hostName, camera.name) : '' //todo implement get URL from live cameras
|
||||
const { isAdmin } = useAdminRole()
|
||||
|
||||
useEffect(() => {
|
||||
if (entry && entry.isIntersecting)
|
||||
setRenderImage(true)
|
||||
}, [entry?.isIntersecting])
|
||||
|
||||
const handleOpenLiveView = () => {
|
||||
const url = routesPath.LIVE_PATH.replace(':id', camera.id)
|
||||
@ -82,7 +76,7 @@ const CameraCard = ({
|
||||
<Grid.Col md={6} lg={3} p='0.2rem'>
|
||||
<Card ref={ref} h='100%' radius="lg" padding='0.5rem' className={classes.mainCard}>
|
||||
<Text align='center' size='md' className={classes.headText} >{camera.name} / {camera.frigateHost?.name}</Text>
|
||||
{!renderImage ? null :
|
||||
{!isVisible ? null :
|
||||
<AutoUpdatedImage onClick={handleOpenLiveView} enabled={camera.config?.enabled} imageUrl={imageUrl} />
|
||||
}
|
||||
<Group
|
||||
@ -8734,6 +8734,11 @@ react-i18next@^14.1.0:
|
||||
"@babel/runtime" "^7.23.9"
|
||||
html-parse-stringify "^3.0.1"
|
||||
|
||||
react-intersection-observer@^9.13.1:
|
||||
version "9.13.1"
|
||||
resolved "https://registry.yarnpkg.com/react-intersection-observer/-/react-intersection-observer-9.13.1.tgz#6c61a75801162491c6348bad09967f2caf445584"
|
||||
integrity sha512-tSzDaTy0qwNPLJHg8XZhlyHTgGW6drFKTtvjdL+p6um12rcnp8Z5XstE+QNBJ7c64n5o0Lj4ilUleA41bmDoMw==
|
||||
|
||||
react-is@^16.13.1, react-is@^16.7.0:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user