fix lint warns

add env to index
create env script
create docker
This commit is contained in:
NlightN22 2024-03-01 03:21:13 +07:00
parent d59e6c50e0
commit 811bc5bac7
38 changed files with 2477 additions and 1982 deletions

5
.env.docker Normal file
View File

@ -0,0 +1,5 @@
REACT_APP_HOST=localhost
REACT_APP_PORT=5173
REACT_APP_FRIGATE_PROXY=http://localhost:4000
REACT_APP_OPENID_SERVER=https://your.server.com:443/realms/your-realm
REACT_APP_CLIENT_ID=frontend-client

8
.gitignore vendored
View File

@ -13,13 +13,9 @@
/build /build
# misc # misc
.env .env*
.env.development !.env.docker
.DS_Store .DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
.idea/* .idea/*
.vscode .vscode

View File

@ -1,19 +1,26 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
# Build commands: # Build commands:
# - rm dist -r -Force ; yarn build # - rm build -r -Force ; yarn build
# - $VERSION=0.1 # - $VERSION=0.1
# - docker build --pull --rm -t oncharterliz/frigate-proxy:latest -t oncharterliz/frigate-proxy:$VERSION "." # - docker build --pull --rm -t oncharterliz/multi-frigate:latest -t oncharterliz/multi-frigate:$VERSION "."
# - docker image push --all-tags oncharterliz/frigate-proxy # - docker image push --all-tags oncharterliz/multi-frigate
FROM node:18-alpine AS frigate-proxy FROM nginx:alpine AS multi-frigate
ENV NODE_ENV=production
WORKDIR /app WORKDIR /app
COPY ./build/ /usr/share/nginx/html/
# Nginx config
RUN rm -rf /etc/nginx/conf.d/*
COPY ./nginx/default.conf /etc/nginx/conf.d/
COPY package.json yarn.lock ./ # Default port exposure
EXPOSE 80
RUN yarn install --production # Copy enviornment script and vars
COPY env.sh .env.docker ./
# Add bash
RUN apk add --no-cache bash
# Make our shell script executable
RUN chmod +x env.sh
COPY ./dist ./dist # Start Nginx server
CMD ["/bin/bash", "-c", "/app/env.sh && nginx -g \"daemon off;\""]
CMD yarn prod
EXPOSE 4000

View File

@ -1,30 +1,22 @@
# Instruction # Instruction
Frontend for [Proxy Frigate](https://github.com/NlightN22/frigate-proxy)
- download
- go to download directory
- run `yarn` to install packages
- create file: `docker-compose.yml` - create file: `docker-compose.yml`
```yml ```yml
version: '3.0' version: '3.1'
services: services:
front: front:
image: nginx:alpine image: oncharterliz/multi-frigate:latest
volumes: environment:
- ./build/:/usr/share/nginx/html/ REACT_APP_HOST: localhost
- ./nginx/:/etc/nginx/conf.d/ REACT_APP_PORT: 5173
REACT_APP_FRIGATE_PROXY: http://localhost:4000
REACT_APP_OPENID_SERVER: https://server:port/realms/your-realm
REACT_APP_CLIENT_ID: frontend-client
ports: ports:
- 8080:80 # set your port here - 5173:80 # set your port here
```
- create file: `.env.production.local`
```bash
REACT_APP_HOST=localhost
REACT_APP_PORT=4000
REACT_APP_OPENID_SERVER=https://server:port/realms/your-realm
REACT_APP_CLIENT_ID=your-client
``` ```
- run: - run:
```bash ```bash
yarn build
docker compose up -d docker compose up -d
``` ```

31
env.ps1 Normal file
View File

@ -0,0 +1,31 @@
$envFileName = ".env.production.local"
$envOutputFile = "./public/env-config.js"
# Recreate config file
Remove-Item -Path $envOutputFile -Force
New-Item -Path $envOutputFile -ItemType File
# Add assignment
Add-Content -Path $envOutputFile -Value "window.env = {"
# Read each line in env file
foreach ($line in Get-Content -Path $envFileName) {
if ($line -match '=') {
$parts = $line -split '=', 2
$varname = $parts[0]
$varvalue = $parts[1]
# Read value of current variable if exists as Environment variable
$value = [System.Environment]::GetEnvironmentVariable($varname)
# Otherwise, use value from env file
if (-not $value) {
$value = $varvalue
}
# Append configuration property to JS file
$lineToAdd = " {0}: `"{1}`"," -f $varname, $value
Add-Content -Path $envOutputFile -Value $lineToAdd
}
}
Add-Content -Path $envOutputFile -Value "}"

32
env.sh Normal file
View File

@ -0,0 +1,32 @@
#!/bin/bash
EnvFileName=".env.docker"
EnvOutputFile="/usr/share/nginx/html/env-config.js"
# Recreate config file
rm -rf $EnvOutputFile
touch $EnvOutputFile
# Add assignment
echo "window.env = {" >> $EnvOutputFile
# Read each line in $EnvFileName file
# Each line represents key=value pairs
while read -r line || [[ -n "$line" ]];
do
# Split env variables by character `=`
if printf '%s\n' "$line" | grep -q -e '='; then
varname=$(printf '%s\n' "$line" | sed -e 's/=.*//')
varvalue=$(printf '%s\n' "$line" | sed -e 's/^[^=]*=//')
fi
# Read value of current variable if exists as Environment variable
value=$(printf '%s\n' "${!varname}")
# Otherwise use value from $EnvFileName file
[[ -z $value ]] && value=${varvalue}
# Append configuration property to JS file
echo " $varname: \"$value\"," >> $EnvOutputFile
done < $EnvFileName
echo "}" >> $EnvOutputFile

View File

@ -0,0 +1,13 @@
version: '3.0'
services:
front:
image: oncharterliz/multi-frigate:latest
environment:
REACT_APP_HOST: localhost
REACT_APP_PORT: 5173
REACT_APP_FRIGATE_PROXY: http://localhost:4000
REACT_APP_OPENID_SERVER: https://server:port/realms/your-realm
REACT_APP_CLIENT_ID: frontend-client
ports:
- 5173:80 # set your port here

7
public/env-config.js Normal file
View File

@ -0,0 +1,7 @@
window.env = {
REACT_APP_HOST: "localhost",
REACT_APP_PORT: "5173",
REACT_APP_FRIGATE_PROXY: "http://localhost:4000",
REACT_APP_OPENID_SERVER: "https://oauth.komponent-m.ru:8443/realms/frigate-realm",
REACT_APP_CLIENT_ID: "frontend-client",
}

View File

@ -25,6 +25,7 @@
Learn how to configure a non-root public URL by running `npm run build`. Learn how to configure a non-root public URL by running `npm run build`.
--> -->
<title>Multi Frigate</title> <title>Multi Frigate</title>
<script src="%PUBLIC_URL%/env-config.js"></script>
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>

View File

@ -6,6 +6,7 @@ import AppRouter from './router/AppRouter';
import { Context } from '.'; import { Context } from '.';
import SideBar from './shared/components/SideBar'; import SideBar from './shared/components/SideBar';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { isProduction } from './shared/env.const';
const AppBody = () => { const AppBody = () => {
@ -23,7 +24,7 @@ const AppBody = () => {
const theme = useMantineTheme(); const theme = useMantineTheme();
console.log("render Main") if (!isProduction) console.log("render Main")
return ( return (
<AppShell <AppShell
styles={{ styles={{

View File

@ -13,6 +13,7 @@ import { strings } from '../shared/strings/strings';
import { useAdminRole } from '../hooks/useAdminRole'; import { useAdminRole } from '../hooks/useAdminRole';
import Forbidden from './403'; import Forbidden from './403';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { isProduction } from '../shared/env.const';
const AccessSettings = () => { const AccessSettings = () => {
const executed = useRef(false) const executed = useRef(false)
@ -45,7 +46,7 @@ const AccessSettings = () => {
setRoleId(value) setRoleId(value)
} }
console.log('AccessSettings rendered') if (!isProduction) console.log('AccessSettings rendered')
return ( return (
<Flex w='100%' h='100%' direction='column'> <Flex w='100%' h='100%' direction='column'>
<Text align='center' size='xl'>{strings.pleaseSelectRole}</Text> <Text align='center' size='xl'>{strings.pleaseSelectRole}</Text>

View File

@ -12,6 +12,7 @@ import RetryErrorPage from './RetryErrorPage';
import { useAdminRole } from '../hooks/useAdminRole'; import { useAdminRole } from '../hooks/useAdminRole';
import Forbidden from './403'; import Forbidden from './403';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { isProduction } from '../shared/env.const';
const HostConfigPage = () => { const HostConfigPage = () => {
@ -84,7 +85,7 @@ const HostConfigPage = () => {
if (!editorRef.current) { if (!editorRef.current) {
return; return;
} }
console.log('save config', save_option) if (!isProduction) console.log('save config', save_option)
}, [editorRef]) }, [editorRef])
if (configPending || adminLoading) return <CenterLoader /> if (configPending || adminLoading) return <CenterLoader />

View File

@ -10,6 +10,7 @@ import SelectedHostList from '../widgets/SelectedHostList';
import { dateToQueryString, parseQueryDateToDate } from '../shared/utils/dateUtil'; import { dateToQueryString, parseQueryDateToDate } from '../shared/utils/dateUtil';
import SelectedDayList from '../widgets/SelectedDayList'; import SelectedDayList from '../widgets/SelectedDayList';
import CenterLoader from '../shared/components/loaders/CenterLoader'; import CenterLoader from '../shared/components/loaders/CenterLoader';
import { isProduction } from '../shared/env.const';
export const recordingsPageQuery = { export const recordingsPageQuery = {
@ -95,7 +96,7 @@ const RecordingsPage = () => {
navigate({ pathname: location.pathname, search: queryParams.toString() }); navigate({ pathname: location.pathname, search: queryParams.toString() });
}, [recStore.selectedRange, location.pathname, navigate, queryParams]) }, [recStore.selectedRange, location.pathname, navigate, queryParams])
console.log('RecordingsPage rendered') if (!isProduction) console.log('RecordingsPage rendered')
if (!firstRender) return <CenterLoader /> if (!firstRender) return <CenterLoader />

View File

@ -17,6 +17,7 @@ import { Context } from '..';
import { useAdminRole } from '../hooks/useAdminRole'; import { useAdminRole } from '../hooks/useAdminRole';
import Forbidden from './403'; import Forbidden from './403';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { isProduction } from '../shared/env.const';
const SettingsPage = () => { const SettingsPage = () => {
const executed = useRef(false) const executed = useRef(false)
@ -59,17 +60,17 @@ const SettingsPage = () => {
}) })
const handleDiscard = () => { const handleDiscard = () => {
console.log('Discard changes') if (!isProduction) console.log('Discard changes')
refetch() refetch()
setConfigs(data ? mapEncryptedToView(data) : []) setConfigs(data ? mapEncryptedToView(data) : [])
} }
useEffect(() => { useEffect(() => {
console.log('data changed') if (!isProduction) console.log('data changed')
setConfigs(mapEncryptedToView(data)) setConfigs(mapEncryptedToView(data))
}, [data]) }, [data])
useEffect(() => { useEffect(() => {
console.log('configs changed') if (!isProduction) console.log('configs changed')
}, [configs]) }, [configs])
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => { const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
@ -95,7 +96,7 @@ const SettingsPage = () => {
value: value, value: value,
} }
}); });
console.log('configsToUpdate', configsToUpdate) if (!isProduction) console.log('configsToUpdate', configsToUpdate)
mutation.mutate(configsToUpdate); mutation.mutate(configsToUpdate);
} }

View File

@ -6,6 +6,7 @@ import RetryError from './RetryError';
import { TransferList, Text, TransferListData, TransferListProps, TransferListItem, Button, Flex } from '@mantine/core'; import { TransferList, Text, TransferListData, TransferListProps, TransferListItem, Button, Flex } from '@mantine/core';
import { OneSelectItem } from './filters.aps/OneSelectFilter'; import { OneSelectItem } from './filters.aps/OneSelectFilter';
import { strings } from '../strings/strings'; import { strings } from '../strings/strings';
import { isProduction } from '../env.const';
interface CamerasTransferListProps { interface CamerasTransferListProps {
roleId: string roleId: string
@ -60,7 +61,7 @@ const CamerasTransferList = ({
refetch() refetch()
} }
console.log('CamerasTransferListProps rendered') if (!isProduction) console.log('CamerasTransferListProps rendered')
return ( return (
<> <>
<Flex w='100%' justify='center'> <Flex w='100%' justify='center'>

View File

@ -9,6 +9,7 @@ import { getResolvedTimeZone, parseQueryDateToDate } from '../../utils/dateUtil'
import RetryError from '../RetryError'; import RetryError from '../RetryError';
import { strings } from '../../strings/strings'; import { strings } from '../../strings/strings';
import { RecordSummary } from '../../../types/record'; import { RecordSummary } from '../../../types/record';
import { isProduction } from '../../env.const';
const CameraAccordion = () => { const CameraAccordion = () => {
const { recordingsStore: recStore } = useContext(Context) const { recordingsStore: recStore } = useContext(Context)
@ -60,7 +61,7 @@ const CameraAccordion = () => {
return [] return []
} }
console.log('CameraAccordion rendered') if (!isProduction) console.log('CameraAccordion rendered')
return ( return (
<Accordion variant='separated' radius="md" w='100%'> <Accordion variant='separated' radius="md" w='100%'>

View File

@ -15,6 +15,7 @@ import { IconExternalLink, IconShare } from '@tabler/icons-react';
import { routesPath } from '../../../router/routes.path'; import { routesPath } from '../../../router/routes.path';
import AccordionShareButton from '../buttons/AccordionShareButton'; import AccordionShareButton from '../buttons/AccordionShareButton';
import VideoDownloader from '../../../widgets/VideoDownloader'; import VideoDownloader from '../../../widgets/VideoDownloader';
import { isProduction } from '../../env.const';
interface RecordingAccordionProps { interface RecordingAccordionProps {
recordSummary?: RecordSummary recordSummary?: RecordSummary
@ -57,7 +58,7 @@ const DayAccordion = ({
if (playedValue) { if (playedValue) {
const url = createRecordURL(playedValue) const url = createRecordURL(playedValue)
if (url) { if (url) {
console.log('GET URL: ', url) if (!isProduction) console.log('GET URL: ', url)
setPlayerUrl(url) setPlayerUrl(url)
} }
} else { } else {
@ -85,7 +86,7 @@ const DayAccordion = ({
setVideoPlayerState(undefined) setVideoPlayerState(undefined)
} }
console.log('DayAccordion rendered') if (!isProduction) console.log('DayAccordion rendered')
const hourLabel = (hour: string, eventsQty: number) => ( const hourLabel = (hour: string, eventsQty: number) => (
<Group> <Group>

View File

@ -16,6 +16,7 @@ import AccordionControlButton from '../buttons/AccordionControlButton';
import AccordionShareButton from '../buttons/AccordionShareButton'; import AccordionShareButton from '../buttons/AccordionShareButton';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import EventPanel from './EventPanel'; import EventPanel from './EventPanel';
import { isProduction } from '../../env.const';
/** /**
* @param day frigate format, e.g day: 2024-02-23 * @param day frigate format, e.g day: 2024-02-23
@ -92,10 +93,9 @@ const EventsAccordion = ({
useEffect(() => { useEffect(() => {
if (playedValue) { if (playedValue) {
// console.log('openVideoPlayer', playedValue)
if (playedValue && host) { if (playedValue && host) {
const url = createEventUrl(playedValue) const url = createEventUrl(playedValue)
console.log('GET EVENT URL: ', url) if (!isProduction) console.log('GET EVENT URL: ', url)
setPlayerUrl(url) setPlayerUrl(url)
} }
} else { } else {
@ -108,8 +108,6 @@ const EventsAccordion = ({
if (!data || data.length < 1) return <Center><Text>Not have events at that period</Text></Center> if (!data || data.length < 1) return <Center><Text>Not have events at that period</Text></Center>
const handleOpenPlayer = (openedValue: string) => { const handleOpenPlayer = (openedValue: string) => {
// console.log(`openVideoPlayer day:${day} hour:${hour}, opened value: ${openedValue}`)
// console.log(`opened value: ${openedValue}, eventId: ${playedValue}`)
if (openedValue !== playedValue) { if (openedValue !== playedValue) {
setOpenedItem(openedValue) setOpenedItem(openedValue)
setPlayedValue(openedValue) setPlayedValue(openedValue)

View File

@ -3,6 +3,7 @@ import { IconShare } from '@tabler/icons-react';
import React from 'react'; import React from 'react';
import AccordionControlButton from './AccordionControlButton'; import AccordionControlButton from './AccordionControlButton';
import { routesPath } from '../../../router/routes.path'; import { routesPath } from '../../../router/routes.path';
import { isProduction } from '../../env.const';
interface AccordionShareButtonProps { interface AccordionShareButtonProps {
recordUrl?: string recordUrl?: string
@ -19,13 +20,13 @@ const AccordionShareButton = ({
if (canShare && url) { if (canShare && url) {
try { try {
await navigator.share({ url }); await navigator.share({ url });
console.log('Content shared successfully'); if (!isProduction) console.log('Content shared successfully');
} catch (err) { } catch (err) {
console.error('Error sharing content: ', err); console.error('Error sharing content: ', err);
} }
} else { } else {
clipboard.copy(url) clipboard.copy(url)
console.log('URL copied to clipboard') if (!isProduction) console.log('URL copied to clipboard')
} }
} }

View File

@ -8,6 +8,7 @@ import { Center, Loader, Text } from '@mantine/core';
import OneSelectFilter, { OneSelectItem } from './OneSelectFilter'; import OneSelectFilter, { OneSelectItem } from './OneSelectFilter';
import { strings } from '../../strings/strings'; import { strings } from '../../strings/strings';
import RetryError from '../RetryError'; import RetryError from '../RetryError';
import { isProduction } from '../../env.const';
interface CameraSelectFilterProps { interface CameraSelectFilterProps {
selectedHostId: string, selectedHostId: string,
@ -26,7 +27,7 @@ const CameraSelectFilter = ({
useEffect(() => { useEffect(() => {
if (!data) return if (!data) return
if (recStore.cameraIdParam) { if (recStore.cameraIdParam) {
console.log('change camera by param') if (!isProduction) console.log('change camera by param')
recStore.filteredCamera = data.find( camera => camera.id === recStore.cameraIdParam) recStore.filteredCamera = data.find( camera => camera.id === recStore.cameraIdParam)
recStore.cameraIdParam = undefined recStore.cameraIdParam = undefined
} }
@ -47,8 +48,7 @@ const CameraSelectFilter = ({
recStore.filteredCamera = camera recStore.filteredCamera = camera
} }
console.log('CameraSelectFilter rendered') if (!isProduction) console.log('CameraSelectFilter rendered')
// console.log('recStore.selectedCameraId', recStore.selectedCameraId)
return ( return (
<OneSelectFilter <OneSelectFilter

View File

@ -5,10 +5,9 @@ import { strings } from '../../strings/strings';
import { Box, Flex, Indicator, Text } from '@mantine/core'; import { Box, Flex, Indicator, Text } from '@mantine/core';
import CloseWithTooltip from '../buttons/CloseWithTooltip'; import CloseWithTooltip from '../buttons/CloseWithTooltip';
import { Context } from '../../..'; import { Context } from '../../..';
import { isProduction } from '../../env.const';
interface DateRangeSelectFilterProps { interface DateRangeSelectFilterProps {}
}
const DateRangeSelectFilter = ({ const DateRangeSelectFilter = ({
@ -16,11 +15,10 @@ const DateRangeSelectFilter = ({
const { recordingsStore: recStore } = useContext(Context) const { recordingsStore: recStore } = useContext(Context)
const handlePick = (value: [Date | null, Date | null]) => { const handlePick = (value: [Date | null, Date | null]) => {
console.log('handlePick',value)
recStore.selectedRange = value recStore.selectedRange = value
} }
console.log('DateRangeSelectFilter rendered') if (!isProduction) console.log('DateRangeSelectFilter rendered')
return ( return (
<Box> <Box>
<Flex <Flex
@ -35,7 +33,6 @@ const DateRangeSelectFilter = ({
allowSingleDateInRange allowSingleDateInRange
valueFormat="YYYY-MM-DD" valueFormat="YYYY-MM-DD"
type="range" type="range"
placeholder={strings.selectRange}
mx="auto" mx="auto"
maw={400} maw={400}
value={recStore.selectedRange} value={recStore.selectedRange}

View File

@ -44,7 +44,7 @@ function MSEPlayer({
const wsURL = useMemo(() => { const wsURL = useMemo(() => {
return wsUrl; return wsUrl;
}, [camera]); }, [wsUrl]);
const play = () => { const play = () => {
const currentVideo = videoRef.current; const currentVideo = videoRef.current;
@ -70,7 +70,7 @@ function MSEPlayer({
return CODECS.filter((codec) => return CODECS.filter((codec) =>
isSupported(`video/mp4; codecs="${codec}"`) isSupported(`video/mp4; codecs="${codec}"`)
).join(); ).join();
}, []); }, [CODECS]);
const onConnect = useCallback(() => { const onConnect = useCallback(() => {
if (!videoRef.current?.isConnected || !wsURL || wsRef.current) return false; if (!videoRef.current?.isConnected || !wsURL || wsRef.current) return false;
@ -252,7 +252,7 @@ function MSEPlayer({
return () => { return () => {
onDisconnect(); onDisconnect();
}; };
}, [playbackEnabled, onDisconnect, onConnect]); }, [playbackEnabled, onDisconnect, onConnect, visibilityCheck]);
return ( return (
<video <video

View File

@ -3,19 +3,21 @@ import videojs from 'video.js';
import Player from 'video.js/dist/types/player'; import Player from 'video.js/dist/types/player';
import 'video.js/dist/video-js.css' import 'video.js/dist/video-js.css'
import { getToken } from '../../../services/frigate.proxy/frigate.api'; import { getToken } from '../../../services/frigate.proxy/frigate.api';
import { isProduction } from '../../env.const';
interface VideoPlayerProps { interface VideoPlayerProps {
videoUrl: string videoUrl: string
} }
const VideoPlayer = ({ videoUrl }: VideoPlayerProps) => { const VideoPlayer = ({ videoUrl }: VideoPlayerProps) => {
const executed = useRef(false)
const videoRef = useRef<HTMLVideoElement>(null); const videoRef = useRef<HTMLVideoElement>(null);
const playerRef = useRef<Player | null>(null); const playerRef = useRef<Player | null>(null);
useEffect(() => { useEffect(() => {
if (!executed.current) {
//@ts-ignore //@ts-ignore
videojs.Vhs.xhr.beforeRequest = function(options: any) { videojs.Vhs.xhr.beforeRequest = function (options: any) {
options.headers = { options.headers = {
...options.headers, ...options.headers,
Authorization: `Bearer ${getToken()}`, Authorization: `Bearer ${getToken()}`,
@ -47,29 +49,31 @@ const VideoPlayer = ({ videoUrl }: VideoPlayerProps) => {
//TODO add rotations on IOS and android devices //TODO add rotations on IOS and android devices
console.log('playerRef.current', playerRef.current) if (!isProduction) console.log('playerRef.current', playerRef.current)
if (videoRef.current) { if (videoRef.current) {
console.log('mount new player') if (!isProduction) console.log('mount new player')
playerRef.current = videojs(videoRef.current, { ...defaultOptions }, () => { playerRef.current = videojs(videoRef.current, { ...defaultOptions }, () => {
console.log('player is ready'); if (!isProduction) console.log('player is ready')
}); });
} }
console.log('VideoPlayer rendered') if (!isProduction) console.log('VideoPlayer rendered')
return () => { return () => {
if (playerRef.current !== null) { if (playerRef.current !== null) {
playerRef.current.dispose(); playerRef.current.dispose();
playerRef.current = null; playerRef.current = null;
console.log('unmount player') if (!isProduction) console.log('unmount player')
} }
}; };
}, []); }
executed.current = true
}, [videoUrl]);
useEffect(() => { useEffect(() => {
if (playerRef.current) { if (playerRef.current) {
playerRef.current.src(videoUrl); playerRef.current.src(videoUrl);
console.log('player change src') if (!isProduction) console.log('player change src')
} }
}, [videoUrl]); }, [videoUrl]);

View File

@ -153,7 +153,7 @@ export default function WebRtcPlayer({
pcRef.current = undefined; pcRef.current = undefined;
} }
}; };
}, [camera, connect, PeerConnection, pcRef, videoRef, playbackEnabled]); }, [camera, connect, PeerConnection, pcRef, videoRef, playbackEnabled, wsUrl]);
return ( return (
<video <video

View File

@ -1,12 +1,12 @@
import { Center, Flex, MantineStyleSystemProps, Text, TextProps, Tooltip } from '@mantine/core'; import { Center, Text, TextProps, Tooltip } from '@mantine/core';
import { IconChevronDown, IconChevronUp, IconSelector, } from '@tabler/icons-react'; import { IconChevronDown, IconChevronUp, IconSelector, } from '@tabler/icons-react';
import React, { FC } from 'react'; import React from 'react';
interface SortedThProps { interface SortedThProps {
title: string, title: string,
reversed: boolean, reversed: boolean,
sortedName: string | null, sortedName: string | null,
textProps?: TextProps & React.RefAttributes<HTMLDivElement> textProps?: TextProps
sorting?: boolean sorting?: boolean
onSort: (title: string) => void, onSort: (title: string) => void,
} }

View File

@ -1,28 +1,15 @@
export const appMode = process.env.NODE_ENV export const appMode = process.env.NODE_ENV
const isProduction = appMode === "production" export const isProduction = appMode === "production"
if (isProduction && typeof process.env.REACT_APP_HOST === 'undefined') { export const host = isProduction ? window.env?.REACT_APP_HOST : process.env.HOST
throw new Error('REACT_APP_HOST environment variable is undefined');
}
export const host = process.env.REACT_APP_HOST
if (isProduction && typeof process.env.REACT_APP_PORT === 'undefined') { export const port = isProduction ? window.env?.REACT_APP_PORT : process.env.PORT
throw new Error('REACT_APP_PORT environment variable is undefined');
}
export const port = process.env.REACT_APP_PORT
if (typeof process.env.REACT_APP_FRIGATE_PROXY === 'undefined') { const proxy = isProduction ? window.env?.REACT_APP_FRIGATE_PROXY : process.env.REACT_APP_FRIGATE_PROXY
throw new Error('REACT_APP_FRIGATE_PROXY environment variable is undefined'); export const proxyURL = new URL(proxy || '')
}
export const proxyURL = new URL(process.env.REACT_APP_FRIGATE_PROXY)
if (typeof process.env.REACT_APP_OPENID_SERVER === 'undefined') {
throw new Error('REACT_APP_OPENID_SERVER environment variable is undefined');
}
if (typeof process.env.REACT_APP_CLIENT_ID === 'undefined') {
throw new Error('REACT_APP_CLIENT_ID environment variable is undefined');
}
const oidpServer = isProduction ? window.env?.REACT_APP_OPENID_SERVER : process.env.REACT_APP_OPENID_SERVER
const oidpClientId = isProduction ? window.env?.REACT_APP_CLIENT_ID : process.env.REACT_APP_CLIENT_ID
export const oidpSettings = { export const oidpSettings = {
server: process.env.REACT_APP_OPENID_SERVER, server: oidpServer || '',
clientId: process.env.REACT_APP_CLIENT_ID, clientId: oidpClientId || '',
} }

View File

@ -1,6 +1,5 @@
import { makeAutoObservable, runInAction } from "mobx" import { makeAutoObservable } from "mobx"
import RootStore from "./root.store" import RootStore from "./root.store"
import { Resource } from "../utils/resource"
export class ModalStore { export class ModalStore {

View File

@ -32,7 +32,7 @@ export const DeliveryPointSchema = z.object({
export type DeliveryPoint = z.infer<typeof DeliveryPointSchema> export type DeliveryPoint = z.infer<typeof DeliveryPointSchema>
export class UserStore { export class UserStore {
private _user: Resource<UserServer> = new Resource<UserServer> private _user: Resource<UserServer> = new Resource<UserServer>()
public get user() { public get user() {
return this._user; return this._user;
} }
@ -55,8 +55,8 @@ export class UserStore {
this._user.isLoading = true this._user.isLoading = true
const res = await this.fetchUserFromServer() const res = await this.fetchUserFromServer()
try { try {
runInAction( () => { runInAction(() => {
this._user = {...this._user, data: res} this._user = { ...this._user, data: res }
}) })
} catch (error) { } catch (error) {
console.error(error) console.error(error)

View File

@ -228,7 +228,7 @@ export const formatUnixTimestampToDateTime = (unixTimestamp: number, config: Dat
const options: Intl.DateTimeFormatOptions = { const options: Intl.DateTimeFormatOptions = {
dateStyle: date_style, dateStyle: date_style,
timeStyle: time_style, timeStyle: time_style,
hour12: time_format !== 'browser' ? time_format == '12hour' : undefined, hour12: time_format !== 'browser' ? time_format === '12hour' : undefined,
}; };
// Only set timeZone option when resolvedTimeZone does not match UTC±HH:MM format, or when timezone is set in config // Only set timeZone option when resolvedTimeZone does not match UTC±HH:MM format, or when timezone is set in config

9
src/types/global.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
declare global {
interface Window {
env?: {
[key: string]: string;
};
}
}
export {};

View File

@ -1,12 +1,10 @@
import React from 'react'; import { Button, Card, Flex, Grid, Group, Text, createStyles } from '@mantine/core';
import { CameraConfig } from '../types/frigateConfig';
import { AspectRatio, Button, Card, Flex, Grid, Group, Space, Text, createStyles, useMantineTheme } from '@mantine/core';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { routesPath } from '../router/routes.path';
import { GetCameraWHostWConfig, GetFrigateHost } from '../services/frigate.proxy/frigate.schema';
import { frigateApi, mapHostToHostname, proxyApi } from '../services/frigate.proxy/frigate.api';
import AutoUpdatedImage from '../shared/components/images/AutoUpdatedImage';
import { recordingsPageQuery } from '../pages/RecordingsPage'; 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 { strings } from '../shared/strings/strings'; import { strings } from '../shared/strings/strings';

View File

@ -1,16 +1,17 @@
import { Button, Flex, Switch, Table, Text, TextInput, useMantineTheme } from '@mantine/core'; import { Button, Flex, Table } from '@mantine/core';
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { IconPlus, IconTrash } from '@tabler/icons-react';
import ObjectId from 'bson-objectid';
import React, { useCallback, useEffect, useState } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { GetFrigateHost } from '../services/frigate.proxy/frigate.schema';
import HostSettingsMenu from '../shared/components/menu/HostSettingsMenu';
import SortedTh from '../shared/components/table.aps/SortedTh'; import SortedTh from '../shared/components/table.aps/SortedTh';
import { strings } from '../shared/strings/strings'; import { strings } from '../shared/strings/strings';
import { v4 as uuidv4 } from 'uuid' import { debounce } from '../shared/utils/debounce';
import { IconBulbFilled, IconBulbOff, IconDeviceFloppy, IconPencil, IconPlus, IconSettings, IconTrash } from '@tabler/icons-react'; import StateCell from './hosts.table/StateCell';
import SwitchCell from './hosts.table/SwitchCell'; import SwitchCell from './hosts.table/SwitchCell';
import TextInputCell from './hosts.table/TextInputCell'; import TextInputCell from './hosts.table/TextInputCell';
import ObjectId from 'bson-objectid'; import { isProduction } from '../shared/env.const';
import { debounce } from '../shared/utils/debounce';
import HostSettingsMenu from '../shared/components/menu/HostSettingsMenu';
import { GetFrigateHost } from '../services/frigate.proxy/frigate.schema';
import StateCell from './hosts.table/StateCell';
interface TableProps<T> { interface TableProps<T> {
data: T[], data: T[],
@ -20,7 +21,7 @@ interface TableProps<T> {
} }
const FrigateHostsTable = ({ data, showAddButton = false, saveCallback, changedCallback }: TableProps<GetFrigateHost>) => { const FrigateHostsTable = ({ data, showAddButton = false, saveCallback, changedCallback }: TableProps<GetFrigateHost>) => {
console.log('FrigateHostsTable rendered') if (!isProduction) console.log('FrigateHostsTable rendered')
const [tableData, setTableData] = useState(data) const [tableData, setTableData] = useState(data)
const [reversed, setReversed] = useState(false) const [reversed, setReversed] = useState(false)
const [sortedName, setSortedName] = useState<string | null>(null) const [sortedName, setSortedName] = useState<string | null>(null)
@ -31,7 +32,7 @@ const FrigateHostsTable = ({ data, showAddButton = false, saveCallback, changedC
const debouncedChanged = useCallback(debounce((tableData: GetFrigateHost[]) => { const debouncedChanged = useCallback(debounce((tableData: GetFrigateHost[]) => {
if (changedCallback) changedCallback(tableData) if (changedCallback) changedCallback(tableData)
}, 200), []) }, 200), [tableData])
useEffect(() => { useEffect(() => {
debouncedChanged(tableData) debouncedChanged(tableData)

View File

@ -1,14 +1,13 @@
import React, { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import JSMpegPlayer from '../shared/components/players/JSMpegPlayer';
import MSEPlayer from '../shared/components/players/MsePlayer';
import { CameraConfig } from '../types/frigateConfig';
import { LivePlayerMode } from '../types/live';
import useCameraActivity from '../hooks/use-camera-activity'; import useCameraActivity from '../hooks/use-camera-activity';
import useCameraLiveMode from '../hooks/use-camera-live-mode'; import useCameraLiveMode from '../hooks/use-camera-live-mode';
import WebRtcPlayer from '../shared/components/players/WebRTCPlayer'; import { proxyApi } from '../services/frigate.proxy/frigate.api';
import { AspectRatio, Flex } from '@mantine/core';
import { frigateApi, proxyApi } from '../services/frigate.proxy/frigate.api';
import { GetCameraWHostWConfig } from '../services/frigate.proxy/frigate.schema'; import { GetCameraWHostWConfig } from '../services/frigate.proxy/frigate.schema';
import JSMpegPlayer from '../shared/components/players/JSMpegPlayer';
import MSEPlayer from '../shared/components/players/MsePlayer';
import WebRtcPlayer from '../shared/components/players/WebRTCPlayer';
import { LivePlayerMode } from '../types/live';
import { isProduction } from '../shared/env.const';
type LivePlayerProps = { type LivePlayerProps = {
camera: GetCameraWHostWConfig; camera: GetCameraWHostWConfig;
@ -27,7 +26,7 @@ const Player = ({
const wsUrl = proxyApi.cameraWsURL(hostNameWPort, camera.name) const wsUrl = proxyApi.cameraWsURL(hostNameWPort, camera.name)
const cameraConfig = camera.config! const cameraConfig = camera.config!
const { activeMotion, activeAudio, activeTracking } = const { activeMotion, activeTracking } =
useCameraActivity(cameraConfig); useCameraActivity(cameraConfig);
const cameraActive = useMemo( const cameraActive = useMemo(
@ -41,7 +40,7 @@ const Player = ({
const [liveReady, setLiveReady] = useState(false); const [liveReady, setLiveReady] = useState(false);
useEffect(() => { useEffect(() => {
if (!liveReady) { if (!liveReady) {
if (cameraActive && liveMode == "jsmpeg") { if (cameraActive && liveMode === "jsmpeg") {
setLiveReady(true); setLiveReady(true);
} }
@ -51,11 +50,11 @@ const Player = ({
if (!cameraActive) { if (!cameraActive) {
setLiveReady(false); setLiveReady(false);
} }
}, [cameraActive, liveReady]); }, [cameraActive, liveReady, liveMode]);
console.log(`liveMode: `, liveMode) if (!isProduction) console.log(`liveMode: `, liveMode)
let player; let player;
if (liveMode == "webrtc") { if (liveMode === "webrtc") {
player = ( player = (
<WebRtcPlayer <WebRtcPlayer
className={`rounded-2xl h-full ${liveReady ? "" : "hidden"}`} className={`rounded-2xl h-full ${liveReady ? "" : "hidden"}`}
@ -65,7 +64,7 @@ const Player = ({
wsUrl={wsUrl} wsUrl={wsUrl}
/> />
); );
} else if (liveMode == "mse") { } else if (liveMode === "mse") {
if ("MediaSource" in window || "ManagedMediaSource" in window) { if ("MediaSource" in window || "ManagedMediaSource" in window) {
player = ( player = (
<MSEPlayer <MSEPlayer
@ -84,7 +83,7 @@ const Player = ({
</div> </div>
); );
} }
} else if (liveMode == "jsmpeg") { } else if (liveMode === "jsmpeg") {
player = ( player = (
<JSMpegPlayer <JSMpegPlayer
wsUrl={wsUrl} wsUrl={wsUrl}

View File

@ -4,15 +4,12 @@ import { Context } from '..';
import CameraSelectFilter from '../shared/components/filters.aps/CameraSelectFilter'; import CameraSelectFilter from '../shared/components/filters.aps/CameraSelectFilter';
import DateRangeSelectFilter from '../shared/components/filters.aps/DateRangeSelectFilter'; import DateRangeSelectFilter from '../shared/components/filters.aps/DateRangeSelectFilter';
import HostSelectFilter from '../shared/components/filters.aps/HostSelectFilter'; import HostSelectFilter from '../shared/components/filters.aps/HostSelectFilter';
import { isProduction } from '../shared/env.const';
interface RecordingsFiltersRightSideProps { const RecordingsFiltersRightSide = () => {
}
const RecordingsFiltersRightSide = ({
}: RecordingsFiltersRightSideProps) => {
const { recordingsStore: recStore } = useContext(Context) const { recordingsStore: recStore } = useContext(Context)
console.log('RecordingsFiltersRightSide rendered') if (!isProduction) console.log('RecordingsFiltersRightSide rendered')
return ( return (
<> <>
<HostSelectFilter /> <HostSelectFilter />

View File

@ -1,11 +1,11 @@
import { Button, Loader, Text, Notification, Progress } from '@mantine/core'; import { Button, Loader, Progress, Text } from '@mantine/core';
import { useMutation } from '@tanstack/react-query';
import { frigateApi, proxyApi } from '../services/frigate.proxy/frigate.api';
import { IconAlertCircle, IconExternalLink } from '@tabler/icons-react';
import { useEffect, useState } from 'react';
import RetryError from '../shared/components/RetryError';
import { formatFileTimestamps, unixTimeToDate } from '../shared/utils/dateUtil';
import { notifications } from '@mantine/notifications'; import { notifications } from '@mantine/notifications';
import { IconAlertCircle, IconExternalLink } from '@tabler/icons-react';
import { useMutation } from '@tanstack/react-query';
import { useEffect, useState } from 'react';
import { proxyApi } from '../services/frigate.proxy/frigate.api';
import RetryError from '../shared/components/RetryError';
import { formatFileTimestamps } from '../shared/utils/dateUtil';
interface VideoDownloaderProps { interface VideoDownloaderProps {
cameraName: string cameraName: string
@ -84,7 +84,7 @@ const VideoDownloader = ({
setTimer(undefined) setTimer(undefined)
}, 5 * 60 * 1000) }, 5 * 60 * 1000)
} }
}, [createName, link, videoBlob]) }, [createName, link, videoBlob, checkVideo, getVideBlob, hostName, timer])
useEffect(() => { useEffect(() => {
if (videoBlob && videoBlob instanceof Blob && createName) { if (videoBlob && videoBlob instanceof Blob && createName) {
@ -97,7 +97,7 @@ const VideoDownloader = ({
} }
} }
} }
}, [videoBlob, createName, link]) }, [videoBlob, createName, link, deleteVideo])
const checkTime = () => { const checkTime = () => {
const duration = endUnixTime - startUnixTime const duration = endUnixTime - startUnixTime
@ -105,8 +105,6 @@ const VideoDownloader = ({
notifications.show({ notifications.show({
id: 'too-much-time', id: 'too-much-time',
withCloseButton: true, withCloseButton: true,
onClose: () => console.log('unmounted'),
onOpen: () => console.log('mounted'),
autoClose: 5000, autoClose: 5000,
title: "Max duration", title: "Max duration",
message: `Time can not be higher than ${maxVideoTime / 60} hour`, message: `Time can not be higher than ${maxVideoTime / 60} hour`,
@ -124,12 +122,13 @@ const VideoDownloader = ({
} }
const handleCancel = () => { // TODO delete
clearTimeout(timer) // const handleCancel = () => {
setTimer(undefined) // clearTimeout(timer)
setCreateName(undefined) // setTimer(undefined)
setLink(undefined) // setCreateName(undefined)
} // setLink(undefined)
// }
if (startUnixTime === 0 || endUnixTime === 0) return null if (startUnixTime === 0 || endUnixTime === 0) return null

View File

@ -14,7 +14,7 @@ interface SwithCellProps {
export const SwitchCell = ( { value, defaultValue, width, id, propertyName, toggle }: SwithCellProps ) => { export const SwitchCell = ( { value, defaultValue, width, id, propertyName, toggle }: SwithCellProps ) => {
const theme = useMantineTheme(); const theme = useMantineTheme();
if (typeof value === undefined && typeof defaultValue !== undefined) value = defaultValue if (!value && !defaultValue) value = defaultValue
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (id && toggle && propertyName) toggle(id, propertyName, event.target.value) if (id && toggle && propertyName) toggle(id, propertyName, event.target.value)
} }

View File

@ -21,6 +21,6 @@
"jsx": "react-jsx" "jsx": "react-jsx"
}, },
"include": [ "include": [
"src" "src",
] ]
} }

3974
yarn.lock

File diff suppressed because it is too large Load Diff