add locales

This commit is contained in:
NlightN22 2024-03-10 17:57:47 +07:00
parent acaf99c878
commit edf8bf7bc9
52 changed files with 500 additions and 424 deletions

View File

@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
# Build commands: # Build commands:
# - $VERSION=0.6 # - $VERSION=0.7
# - rm build -r -Force ; rm ./node_modules/.cache/babel-loader -r -Force ; yarn build # - 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 build --pull --rm -t oncharterliz/multi-frigate:latest -t oncharterliz/multi-frigate:$VERSION "."
# - docker image push --all-tags oncharterliz/multi-frigate # - docker image push --all-tags oncharterliz/multi-frigate

View File

@ -27,6 +27,8 @@
"date-fns": "^3.3.1", "date-fns": "^3.3.1",
"dayjs": "^1.11.9", "dayjs": "^1.11.9",
"embla-carousel-react": "^8.0.0-rc10", "embla-carousel-react": "^8.0.0-rc10",
"i18next": "^23.10.1",
"i18next-browser-languagedetector": "^7.2.0",
"idb-keyval": "^6.2.1", "idb-keyval": "^6.2.1",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"mantine-react-table": "^1.0.0-beta.25", "mantine-react-table": "^1.0.0-beta.25",
@ -39,6 +41,7 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-device-detect": "^2.2.3", "react-device-detect": "^2.2.3",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-i18next": "^14.1.0",
"react-oidc-context": "^2.2.2", "react-oidc-context": "^2.2.2",
"react-router-dom": "^6.14.1", "react-router-dom": "^6.14.1",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",

View File

@ -62,6 +62,7 @@ function App() {
urlParams.delete('state'); urlParams.delete('state');
urlParams.delete('session_state'); urlParams.delete('session_state');
urlParams.delete('code'); urlParams.delete('code');
urlParams.delete('iss');
navigate(`${location.pathname}${urlParams.toString() ? '?' + urlParams.toString() : ''}`, { replace: true }) navigate(`${location.pathname}${urlParams.toString() ? '?' + urlParams.toString() : ''}`, { replace: true })
} }

View File

@ -1,14 +1,24 @@
import React, { useContext, useState } from 'react'; import { AppShell, useMantineTheme, } from "@mantine/core";
import { AppShell, useMantineTheme, } from "@mantine/core"
import { HeaderAction } from './widgets/header/HeaderAction';
import { headerLinks } from './widgets/header/header.links';
import AppRouter from './router/AppRouter';
import { Context } from '.';
import SideBar from './shared/components/SideBar';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { useContext, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Context } from '.';
import AppRouter from './router/AppRouter';
import { routesPath } from './router/routes.path';
import SideBar from './shared/components/SideBar';
import { isProduction } from './shared/env.const'; import { isProduction } from './shared/env.const';
import { HeaderAction } from './widgets/header/HeaderAction';
const AppBody = () => { const AppBody = () => {
const { t } = useTranslation()
const headerLinks = [
{ link: routesPath.MAIN_PATH, label: t('header.home') },
{ link: routesPath.SETTINGS_PATH, label: t('header.settings'), admin: true },
{ link: routesPath.RECORDINGS_PATH, label: t('header.recordings') },
{ link: routesPath.HOSTS_PATH, label: t('header.hostsConfig'), admin: true },
{ link: routesPath.ACCESS_PATH, label: t('header.acessSettings'), admin: true },
]
const { sideBarsStore } = useContext(Context) const { sideBarsStore } = useContext(Context)

View File

@ -6,6 +6,7 @@ import RootStore from './shared/stores/root.store';
import { AuthProvider, AuthProviderProps } from 'react-oidc-context'; import { AuthProvider, AuthProviderProps } from 'react-oidc-context';
import { isProduction, oidpSettings } from './shared/env.const'; import { isProduction, oidpSettings } from './shared/env.const';
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom';
import './services/i18n';
const root = ReactDOM.createRoot( const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement document.getElementById('root') as HTMLElement
@ -23,6 +24,7 @@ export const keycloakConfig: AuthProviderProps = {
params.delete('state'); params.delete('state');
params.delete('session_state'); params.delete('session_state');
params.delete('code'); params.delete('code');
params.delete('iss');
const newUrl = `${window.location.pathname}${params.toString() ? '?' + params.toString() : ''}` const newUrl = `${window.location.pathname}${params.toString() ? '?' + params.toString() : ''}`
window.history.replaceState({}, document.title, newUrl) window.history.replaceState({}, document.title, newUrl)
} }

63
src/locales/en.ts Normal file
View File

@ -0,0 +1,63 @@
const en = {
header: {
home: 'Main',
settings: 'Settings',
recordings: 'Recordings',
hostsConfig: 'Frigate servers',
acessSettings: 'Access settings',
}, hostArr: {
host: 'Host',
name: 'Host name',
url: 'Address',
enabled: 'Enabled',
},
player: {
startVideo: 'Enable Video',
stopVideo: 'Disable Video',
object: 'Object',
duration: 'Duration',
startTime: 'Start',
endTime: 'End',
doubleClickToFullHint: 'Double click for fullscreen',
rating: 'Rating',
},
pleaseSelectRole: 'Please select Role',
pleaseSelectHost: 'Please select Host',
pleaseSelectCamera: 'Please select Camera',
pleaseSelectDate: 'Please select Date',
nothingHere: 'Nothing here',
allowed: 'Allowed',
notAllowed: 'Not allowed',
camera: 'Camera',
camersDoesNotExist: 'No cameras',
search: 'Search',
recordings: 'Recordings',
hour: 'Hour',
events: 'Events',
notHaveEvents: 'No events',
day: 'Day',
selectHost: 'Select host',
selectCamera: 'Select Camera',
selectRange: 'Select period',
changeTheme: "Change theme",
logout: "Logout",
enterQuantity: "Enter quantity:",
quantity: "Quantity",
tooltipСlose: "Press Enter",
hide: "Hide",
confirm: "Confirm",
save: "Save",
discard: "Cancel",
next: "Next",
back: "Back",
goToMainPage: "Return to main page",
retry: "Retry",
youCanRetryOrGoToMain: "You can retry or return to the main page",
errors: {
somthingGoesWrong: "Something went wrong",
403: "Sorry, you do not have access",
404: "Sorry, we cannot find that page",
}
}
export default en

64
src/locales/ru.ts Normal file
View File

@ -0,0 +1,64 @@
const ru = {
header: {
home: 'Главная',
settings: 'Настройки',
recordings: 'Записи',
hostsConfig: 'Серверы Frigate',
acessSettings: 'Настройка доступа',
},
hostArr: {
host: 'Хост',
name: 'Имя хоста',
url: 'Адрес',
enabled: 'Включен',
},
player: {
startVideo: 'Вкл. Видео',
stopVideo: 'Выкл. Видео',
object: 'Объект',
duration: 'Длительность',
startTime: 'Начало',
endTime: 'Конец',
doubleClickToFullHint: 'Двойное нажатие мышью для полноэкранного просмотра',
rating: 'Рейтинг',
},
pleaseSelectRole: 'Пожалуйста выберите роль',
pleaseSelectHost: 'Пожалуйста выберите хост',
pleaseSelectCamera: 'Пожалуйста выберите камеру',
pleaseSelectDate: 'Пожалуйста выберите дату',
nothingHere: 'Ничего нет',
allowed: 'Разрешено',
notAllowed: 'Не разрешено',
camera: 'Камера',
camersDoesNotExist: 'Камер нет',
search: 'Поиск',
recordings: 'Записи',
hour: 'Час',
events: 'События',
notHaveEvents: 'Событий нет',
day: 'День',
selectHost:'Выбери хост',
selectCamera: 'Выбери камеру',
selectRange: 'Выбери период',
changeTheme: "Изменить тему",
logout: "Выйти",
enterQuantity: "Введите количество:",
quantity: "Количество",
tooltipСlose: "Нажмите Enter",
hide: "Скрыть",
confirm: "Подтвердить",
save: "Сохранить",
discard: "Отменить",
next: "Далее",
back: "Назад",
goToMainPage: "Вернуться на главную",
retry: "Повторить",
youCanRetryOrGoToMain: "Вы можете повторить или вернуться на главную",
errors: {
somthingGoesWrong: "Что-то пошло не так",
403: "Извините, у вас нет доступа",
404: "Извините, мы не можем найти такую страницу",
}
}
export default ru

View File

@ -1,12 +1,13 @@
import { Button, Flex, Text } from '@mantine/core'; import { Button, Flex, Text } from '@mantine/core';
import React, { useContext, useEffect, useRef } from 'react';
import CogWheelWithText from '../shared/components/loaders/CogWheelWithText';
import { strings } from '../shared/strings/strings';
import { routesPath } from '../router/routes.path';
import { Context } from '..';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { useContext, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { Context } from '..';
import { routesPath } from '../router/routes.path';
import CogWheelWithText from '../shared/components/loaders/CogWheelWithText';
const Forbidden = () => { const Forbidden = () => {
const { t } = useTranslation()
const executed = useRef(false) const executed = useRef(false)
const { sideBarsStore } = useContext(Context) const { sideBarsStore } = useContext(Context)
@ -25,9 +26,9 @@ const Forbidden = () => {
return ( return (
<Flex h='100%' direction='column' justify='center' align='center' gap='1rem'> <Flex h='100%' direction='column' justify='center' align='center' gap='1rem'>
<Text fz='lg' fw={700}>{strings.errors[403]}</Text> <Text fz='lg' fw={700}>{t('errors.403')}</Text>
<CogWheelWithText text='403' /> <CogWheelWithText text='403' />
<Button onClick={handleGoToMain}>{strings.goToMainPage}</Button> <Button onClick={handleGoToMain}>{t('goToMainPage')}</Button>
</Flex> </Flex>
); );

View File

@ -1,12 +1,13 @@
import { Button, Flex, Text } from '@mantine/core'; import { Button, Flex, Text } from '@mantine/core';
import React, { useContext, useEffect, useRef } from 'react'; import React, { useContext, useEffect, useRef } from 'react';
import CogWheelWithText from '../shared/components/loaders/CogWheelWithText'; import CogWheelWithText from '../shared/components/loaders/CogWheelWithText';
import { strings } from '../shared/strings/strings';
import { routesPath } from '../router/routes.path'; import { routesPath } from '../router/routes.path';
import { Context } from '..'; import { Context } from '..';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { useTranslation } from 'react-i18next';
const NotFound = () => { const NotFound = () => {
const { t } = useTranslation()
const executed = useRef(false) const executed = useRef(false)
const { sideBarsStore } = useContext(Context) const { sideBarsStore } = useContext(Context)
@ -25,9 +26,9 @@ const NotFound = () => {
return ( return (
<Flex h='100%' direction='column' justify='center' align='center' gap='1rem'> <Flex h='100%' direction='column' justify='center' align='center' gap='1rem'>
<Text fz='lg' fw={700}>{strings.errors[404]}</Text> <Text fz='lg' fw={700}>{t('errors.404')}</Text>
<CogWheelWithText text='404' /> <CogWheelWithText text='404' />
<Button onClick={handleGoToMain}>{strings.goToMainPage}</Button> <Button onClick={handleGoToMain}>{t('goToMainPage')}</Button>
</Flex> </Flex>
); );

View File

@ -1,21 +1,22 @@
import { useQuery } from '@tanstack/react-query';
import React, { useContext, useEffect, useRef, useState } from 'react';
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
import CenterLoader from '../shared/components/loaders/CenterLoader';
import RetryErrorPage from './RetryErrorPage';
import { Flex, Group, Select, Text } from '@mantine/core'; import { Flex, Group, Select, Text } from '@mantine/core';
import { OneSelectItem } from '../shared/components/filters/OneSelectFilter';
import { useMediaQuery } from '@mantine/hooks'; import { useMediaQuery } from '@mantine/hooks';
import { dimensions } from '../shared/dimensions/dimensions'; import { useQuery } from '@tanstack/react-query';
import CamerasTransferList from '../shared/components/CamerasTransferList';
import { Context } from '..';
import { strings } from '../shared/strings/strings';
import { useAdminRole } from '../hooks/useAdminRole';
import Forbidden from './403';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { useContext, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Context } from '..';
import { useAdminRole } from '../hooks/useAdminRole';
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
import CamerasTransferList from '../shared/components/CamerasTransferList';
import { OneSelectItem } from '../shared/components/filters/OneSelectFilter';
import CenterLoader from '../shared/components/loaders/CenterLoader';
import { dimensions } from '../shared/dimensions/dimensions';
import { isProduction } from '../shared/env.const'; import { isProduction } from '../shared/env.const';
import Forbidden from './403';
import RetryErrorPage from './RetryErrorPage';
const AccessSettings = () => { const AccessSettings = () => {
const { t } = useTranslation()
const executed = useRef(false) const executed = useRef(false)
const { data, isPending, isError, refetch } = useQuery({ const { data, isPending, isError, refetch } = useQuery({
queryKey: [frigateQueryKeys.getRoles], queryKey: [frigateQueryKeys.getRoles],
@ -49,7 +50,7 @@ const AccessSettings = () => {
if (!isProduction) 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'>{t('pleaseSelectRole')}</Text>
<Flex justify='space-between' align='center' w='100%'> <Flex justify='space-between' align='center' w='100%'>
{!isMobile ? <Group w='40%' /> : <></>} {!isMobile ? <Group w='40%' /> : <></>}
<Select <Select

View File

@ -1,22 +1,24 @@
import { Button, Flex, Text } from '@mantine/core'; import { Button, Flex, Text } from '@mantine/core';
import { notifications } from '@mantine/notifications';
import { IconAlertCircle } from '@tabler/icons-react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { useContext, useEffect, useRef, useState } from 'react'; import { useContext, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Context } from '..'; import { Context } from '..';
import { useAdminRole } from '../hooks/useAdminRole'; import { useAdminRole } from '../hooks/useAdminRole';
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api'; import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
import { GetFrigateHost, deleteFrigateHostSchema, putFrigateHostSchema } from '../services/frigate.proxy/frigate.schema'; import { GetFrigateHost, deleteFrigateHostSchema, putFrigateHostSchema } from '../services/frigate.proxy/frigate.schema';
import CenterLoader from '../shared/components/loaders/CenterLoader'; import CenterLoader from '../shared/components/loaders/CenterLoader';
import { isProduction } from '../shared/env.const'; import { isProduction } from '../shared/env.const';
import { strings } from '../shared/strings/strings';
import FrigateHostsTable from '../widgets/FrigateHostsTable'; import FrigateHostsTable from '../widgets/FrigateHostsTable';
import Forbidden from './403'; import Forbidden from './403';
import RetryErrorPage from './RetryErrorPage'; import RetryErrorPage from './RetryErrorPage';
import { notifications } from '@mantine/notifications';
import { IconAlertCircle } from '@tabler/icons-react';
const FrigateHostsPage = () => { const FrigateHostsPage = () => {
const { t } = useTranslation()
const executed = useRef(false) const executed = useRef(false)
const queryClient = useQueryClient() const queryClient = useQueryClient()
const { isPending: hostsPending, error: hostsError, data } = useQuery({ const { isPending: hostsPending, error: hostsError, data } = useQuery({
@ -106,8 +108,8 @@ const FrigateHostsPage = () => {
<Flex w='100%' h='100%' direction='column'> <Flex w='100%' h='100%' direction='column'>
<FrigateHostsTable data={pageData} showAddButton changedCallback={handleChange} /> <FrigateHostsTable data={pageData} showAddButton changedCallback={handleChange} />
<Flex justify='center'> <Flex justify='center'>
<Button m='0.5rem' onClick={handleDiscard}>{strings.discard}</Button> <Button m='0.5rem' onClick={handleDiscard}>{t('discard')}</Button>
<Button m='0.5rem' onClick={handleSave}>{strings.save}</Button> <Button m='0.5rem' onClick={handleSave}>{t('save')}</Button>
</Flex> </Flex>
</Flex> </Flex>
); );

View File

@ -8,11 +8,12 @@ import CenterLoader from '../shared/components/loaders/CenterLoader';
import RetryErrorPage from './RetryErrorPage'; import RetryErrorPage from './RetryErrorPage';
import Player from '../widgets/Player'; import Player from '../widgets/Player';
import { Button, Flex, Text } from '@mantine/core'; import { Button, Flex, Text } from '@mantine/core';
import { strings } from '../shared/strings/strings';
import { routesPath } from '../router/routes.path'; import { routesPath } from '../router/routes.path';
import { recordingsPageQuery } from './RecordingsPage'; import { recordingsPageQuery } from './RecordingsPage';
import { useTranslation } from 'react-i18next';
const LiveCameraPage = () => { const LiveCameraPage = () => {
const { t } = useTranslation()
const executed = useRef(false) const executed = useRef(false)
const navigate = useNavigate() const navigate = useNavigate()
let { id: cameraId } = useParams<'id'>() let { id: cameraId } = useParams<'id'>()
@ -48,9 +49,9 @@ const LiveCameraPage = () => {
return ( return (
<Flex w='100%' h='100%' justify='center' align='center' direction='column'> <Flex w='100%' h='100%' justify='center' align='center' direction='column'>
<Flex w='100%' justify='center' align='baseline' mb='1rem'> <Flex w='100%' justify='center' align='baseline' mb='1rem'>
<Text mr='1rem'>{strings.camera}: {camera.name} {camera.frigateHost ? `/ ${camera.frigateHost.name}` : ''}</Text> <Text mr='1rem'>{t('camera')}: {camera.name} {camera.frigateHost ? `/ ${camera.frigateHost.name}` : ''}</Text>
{!camera.frigateHost ? <></> : {!camera.frigateHost ? <></> :
<Button onClick={handleOpenRecordings}>{strings.recordings}</Button> <Button onClick={handleOpenRecordings}>{t('recordings')}</Button>
} }
</Flex> </Flex>
<Player camera={camera} /> <Player camera={camera} />

View File

@ -8,11 +8,13 @@ import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.
import { GetCameraWHostWConfig } from '../services/frigate.proxy/frigate.schema'; import { GetCameraWHostWConfig } from '../services/frigate.proxy/frigate.schema';
import HostSelect from '../shared/components/filters/HostSelect'; import HostSelect from '../shared/components/filters/HostSelect';
import CenterLoader from '../shared/components/loaders/CenterLoader'; import CenterLoader from '../shared/components/loaders/CenterLoader';
import { strings } from '../shared/strings/strings';
import CameraCard from '../widgets/CameraCard'; import CameraCard from '../widgets/CameraCard';
import RetryErrorPage from './RetryErrorPage'; import RetryErrorPage from './RetryErrorPage';
import ClearableTextInput from '../shared/components/inputs/ClearableTextInput';
import { useTranslation } from 'react-i18next';
const MainPage = () => { const MainPage = () => {
const { t } = useTranslation()
const executed = useRef(false) const executed = useRef(false)
const { sideBarsStore } = useContext(Context) const { sideBarsStore } = useContext(Context)
const [searchQuery, setSearchQuery] = useState<string>() const [searchQuery, setSearchQuery] = useState<string>()
@ -88,10 +90,11 @@ const MainPage = () => {
justifyContent: 'center', justifyContent: 'center',
}} }}
> >
<TextInput <ClearableTextInput
clerable
maw={400} maw={400}
style={{ flexGrow: 1 }} style={{ flexGrow: 1 }}
placeholder={strings.search} placeholder={t('search')}
icon={<IconSearch size="0.9rem" stroke={1.5} />} icon={<IconSearch size="0.9rem" stroke={1.5} />}
value={searchQuery} value={searchQuery}
onChange={(event) => setSearchQuery(event.currentTarget.value)} onChange={(event) => setSearchQuery(event.currentTarget.value)}
@ -101,7 +104,7 @@ const MainPage = () => {
onChange={handleSelectHost} onChange={handleSelectHost}
ml='1rem' ml='1rem'
spaceBetween='0px' spaceBetween='0px'
placeholder={strings.selectHost} placeholder={t('selectHost')}
/> />
</Flex> </Flex>
</Flex> </Flex>

View File

@ -10,6 +10,7 @@ import RecordingsFiltersRightSide from '../widgets/RecordingsFiltersRightSide';
import SelectedCameraList from '../widgets/SelectedCameraList'; import SelectedCameraList from '../widgets/SelectedCameraList';
import SelectedDayList from '../widgets/SelectedDayList'; import SelectedDayList from '../widgets/SelectedDayList';
import SelectedHostList from '../widgets/SelectedHostList'; import SelectedHostList from '../widgets/SelectedHostList';
import { useTranslation } from 'react-i18next';
export const recordingsPageQuery = { export const recordingsPageQuery = {
@ -21,6 +22,7 @@ export const recordingsPageQuery = {
} }
const RecordingsPage = () => { const RecordingsPage = () => {
const { t } = useTranslation()
const executed = useRef(false) const executed = useRef(false)
const { sideBarsStore, recordingsStore: recStore } = useContext(Context) const { sideBarsStore, recordingsStore: recStore } = useContext(Context)
@ -116,10 +118,10 @@ const RecordingsPage = () => {
return ( return (
<Flex w='100%' h='100%' direction='column' justify='center' align='center'> <Flex w='100%' h='100%' direction='column' justify='center' align='center'>
{!hostId ? {!hostId ?
<Text size='xl'>Please select host</Text> <Text size='xl'>{t('pleaseSelectHost')}</Text>
: <></>} : <></>}
{hostId && !(startDay && endDay) ? {hostId && !(startDay && endDay) ?
<Text size='xl'>Please select date</Text> <Text size='xl'>{t('pleaseSelectDate')}</Text>
: <></> : <></>
} }
</Flex> </Flex>

View File

@ -1,11 +1,11 @@
import { Flex, Button, Text } from '@mantine/core'; import { Flex, Button, Text } from '@mantine/core';
import React, { useContext, useEffect, useRef } from 'react'; import React, { useContext, useEffect, useRef } from 'react';
import { routesPath } from '../router/routes.path'; import { routesPath } from '../router/routes.path';
import { strings } from '../shared/strings/strings';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { ExclamationCogWheel } from '../shared/components/svg/ExclamationCogWheel'; import { ExclamationCogWheel } from '../shared/components/svg/ExclamationCogWheel';
import { Context } from '..'; import { Context } from '..';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { useTranslation } from 'react-i18next';
interface RetryErrorPageProps { interface RetryErrorPageProps {
repeatVisible?: boolean repeatVisible?: boolean
@ -20,6 +20,7 @@ const RetryErrorPage = ({
mainVisible = true, mainVisible = true,
onRetry onRetry
}: RetryErrorPageProps) => { }: RetryErrorPageProps) => {
const { t } = useTranslation()
const executed = useRef(false) const executed = useRef(false)
const navigate = useNavigate() const navigate = useNavigate()
@ -49,13 +50,13 @@ const RetryErrorPage = ({
return ( return (
<Flex h='100%' direction='column' justify='center' align='center' gap='1rem'> <Flex h='100%' direction='column' justify='center' align='center' gap='1rem'>
<Text fz='lg' fw={700}>{strings.errors.somthengGoesWrong}</Text> <Text fz='lg' fw={700}>{t('errors.somthingGoesWrong')}</Text>
{ExclamationCogWheel} {ExclamationCogWheel}
<Text fz='lg' fw={700}>{strings.youCanRetryOrGoToMain}</Text> <Text fz='lg' fw={700}>{t('youCanRetryOrGoToMain')}</Text>
<Flex> <Flex>
{repeatVisible ? <Button ml='1rem' onClick={handleRetry}>{strings.retry}</Button> : null} {repeatVisible ? <Button ml='1rem' onClick={handleRetry}>{t('retry')}</Button> : null}
{ backVisible ? <Button ml='1rem' onClick={handleGoBack}>{strings.back}</Button> : null } { backVisible ? <Button ml='1rem' onClick={handleGoBack}>{t('back')}</Button> : null }
{ mainVisible ? <Button ml='1rem' onClick={handleGoToMain}>{strings.goToMainPage}</Button> : null } { mainVisible ? <Button ml='1rem' onClick={handleGoToMain}>{t('goToMainPage')}</Button> : null }
</Flex> </Flex>
</Flex> </Flex>
); );

View File

@ -1,25 +1,26 @@
import React, { useContext, useEffect, useRef, useState } from 'react';
import {
useQuery,
useMutation,
useQueryClient,
} from '@tanstack/react-query'
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
import CenterLoader from '../shared/components/loaders/CenterLoader';
import RetryErrorPage from './RetryErrorPage';
import { Button, Flex, Space } from '@mantine/core'; import { Button, Flex, Space } from '@mantine/core';
import { FloatingLabelInput } from '../shared/components/inputs/FloatingLabelInput';
import { strings } from '../shared/strings/strings';
import { dimensions } from '../shared/dimensions/dimensions';
import { useMediaQuery } from '@mantine/hooks'; import { useMediaQuery } from '@mantine/hooks';
import { GetConfig } from '../services/frigate.proxy/frigate.schema'; import {
useMutation,
useQuery,
useQueryClient,
} from '@tanstack/react-query';
import { observer } from 'mobx-react-lite';
import React, { useContext, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Context } from '..'; import { Context } from '..';
import { useAdminRole } from '../hooks/useAdminRole'; import { useAdminRole } from '../hooks/useAdminRole';
import Forbidden from './403'; import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
import { observer } from 'mobx-react-lite'; import { GetConfig } from '../services/frigate.proxy/frigate.schema';
import { FloatingLabelInput } from '../shared/components/inputs/FloatingLabelInput';
import CenterLoader from '../shared/components/loaders/CenterLoader';
import { dimensions } from '../shared/dimensions/dimensions';
import { isProduction } from '../shared/env.const'; import { isProduction } from '../shared/env.const';
import Forbidden from './403';
import RetryErrorPage from './RetryErrorPage';
const SettingsPage = () => { const SettingsPage = () => {
const { t } = useTranslation()
const executed = useRef(false) const executed = useRef(false)
const queryClient = useQueryClient() const queryClient = useQueryClient()
const { isPending: configPending, error: configError, data, refetch } = useQuery({ const { isPending: configPending, error: configError, data, refetch } = useQuery({
@ -126,8 +127,8 @@ const SettingsPage = () => {
))} ))}
<Space h='2%' /> <Space h='2%' />
<Flex w='100%' justify='stretch' wrap='nowrap' align='center'> <Flex w='100%' justify='stretch' wrap='nowrap' align='center'>
<Button w='100%' onClick={handleDiscard} m='0.5rem'>{strings.discard}</Button> <Button w='100%' onClick={handleDiscard} m='0.5rem'>{t('discard')}</Button>
<Button w='100%' type="submit" m='0.5rem'>{strings.confirm}</Button> <Button w='100%' type="submit" m='0.5rem'>{t('confirm')}</Button>
</Flex> </Flex>
</form> </form>
</Flex> </Flex>

View File

@ -21,7 +21,7 @@ export const getToken = (): string | undefined => {
const instanceApi = axios.create({ const instanceApi = axios.create({
baseURL: proxyURL.toString(), baseURL: proxyURL.toString(),
timeout: 60 * 1000, timeout: 20 * 1000,
}) })
instanceApi.interceptors.request.use( instanceApi.interceptors.request.use(

21
src/services/i18n.ts Normal file
View File

@ -0,0 +1,21 @@
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
import LanguageDetector from 'i18next-browser-languagedetector'
import en from '../locales/en'
import ru from '../locales/ru'
i18n
.use(initReactI18next)
.use(LanguageDetector)
.init({
resources: {
en: { translation: en},
ru: { translation: ru},
},
fallbackLng: 'en',
interpolation: {
escapeValue: false,
},
})
export default i18n

View File

@ -1,12 +1,11 @@
import { Button, Flex, Text, TransferList, TransferListData, TransferListItem } from '@mantine/core';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import React, { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { frigateApi, frigateQueryKeys } from '../../services/frigate.proxy/frigate.api'; import { frigateApi, frigateQueryKeys } from '../../services/frigate.proxy/frigate.api';
import CogwheelLoader from './loaders/CogwheelLoader';
import RetryError from './RetryError';
import { TransferList, Text, TransferListData, TransferListProps, TransferListItem, Button, Flex } from '@mantine/core';
import { OneSelectItem } from './filters/OneSelectFilter';
import { strings } from '../strings/strings';
import { isProduction } from '../env.const'; import { isProduction } from '../env.const';
import RetryError from './RetryError';
import CogwheelLoader from './loaders/CogwheelLoader';
interface CamerasTransferListProps { interface CamerasTransferListProps {
roleId: string roleId: string
@ -15,6 +14,7 @@ interface CamerasTransferListProps {
const CamerasTransferList = ({ const CamerasTransferList = ({
roleId, roleId,
}: CamerasTransferListProps) => { }: CamerasTransferListProps) => {
const { t } = useTranslation()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const { data: cameras, isPending, isError, refetch } = useQuery({ const { data: cameras, isPending, isError, refetch } = useQuery({
queryKey: [frigateQueryKeys.getCamerasWHost, roleId], queryKey: [frigateQueryKeys.getCamerasWHost, roleId],
@ -50,7 +50,7 @@ const CamerasTransferList = ({
if (isPending) return <CogwheelLoader /> if (isPending) return <CogwheelLoader />
if (isError || !cameras) return <RetryError onRetry={refetch} /> if (isError || !cameras) return <RetryError onRetry={refetch} />
if (cameras.length < 1) return <Text> {strings.camersDoesNotExist}</Text> if (cameras.length < 1) return <Text> {t('camersDoesNotExist')}</Text>
const handleSave = () => { const handleSave = () => {
@ -65,8 +65,8 @@ const CamerasTransferList = ({
return ( return (
<> <>
<Flex w='100%' justify='center'> <Flex w='100%' justify='center'>
<Button mt='1rem' miw='6rem' mr='1rem' onClick={handleDiscard}>{strings.discard}</Button> <Button mt='1rem' miw='6rem' mr='1rem' onClick={handleDiscard}>{t('discard')}</Button>
<Button mt='1rem' miw='5rem' onClick={handleSave}>{strings.save}</Button> <Button mt='1rem' miw='5rem' onClick={handleSave}>{t('save')}</Button>
</Flex> </Flex>
<TransferList <TransferList
transferAllMatchingFilter transferAllMatchingFilter
@ -74,9 +74,9 @@ const CamerasTransferList = ({
mt='1rem' mt='1rem'
value={lists} value={lists}
onChange={handleChange} onChange={handleChange}
searchPlaceholder={strings.search} searchPlaceholder={t('search')}
nothingFound={strings.nothingHere} nothingFound={t('nothingHere')}
titles={[strings.notAllowed, strings.allowed]} titles={[t('notAllowed'), t('allowed')]}
breakpoint="sm" breakpoint="sm"
/> />
</> </>

View File

@ -1,10 +1,10 @@
import { Aside, Button, Navbar, createStyles } from "@mantine/core"; import { Aside, Button, Navbar, createStyles } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks"; import { useDisclosure } from "@mantine/hooks";
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import React, { useContext, useEffect, useRef, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { useTranslation } from "react-i18next";
import { Context } from '../..'; import { Context } from '../..';
import { dimensions } from '../dimensions/dimensions'; import { dimensions } from '../dimensions/dimensions';
import { strings } from '../strings/strings';
import { useMantineSize } from '../utils/mantine.size.convertor'; import { useMantineSize } from '../utils/mantine.size.convertor';
import { SideButton } from './SideButton'; import { SideButton } from './SideButton';
@ -29,6 +29,7 @@ const useStyles = createStyles((theme,
const SideBar = ({ isHidden, side, children }: SideBarProps) => { const SideBar = ({ isHidden, side, children }: SideBarProps) => {
const { t } = useTranslation()
const hideSizePx = useMantineSize(dimensions.hideSidebarsSize) const hideSizePx = useMantineSize(dimensions.hideSidebarsSize)
const initialVisible = () => { const initialVisible = () => {
const savedVisibility = localStorage.getItem(`sidebarVisible_${side}`); const savedVisibility = localStorage.getItem(`sidebarVisible_${side}`);
@ -97,7 +98,7 @@ const SideBar = ({ isHidden, side, children }: SideBarProps) => {
p={dimensions.hideSidebarsSize} p={dimensions.hideSidebarsSize}
width={{ sm: 200, lg: 300, }} width={{ sm: 200, lg: 300, }}
> >
<Button onClick={() => handleClickVisible(false)}>{strings.hide}</Button> <Button onClick={() => handleClickVisible(false)}>{t('hide')}</Button>
{leftChildren} {leftChildren}
</Navbar> </Navbar>
: :
@ -105,7 +106,7 @@ const SideBar = ({ isHidden, side, children }: SideBarProps) => {
className={classes.aside} className={classes.aside}
p={dimensions.hideSidebarsSize} p={dimensions.hideSidebarsSize}
width={{ sm: 200, lg: 300 }}> width={{ sm: 200, lg: 300 }}>
<Button onClick={() => handleClickVisible(false)}>{strings.hide}</Button> <Button onClick={() => handleClickVisible(false)}>{t('hide')}</Button>
{rightChildren} {rightChildren}
</Aside> </Aside>
} }

View File

@ -1,17 +1,25 @@
import React from 'react'; import { Avatar, Button, Flex, Group, Menu, Text } from "@mantine/core";
import { Avatar, Group, Menu, Text, Button, Flex } from "@mantine/core";
import { useAuth } from 'react-oidc-context';
import { strings } from '../strings/strings';
import { useMediaQuery } from '@mantine/hooks'; import { useMediaQuery } from '@mantine/hooks';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useAuth } from 'react-oidc-context';
import { keycloakConfig } from '../..';
import { dimensions } from '../dimensions/dimensions'; import { dimensions } from '../dimensions/dimensions';
import ColorSchemeToggle from './buttons/ColorSchemeToggle'; import ColorSchemeToggle from './buttons/ColorSchemeToggle';
import { keycloakConfig } from '../..';
interface UserMenuProps { interface UserMenuProps {
user: { name: string; image: string } user: { name: string; image: string }
} }
const UserMenu = ({ user }: UserMenuProps) => { const UserMenu = ({ user }: UserMenuProps) => {
const { t, i18n } = useTranslation()
const languages = [
{ lng: 'en', name: 'Eng' },
{ lng: 'ru', name: 'Rus' },
]
const auth = useAuth() const auth = useAuth()
const isMiddleScreen = useMediaQuery(dimensions.middleScreenSize) const isMiddleScreen = useMediaQuery(dimensions.middleScreenSize)
@ -23,6 +31,24 @@ const UserMenu = ({ user }: UserMenuProps) => {
await auth.signoutRedirect({ post_logout_redirect_uri: keycloakConfig.redirect_uri, id_token_hint: id_token_hint }) await auth.signoutRedirect({ post_logout_redirect_uri: keycloakConfig.redirect_uri, id_token_hint: id_token_hint })
} }
const handleChangeLanguage = async (lng: string) => {
await i18n.changeLanguage(lng)
}
const languageSelector = useCallback(() => {
return languages.map(lang => (
<Button
key={lang.lng}
size='xs'
component="a"
variant="outline"
disabled={i18n.resolvedLanguage === lang.lng}
onClick={() => handleChangeLanguage(lang.lng)}>
{lang.name}
</Button>
))
}, [i18n.resolvedLanguage])
return ( return (
<Menu <Menu
width={260} width={260}
@ -43,20 +69,17 @@ const UserMenu = ({ user }: UserMenuProps) => {
{ {
isMiddleScreen ? isMiddleScreen ?
<Flex w='100%' justify='space-between' align='center'> <Flex w='100%' justify='space-between' align='center'>
<Text fz='sm' ml='0.7rem'>{strings.changeTheme}</Text> <Text fz='sm' ml='0.7rem'>{t('changeTheme')}</Text>
<ColorSchemeToggle /> <ColorSchemeToggle />
</Flex> </Flex>
: :
<></> <></>
} }
{/* <Menu.Item> {
{strings.settings} languageSelector()
</Menu.Item> }
<Menu.Item onClick={handleAboutMe}>
{strings.aboutMe}
</Menu.Item> */}
<Menu.Item onClick={handleLogout}> <Menu.Item onClick={handleLogout}>
{strings.logout} {t('logout')}
</Menu.Item> </Menu.Item>
</Menu.Dropdown> </Menu.Dropdown>
</Menu> </Menu>

View File

@ -1,17 +1,18 @@
import { Accordion, Center, Loader } from '@mantine/core'; import { Accordion, Center, Loader } from '@mantine/core';
import React, { useContext, useMemo } from 'react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { frigateQueryKeys, mapHostToHostname, proxyApi } from '../../../services/frigate.proxy/frigate.api';
import DayAccordion from './DayAccordion';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { useContext, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Context } from '../../..'; import { Context } from '../../..';
import { getResolvedTimeZone, parseQueryDateToDate } from '../../utils/dateUtil'; import { frigateQueryKeys, mapHostToHostname, proxyApi } from '../../../services/frigate.proxy/frigate.api';
import RetryError from '../RetryError';
import { strings } from '../../strings/strings';
import { RecordSummary } from '../../../types/record'; import { RecordSummary } from '../../../types/record';
import { isProduction } from '../../env.const'; import { isProduction } from '../../env.const';
import { getResolvedTimeZone, parseQueryDateToDate } from '../../utils/dateUtil';
import RetryError from '../RetryError';
import DayAccordion from './DayAccordion';
const CameraAccordion = () => { const CameraAccordion = () => {
const { t } = useTranslation()
const { recordingsStore: recStore } = useContext(Context) const { recordingsStore: recStore } = useContext(Context)
const camera = recStore.openedCamera || recStore.filteredCamera const camera = recStore.openedCamera || recStore.filteredCamera
@ -30,7 +31,7 @@ const CameraAccordion = () => {
const recodItem = (record: RecordSummary) => ( const recodItem = (record: RecordSummary) => (
<Accordion.Item key={record.day} value={record.day}> <Accordion.Item key={record.day} value={record.day}>
<Accordion.Control key={record.day + 'control'}>{strings.day}: {record.day}</Accordion.Control> <Accordion.Control key={record.day + 'control'}>{t('day')}: {record.day}</Accordion.Control>
<Accordion.Panel key={record.day + 'panel'}> <Accordion.Panel key={record.day + 'panel'}>
<DayAccordion key={record.day + 'day'} recordSummary={record} /> <DayAccordion key={record.day + 'day'} recordSummary={record} />
</Accordion.Panel> </Accordion.Panel>

View File

@ -1,12 +1,12 @@
import { Accordion, Flex, Group, Text } from '@mantine/core'; import { Accordion, Flex, Group, Text } from '@mantine/core';
import { IconExternalLink } from '@tabler/icons-react'; import { IconExternalLink } from '@tabler/icons-react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { routesPath } from '../../../router/routes.path'; import { routesPath } from '../../../router/routes.path';
import { proxyApi } from '../../../services/frigate.proxy/frigate.api'; import { proxyApi } from '../../../services/frigate.proxy/frigate.api';
import { RecordHour, RecordSummary } from '../../../types/record'; import { RecordHour, RecordSummary } from '../../../types/record';
import { isProduction } from '../../env.const'; import { isProduction } from '../../env.const';
import { strings } from '../../strings/strings';
import { getResolvedTimeZone, mapDateHourToUnixTime } from '../../utils/dateUtil'; import { getResolvedTimeZone, mapDateHourToUnixTime } from '../../utils/dateUtil';
import AccordionControlButton from '../buttons/AccordionControlButton'; import AccordionControlButton from '../buttons/AccordionControlButton';
import AccordionShareButton from '../buttons/AccordionShareButton'; import AccordionShareButton from '../buttons/AccordionShareButton';
@ -30,6 +30,7 @@ const DayAccordionItem = ({
played, played,
openPlayer openPlayer
}: DayAccordionItemProps) => { }: DayAccordionItemProps) => {
const { t } = useTranslation()
const navigate = useNavigate() const navigate = useNavigate()
const [playedURL, setPlayedUrl] = useState<string>() const [playedURL, setPlayedUrl] = useState<string>()
@ -72,11 +73,11 @@ const DayAccordionItem = ({
<Accordion.Control key={hour + 'Control'}> <Accordion.Control key={hour + 'Control'}>
<Flex justify='space-between'> <Flex justify='space-between'>
<Group> <Group>
<Text>{strings.hour}: {hour}:00</Text> <Text>{t('hour')}: {hour}:00</Text>
{recordHour.events > 0 ? {recordHour.events > 0 ?
<Text>{strings.events}: {recordHour.events}</Text> <Text>{t('events')}: {recordHour.events}</Text>
: :
<Text>{strings.notHaveEvents}</Text> <Text>{t('notHaveEvents')}</Text>
} }
</Group> </Group>
<Group> <Group>

View File

@ -1,6 +1,6 @@
import { Accordion, Text } from '@mantine/core'; import { Accordion, Text } from '@mantine/core';
import React, { Suspense, lazy, useState } from 'react'; import { Suspense, lazy, useState } from 'react';
import { strings } from '../../strings/strings'; import { useTranslation } from 'react-i18next';
const EventsAccordion = lazy(() => import('./EventsAccordion')) const EventsAccordion = lazy(() => import('./EventsAccordion'))
interface DayEventsAccordionProps { interface DayEventsAccordionProps {
@ -14,6 +14,7 @@ const DayEventsAccordion = ({
hour, hour,
qty, qty,
}: DayEventsAccordionProps) => { }: DayEventsAccordionProps) => {
const { t } = useTranslation()
const [openedItem, setOpenedItem] = useState<string>() const [openedItem, setOpenedItem] = useState<string>()
const handleClick = (value: string | null) => { const handleClick = (value: string | null) => {
@ -22,7 +23,7 @@ const DayEventsAccordion = ({
return ( return (
<Accordion onChange={handleClick}> <Accordion onChange={handleClick}>
<Accordion.Item value={hour}> <Accordion.Item value={hour}>
<Accordion.Control><Text>{strings.events}: {qty}</Text></Accordion.Control> <Accordion.Control><Text>{t('events')}: {qty}</Text></Accordion.Control>
<Accordion.Panel> <Accordion.Panel>
{openedItem === hour ? {openedItem === hour ?
<Suspense> <Suspense>

View File

@ -1,8 +1,8 @@
import { Accordion, Center, Flex, Text } from '@mantine/core'; import { Accordion, Center, Flex, Text } from '@mantine/core';
import VideoDownloader from '../../../widgets/VideoDownloader'; import VideoDownloader from '../../../widgets/VideoDownloader';
import { strings } from '../../strings/strings';
import VideoPlayer from '../players/VideoPlayer'; import VideoPlayer from '../players/VideoPlayer';
import DayEventsAccordion from './DayEventsAccordion'; import DayEventsAccordion from './DayEventsAccordion';
import { useTranslation } from 'react-i18next';
interface DayPanelProps { interface DayPanelProps {
day: string, day: string,
@ -27,6 +27,7 @@ const DayPanel = ({
startUnixTime, startUnixTime,
endUnixTime, endUnixTime,
}: DayPanelProps) => { }: DayPanelProps) => {
const { t } = useTranslation()
return ( return (
<Accordion.Panel key={hour + 'Panel'}> <Accordion.Panel key={hour + 'Panel'}>
{playedURL && playedURL === videoURL ? <VideoPlayer videoUrl={playedURL} /> : <></>} {playedURL && playedURL === videoURL ? <VideoPlayer videoUrl={playedURL} /> : <></>}
@ -43,7 +44,7 @@ const DayPanel = ({
{events > 0 ? {events > 0 ?
<DayEventsAccordion day={day} hour={hour} qty={events} /> <DayEventsAccordion day={day} hour={hour} qty={events} />
: :
<Center><Text>{strings.notHaveEvents}</Text></Center> <Center><Text>{t('notHaveEvents')}</Text></Center>
} }
</Accordion.Panel> </Accordion.Panel>
); );

View File

@ -2,10 +2,10 @@ import { Button, Flex, Text } from '@mantine/core';
import { IconExternalLink } from '@tabler/icons-react'; import { IconExternalLink } from '@tabler/icons-react';
import { proxyApi } from '../../../services/frigate.proxy/frigate.api'; import { proxyApi } from '../../../services/frigate.proxy/frigate.api';
import { EventFrigate } from '../../../types/event'; import { EventFrigate } from '../../../types/event';
import { strings } from '../../strings/strings';
import { getDurationFromTimestamps, unixTimeToDate } from '../../utils/dateUtil'; import { getDurationFromTimestamps, unixTimeToDate } from '../../utils/dateUtil';
import BlobImage from '../images/BlobImage'; import BlobImage from '../images/BlobImage';
import VideoPlayer from '../players/VideoPlayer'; import VideoPlayer from '../players/VideoPlayer';
import { useTranslation } from 'react-i18next';
interface EventPanelProps { interface EventPanelProps {
event: EventFrigate event: EventFrigate
@ -20,6 +20,7 @@ const EventPanel = ({
videoURL, videoURL,
playedURL, playedURL,
}: EventPanelProps) => { }: EventPanelProps) => {
const { t } = useTranslation()
return ( return (
<> <>
@ -45,12 +46,12 @@ const EventPanel = ({
</Button> </Button>
</Flex> </Flex>
} }
<Text mt='1rem'>{strings.camera}: {event.camera}</Text> <Text mt='1rem'>{t('camera')}: {event.camera}</Text>
<Text>{strings.player.object}: {event.label}</Text> <Text>{t('player.object')}: {event.label}</Text>
<Text>{strings.player.startTime}: {unixTimeToDate(event.start_time)}</Text> <Text>{t('player.startTime')}: {unixTimeToDate(event.start_time)}</Text>
<Text>{strings.player.duration}: {getDurationFromTimestamps(event.start_time, event.end_time)}</Text> <Text>{t('player.duration')}: {getDurationFromTimestamps(event.start_time, event.end_time)}</Text>
{!event.data?.score? <></> : {!event.data?.score? <></> :
<Text>{strings.player.rating}: {(event.data.score * 100).toFixed(2)}%</Text> <Text>{t('player.rating')}: {(event.data.score * 100).toFixed(2)}%</Text>
} }
</Flex> </Flex>
</Flex> </Flex>

View File

@ -6,11 +6,11 @@ import AccordionShareButton from '../buttons/AccordionShareButton';
import PlayControl from '../buttons/PlayControl'; import PlayControl from '../buttons/PlayControl';
import EventPanel from './EventPanel'; import EventPanel from './EventPanel';
import { EventFrigate } from '../../../types/event'; import { EventFrigate } from '../../../types/event';
import { strings } from '../../strings/strings';
import { unixTimeToDate, getDurationFromTimestamps } from '../../utils/dateUtil'; import { unixTimeToDate, getDurationFromTimestamps } from '../../utils/dateUtil';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { routesPath } from '../../../router/routes.path'; import { routesPath } from '../../../router/routes.path';
import { proxyApi } from '../../../services/frigate.proxy/frigate.api'; import { proxyApi } from '../../../services/frigate.proxy/frigate.api';
import { useTranslation } from 'react-i18next';
interface EventsAccordionItemProps { interface EventsAccordionItemProps {
@ -25,8 +25,8 @@ const EventsAccordionItem = ({
hostName, hostName,
played, played,
openPlayer, openPlayer,
} : EventsAccordionItemProps) => { }: EventsAccordionItemProps) => {
const { t } = useTranslation()
const [playedURL, setPlayedUrl] = useState<string>() const [playedURL, setPlayedUrl] = useState<string>()
const navigate = useNavigate() const navigate = useNavigate()
@ -44,7 +44,7 @@ const EventsAccordionItem = ({
const duration = getDurationFromTimestamps(event.start_time, event.end_time) const duration = getDurationFromTimestamps(event.start_time, event.end_time)
return ( return (
<Group> <Group>
<Text>{strings.player.object}: {event.label}</Text> <Text>{t('player.object')}: {event.label}</Text>
<Text>{time}</Text> <Text>{time}</Text>
{duration ? {duration ?
<Text>{duration}</Text> <Text>{duration}</Text>
@ -84,7 +84,7 @@ const EventsAccordionItem = ({
<IconExternalLink /> <IconExternalLink />
</AccordionControlButton> </AccordionControlButton>
<PlayControl <PlayControl
played={played ? played : false } played={played ? played : false}
onClick={handleOpenPlayer} /> onClick={handleOpenPlayer} />
</Group> </Group>
</Flex> </Flex>

View File

@ -1,4 +1,4 @@
import { Button, ButtonProps, Group, UnstyledButton, createStyles } from '@mantine/core'; import { ButtonProps, Group, createStyles } from '@mantine/core';
import React from 'react'; import React from 'react';
const useStyles = createStyles((theme) => ({ const useStyles = createStyles((theme) => ({

View File

@ -1,6 +1,5 @@
import { Tooltip, CloseButton, CloseButtonProps } from '@mantine/core'; import { Tooltip, CloseButton, CloseButtonProps } from '@mantine/core';
import React from 'react'; import React from 'react';
import { strings } from '../../strings/strings';
interface CloseWithTooltipProps { interface CloseWithTooltipProps {

View File

@ -1,7 +1,7 @@
import { Flex, createStyles } from '@mantine/core'; import { Flex, createStyles } from '@mantine/core';
import { IconPlayerPlayFilled, IconPlayerStopFilled } from '@tabler/icons-react'; import { IconPlayerPlayFilled, IconPlayerStopFilled } from '@tabler/icons-react';
import { strings } from '../../strings/strings';
import AccordionControlButton from './AccordionControlButton'; import AccordionControlButton from './AccordionControlButton';
import { useTranslation } from 'react-i18next';
const useStyles = createStyles((theme) => ({ const useStyles = createStyles((theme) => ({
@ -22,6 +22,7 @@ const PlayControl = ({
played, played,
onClick onClick
}: PlayControlProps) => { }: PlayControlProps) => {
const { t } = useTranslation()
const { classes } = useStyles(); const { classes } = useStyles();
const handleClick = () => { const handleClick = () => {
@ -32,7 +33,7 @@ const PlayControl = ({
onClick={() => { handleClick() }} onClick={() => { handleClick() }}
> >
<Flex align='center'> <Flex align='center'>
{played ? strings.player.stopVideo : strings.player.startVideo} {played ? t('player.stopVideo') : t('player.startVideo')}
{played ? {played ?
<IconPlayerStopFilled <IconPlayerStopFilled
className={classes.iconStop} /> className={classes.iconStop} />

View File

@ -6,9 +6,9 @@ import { frigateApi, frigateQueryKeys } from '../../../services/frigate.proxy/fr
import CogwheelLoader from '../loaders/CogwheelLoader'; import CogwheelLoader from '../loaders/CogwheelLoader';
import { Center, Loader, Text } from '@mantine/core'; import { Center, Loader, Text } from '@mantine/core';
import OneSelectFilter, { OneSelectItem } from './OneSelectFilter'; import OneSelectFilter, { OneSelectItem } from './OneSelectFilter';
import { strings } from '../../strings/strings';
import RetryError from '../RetryError'; import RetryError from '../RetryError';
import { isProduction } from '../../env.const'; import { isProduction } from '../../env.const';
import { useTranslation } from 'react-i18next';
interface CameraSelectFilterProps { interface CameraSelectFilterProps {
selectedHostId: string, selectedHostId: string,
@ -17,6 +17,7 @@ interface CameraSelectFilterProps {
const CameraSelectFilter = ({ const CameraSelectFilter = ({
selectedHostId, selectedHostId,
}: CameraSelectFilterProps) => { }: CameraSelectFilterProps) => {
const { t } = useTranslation()
const { recordingsStore: recStore } = useContext(Context) const { recordingsStore: recStore } = useContext(Context)
const { data, isError, isPending, isSuccess, refetch } = useQuery({ const { data, isError, isPending, isSuccess, refetch } = useQuery({
@ -53,7 +54,7 @@ const CameraSelectFilter = ({
return ( return (
<OneSelectFilter <OneSelectFilter
id='frigate-cameras' id='frigate-cameras'
label={strings.selectCamera} label={t('selectCamera')}
spaceBetween='1rem' spaceBetween='1rem'
value={recStore.filteredCamera?.id || ''} value={recStore.filteredCamera?.id || ''}
defaultValue={recStore.filteredCamera?.id || ''} defaultValue={recStore.filteredCamera?.id || ''}

View File

@ -2,15 +2,16 @@ import { Box, Flex, Indicator, Text } from '@mantine/core';
import { DatePickerInput } from '@mantine/dates'; import { DatePickerInput } from '@mantine/dates';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { useContext } from 'react'; import { useContext } from 'react';
import { useTranslation } from 'react-i18next';
import { Context } from '../../..'; import { Context } from '../../..';
import { isProduction } from '../../env.const'; import { isProduction } from '../../env.const';
import { strings } from '../../strings/strings';
interface DateRangeSelectFilterProps {} interface DateRangeSelectFilterProps {}
const DateRangeSelectFilter = ({ const DateRangeSelectFilter = ({
}: DateRangeSelectFilterProps) => { }: DateRangeSelectFilterProps) => {
const { t } = useTranslation()
const { recordingsStore: recStore } = useContext(Context) const { recordingsStore: recStore } = useContext(Context)
const handlePick = (value: [Date | null, Date | null]) => { const handlePick = (value: [Date | null, Date | null]) => {
@ -23,7 +24,7 @@ const DateRangeSelectFilter = ({
<Flex <Flex
mt='1rem' mt='1rem'
justify='space-between'> justify='space-between'>
<Text>{strings.selectRange}</Text> <Text>{t('selectRange')}</Text>
</Flex> </Flex>
<DatePickerInput <DatePickerInput
w='100%' w='100%'

View File

@ -1,7 +1,6 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { frigateQueryKeys, frigateApi } from '../../../services/frigate.proxy/frigate.api'; import { frigateQueryKeys, frigateApi } from '../../../services/frigate.proxy/frigate.api';
import { strings } from '../../strings/strings';
import RetryError from '../RetryError'; import RetryError from '../RetryError';
import CogwheelLoader from '../loaders/CogwheelLoader'; import CogwheelLoader from '../loaders/CogwheelLoader';
import OneSelectFilter, { OneSelectItem } from './OneSelectFilter'; import OneSelectFilter, { OneSelectItem } from './OneSelectFilter';

View File

@ -1,6 +1,6 @@
import { SystemProp, SpacingValue, Box, Flex, CloseButton, MultiSelect, SelectItem, MultiSelectProps, Text, Tooltip } from '@mantine/core'; import { Box, Flex, MultiSelect, MultiSelectProps, SelectItem, SpacingValue, SystemProp, Text } from '@mantine/core';
import React, { CSSProperties, useState } from 'react'; import { CSSProperties } from 'react';
import { strings } from '../../strings/strings'; import { useTranslation } from 'react-i18next';
import CloseWithTooltip from '../buttons/CloseWithTooltip'; import CloseWithTooltip from '../buttons/CloseWithTooltip';
interface MultiSelectFilterProps { interface MultiSelectFilterProps {
@ -22,6 +22,7 @@ const MultiSelectFilter = ({
label, defaultValue, textClassName, label, defaultValue, textClassName,
selectProps, display, showClose, changedState, onClose selectProps, display, showClose, changedState, onClose
}: MultiSelectFilterProps) => { }: MultiSelectFilterProps) => {
const { t } = useTranslation()
const handleOnChange = (value: string[]) => { const handleOnChange = (value: string[]) => {
if (changedState) { if (changedState) {
@ -34,7 +35,7 @@ const MultiSelectFilter = ({
<Flex justify='space-between'> <Flex justify='space-between'>
<Text className={textClassName}>{label}</Text> <Text className={textClassName}>{label}</Text>
{showClose ? {showClose ?
<CloseWithTooltip label={strings.hide} onClose={onClose} /> <CloseWithTooltip label={t('hide')} onClose={onClose} />
: null} : null}
</Flex> </Flex>
<MultiSelect <MultiSelect

View File

@ -1,7 +1,6 @@
import { SelectItem, SystemProp, SpacingValue, SelectProps, Box, Flex, CloseButton, Text, Select } from '@mantine/core'; import { Box, Flex, Select, SelectProps, SpacingValue, SystemProp, Text } from '@mantine/core';
import React, { CSSProperties } from 'react'; import { useTranslation } from 'react-i18next';
import CloseWithTooltip from '../buttons/CloseWithTooltip'; import CloseWithTooltip from '../buttons/CloseWithTooltip';
import { strings } from '../../strings/strings';
export interface OneSelectItem { export interface OneSelectItem {
@ -30,6 +29,7 @@ const OneSelectFilter = ({
label, defaultValue, textClassName, label, defaultValue, textClassName,
showClose, value, onChange: onChange, onClose, ...selectProps showClose, value, onChange: onChange, onClose, ...selectProps
}: OneSelectFilterProps) => { }: OneSelectFilterProps) => {
const { t } = useTranslation()
const handleOnChange = (value: string) => { const handleOnChange = (value: string) => {
if (onChange) onChange(value, id,) if (onChange) onChange(value, id,)
@ -44,7 +44,7 @@ const OneSelectFilter = ({
{!label ? null : {!label ? null :
<Flex justify='space-between'> <Flex justify='space-between'>
<Text className={textClassName}>{label}</Text> <Text className={textClassName}>{label}</Text>
{showClose ? <CloseWithTooltip label={strings.hide} onClose={handleOnClose} /> {showClose ? <CloseWithTooltip label={t('hide')} onClose={handleOnClose} />
: null} : null}
</Flex> </Flex>
} }

View File

@ -1,7 +1,7 @@
import { SystemProp, SpacingValue, SliderProps, Box, RangeSlider, RangeSliderProps, Text, Flex, CloseButton } from '@mantine/core'; import { Box, Flex, RangeSlider, RangeSliderProps, SpacingValue, SystemProp, Text } from '@mantine/core';
import React, { CSSProperties, useState } from 'react'; import { CSSProperties } from 'react';
import { useTranslation } from 'react-i18next';
import CloseWithTooltip from '../buttons/CloseWithTooltip'; import CloseWithTooltip from '../buttons/CloseWithTooltip';
import { strings } from '../../strings/strings';
interface SliderFilterProps { interface SliderFilterProps {
id: string id: string
@ -16,13 +16,14 @@ interface SliderFilterProps {
display?: SystemProp<CSSProperties['display']> display?: SystemProp<CSSProperties['display']>
showClose?: boolean, showClose?: boolean,
changedState?(id: string, value: [number, number]): void changedState?(id: string, value: [number, number]): void
onClose?():void onClose?(): void
} }
const RangeSliderFilter = ({ id, min, max, value, spaceBetween, const RangeSliderFilter = ({ id, min, max, value, spaceBetween,
label, defaultValue, textClassName, label, defaultValue, textClassName,
sliderProps, display, showClose, changedState, onClose }: SliderFilterProps) => { sliderProps, display, showClose, changedState, onClose }: SliderFilterProps) => {
const { t } = useTranslation()
const handleOnChange = (value: [number, number]) => { const handleOnChange = (value: [number, number]) => {
if (changedState) { if (changedState) {
changedState(id, value) changedState(id, value)
@ -33,7 +34,7 @@ const RangeSliderFilter = ({ id, min, max, value, spaceBetween,
<Box display={display} mt={spaceBetween}> <Box display={display} mt={spaceBetween}>
<Flex justify='space-between'> <Flex justify='space-between'>
<Text className={textClassName}>{label}</Text> <Text className={textClassName}>{label}</Text>
{showClose? <CloseWithTooltip label={strings.hide} onClose={onClose} /> : null} {showClose ? <CloseWithTooltip label={t('hide')} onClose={onClose} /> : null}
</Flex> </Flex>
<RangeSlider {...sliderProps} value={value} onChangeEnd={handleOnChange} min={min} max={max} defaultValue={defaultValue} pl='1rem' mt='0.5rem' /> <RangeSlider {...sliderProps} value={value} onChangeEnd={handleOnChange} min={min} max={max} defaultValue={defaultValue} pl='1rem' mt='0.5rem' />
</Box> </Box>

View File

@ -3,10 +3,11 @@ import { observer } from 'mobx-react-lite';
import { useContext } from 'react'; import { useContext } from 'react';
import { Context } from '../../..'; import { Context } from '../../..';
import { frigateApi, frigateQueryKeys } from '../../../services/frigate.proxy/frigate.api'; import { frigateApi, frigateQueryKeys } from '../../../services/frigate.proxy/frigate.api';
import { strings } from '../../strings/strings';
import HostSelect from './HostSelect'; import HostSelect from './HostSelect';
import { useTranslation } from 'react-i18next';
const RecordingsHostFilter = () => { const RecordingsHostFilter = () => {
const { t } = useTranslation()
const { recordingsStore: recStore } = useContext(Context) const { recordingsStore: recStore } = useContext(Context)
const { data: hosts } = useQuery({ const { data: hosts } = useQuery({
@ -38,7 +39,7 @@ const RecordingsHostFilter = () => {
return ( return (
<HostSelect <HostSelect
label={strings.selectHost} label={t('selectHost')}
valueId={recStore.filteredHost?.id} valueId={recStore.filteredHost?.id}
defaultId={recStore.filteredHost?.id} defaultId={recStore.filteredHost?.id}
onChange={handleSelect} onChange={handleSelect}

View File

@ -1,7 +1,7 @@
import { Box, CloseButton, Flex, Slider, SliderProps, SpacingValue, SystemProp, Text } from '@mantine/core'; import { Box, Flex, Slider, SliderProps, SpacingValue, SystemProp, Text } from '@mantine/core';
import React, { CSSProperties, useState, } from 'react'; import { CSSProperties } from 'react';
import { useTranslation } from 'react-i18next';
import CloseWithTooltip from '../buttons/CloseWithTooltip'; import CloseWithTooltip from '../buttons/CloseWithTooltip';
import { strings } from '../../strings/strings';
interface SliderFilterProps { interface SliderFilterProps {
id: string id: string
@ -16,11 +16,12 @@ interface SliderFilterProps {
display?: SystemProp<CSSProperties['display']> display?: SystemProp<CSSProperties['display']>
showClose?: boolean, showClose?: boolean,
changedState?(id: string, value: number): void changedState?(id: string, value: number): void
onClose?():void onClose?(): void
} }
const SliderFilter = ({ id, min, max, value, spaceBetween, label, defaultValue, textClassName, sliderProps, display, showClose, changedState, onClose }: SliderFilterProps) => { const SliderFilter = ({ id, min, max, value, spaceBetween, label, defaultValue, textClassName, sliderProps, display, showClose, changedState, onClose }: SliderFilterProps) => {
const { t } = useTranslation()
const handleOnChange = (value: number) => { const handleOnChange = (value: number) => {
if (changedState) { if (changedState) {
@ -32,7 +33,7 @@ const SliderFilter = ({ id, min, max, value, spaceBetween, label, defaultValue,
<Box display={display} mt={spaceBetween}> <Box display={display} mt={spaceBetween}>
<Flex justify='space-between'> <Flex justify='space-between'>
<Text className={textClassName}>{label}</Text> <Text className={textClassName}>{label}</Text>
{showClose ? <CloseWithTooltip label={strings.hide} onClose={onClose} /> : null} {showClose ? <CloseWithTooltip label={t('hide')} onClose={onClose} /> : null}
</Flex> </Flex>
<Slider {...sliderProps} <Slider {...sliderProps}
onChangeEnd={handleOnChange} onChangeEnd={handleOnChange}

View File

@ -1,8 +1,7 @@
import { SystemProp, SpacingValue, Flex, Switch, Text, CloseButton, Group, Box } from '@mantine/core'; import { Box, Flex, SpacingValue, Switch, SystemProp, Text } from '@mantine/core';
import React, { CSSProperties, ChangeEvent } from 'react'; import { CSSProperties, ChangeEvent } from 'react';
import { boolean } from 'zod'; import { useTranslation } from 'react-i18next';
import CloseWithTooltip from '../buttons/CloseWithTooltip'; import CloseWithTooltip from '../buttons/CloseWithTooltip';
import { strings } from '../../strings/strings';
interface SwitchFilterProps { interface SwitchFilterProps {
id: string id: string
@ -14,7 +13,7 @@ interface SwitchFilterProps {
display?: SystemProp<CSSProperties['display']> display?: SystemProp<CSSProperties['display']>
showClose?: boolean showClose?: boolean
changedState?(id: string, value: boolean): void changedState?(id: string, value: boolean): void
onClose?():void onClose?(): void
} }
export interface SwitchChangeState { export interface SwitchChangeState {
@ -23,6 +22,7 @@ export interface SwitchChangeState {
} }
const SwitchFilter = ({ id, value, defaultValue, spaceBetween, label, textClassName, display, showClose, changedState, onClose }: SwitchFilterProps) => { const SwitchFilter = ({ id, value, defaultValue, spaceBetween, label, textClassName, display, showClose, changedState, onClose }: SwitchFilterProps) => {
const { t } = useTranslation()
const handleChange = (event: ChangeEvent<HTMLInputElement> | undefined) => { const handleChange = (event: ChangeEvent<HTMLInputElement> | undefined) => {
const checked = event?.currentTarget.checked const checked = event?.currentTarget.checked
if (changedState && typeof checked === 'boolean') { if (changedState && typeof checked === 'boolean') {
@ -35,7 +35,7 @@ const SwitchFilter = ({ id, value, defaultValue, spaceBetween, label, textClassN
<Flex align='center' w='100%'> <Flex align='center' w='100%'>
<Text className={textClassName}>{label}</Text> <Text className={textClassName}>{label}</Text>
<Switch onChange={handleChange} checked={value} defaultChecked={defaultValue} ml='lg' mr='md' /> <Switch onChange={handleChange} checked={value} defaultChecked={defaultValue} ml='lg' mr='md' />
{showClose ? <CloseWithTooltip label={strings.hide} onClose={onClose} /> : null} {showClose ? <CloseWithTooltip label={t('hide')} onClose={onClose} /> : null}
</Flex> </Flex>
</Box> </Box>
); );

View File

@ -0,0 +1,43 @@
import { CloseButton, TextInput, TextInputProps } from '@mantine/core';
import React, { useState } from 'react';
interface ClearableTextInputProps extends TextInputProps {
clerable?: boolean
}
const ClearableTextInput: React.FC<ClearableTextInputProps> = ({
value,
onChange,
clerable,
...textInputProps
}) => {
const [text, setText] = useState(value)
const handleClear = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
setText(''); // обновляем локальное состояние
if (onChange) {
const fakeEvent = {
target: { value: '' },
currentTarget: { value: '' }
} as unknown as React.ChangeEvent<HTMLInputElement>;
onChange(fakeEvent);
}
}
const handleChange = (value: React.ChangeEvent<HTMLInputElement>) => {
if (onChange) onChange(value)
setText(value.currentTarget.value)
}
return (
<TextInput
rightSection={clerable ? <CloseButton onClick={handleClear} /> : null}
value={text}
onChange={handleChange}
{...textInputProps}
/>
);
};
export default ClearableTextInput;

View File

@ -1,9 +1,9 @@
import { ActionIcon, CloseButton, Flex, Modal, NumberInput, TextInput, Tooltip, createStyles, } from '@mantine/core'; import { ActionIcon, CloseButton, Flex, Modal, NumberInput, TextInput, Tooltip, createStyles, } from '@mantine/core';
import { getHotkeyHandler, useMediaQuery } from '@mantine/hooks'; import { getHotkeyHandler, useMediaQuery } from '@mantine/hooks';
import React, { ReactEventHandler, useState, FocusEvent, useRef, Ref } from 'react'; import React, { ReactEventHandler, useState, FocusEvent, useRef, Ref } from 'react';
import { strings } from '../../strings/strings';
import { IconAlertCircle, IconX } from '@tabler/icons-react'; import { IconAlertCircle, IconX } from '@tabler/icons-react';
import { dimensions } from '../../dimensions/dimensions'; import { dimensions } from '../../dimensions/dimensions';
import { useTranslation } from 'react-i18next';
const useStyles = createStyles((theme) => ({ const useStyles = createStyles((theme) => ({
rightSection: { rightSection: {
@ -21,6 +21,7 @@ interface InputModalProps {
} }
const InputModal = ({ inValue, putValue, opened, open, close }: InputModalProps) => { const InputModal = ({ inValue, putValue, opened, open, close }: InputModalProps) => {
const { t } = useTranslation()
const { classes } = useStyles() const { classes } = useStyles()
const [value, setValue] = useState(inValue) const [value, setValue] = useState(inValue)
const isMobile = useMediaQuery(dimensions.mobileSize) const isMobile = useMediaQuery(dimensions.mobileSize)
@ -54,7 +55,7 @@ const InputModal = ({ inValue, putValue, opened, open, close }: InputModalProps)
fullScreen={isMobile} fullScreen={isMobile}
> >
<Flex justify="space-between"> <Flex justify="space-between">
<div>{strings.enterQuantity}</div> <div>{t('enterQuantity')}</div>
<CloseButton size="lg" onClick={handleClose} /> <CloseButton size="lg" onClick={handleClose} />
</Flex> </Flex>
<NumberInput <NumberInput
@ -64,7 +65,7 @@ const InputModal = ({ inValue, putValue, opened, open, close }: InputModalProps)
value={value} value={value}
onChange={handleSetValue} onChange={handleSetValue}
data-autofocus data-autofocus
placeholder={strings.quantity} placeholder={t('quantity')}
hideControls hideControls
min={0} min={0}
onFocus={handeLoaded} onFocus={handeLoaded}
@ -73,9 +74,9 @@ const InputModal = ({ inValue, putValue, opened, open, close }: InputModalProps)
['Enter', handleClose] ['Enter', handleClose]
]) ])
} }
rightSection={ // value.toString().length > 0 ? <ActionIcon onClick={(event) => handeClear()}><IconX size="1.4rem" /></ActionIcon> : null // todo move to textinput rightSection={
<Flex w='100%' h='100%' justify='right' align='center'> <Flex w='100%' h='100%' justify='right' align='center'>
<Tooltip label={strings.tooltip_close} position="top-end" withArrow> <Tooltip label={t('tooltipСlose')} position="top-end" withArrow>
<div> <div>
<IconAlertCircle size="1.4rem" style={{ display: 'block', opacity: 0.5 }} /> <IconAlertCircle size="1.4rem" style={{ display: 'block', opacity: 0.5 }} />
</div> </div>

View File

@ -1,9 +1,8 @@
// @ts-ignore we know this doesn't have types // @ts-ignore we know this doesn't have types
import JSMpeg from "@cycjimmy/jsmpeg-player"; import JSMpeg from "@cycjimmy/jsmpeg-player";
import { Flex } from "@mantine/core";
import { useViewportSize } from "@mantine/hooks"; import { useViewportSize } from "@mantine/hooks";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { strings } from "../../strings/strings"; import { useTranslation } from "react-i18next";
type JSMpegPlayerProps = { type JSMpegPlayerProps = {
wsUrl: string; wsUrl: string;
@ -18,6 +17,7 @@ const JSMpegPlayer = (
cameraHeight = 800, cameraHeight = 800,
}: JSMpegPlayerProps }: JSMpegPlayerProps
) => { ) => {
const { t } = useTranslation()
const playerRef = useRef<HTMLDivElement>(null); const playerRef = useRef<HTMLDivElement>(null);
const [playerInitialized, setPlayerInitialized] = useState(false) const [playerInitialized, setPlayerInitialized] = useState(false)
@ -28,7 +28,7 @@ const JSMpegPlayer = (
playerRef.current, playerRef.current,
wsUrl, wsUrl,
{}, {},
{protocols: [], audio: false, videoBufferSize: 1024*1024*4} { protocols: [], audio: false, videoBufferSize: 1024 * 1024 * 4 }
); );
const toggleFullscreen = () => { const toggleFullscreen = () => {
@ -54,7 +54,7 @@ const JSMpegPlayer = (
} }
}; };
video.els.canvas.addEventListener('dblclick',toggleFullscreen); video.els.canvas.addEventListener('dblclick', toggleFullscreen);
return () => { return () => {
video.destroy(); video.destroy();
@ -66,8 +66,8 @@ const JSMpegPlayer = (
<div <div
ref={playerRef} ref={playerRef}
key={wsUrl} key={wsUrl}
title={strings.player.doubleClickToFullHint} title={t('player.doubleClickToFullHint')}
style={{width:cameraWidth, height:cameraHeight, maxWidth:maxWidth, maxHeight: maxHeight-100, }} /> style={{ width: cameraWidth, height: cameraHeight, maxWidth: maxWidth, maxHeight: maxHeight - 100, }} />
) )
}; };

View File

@ -1,53 +0,0 @@
import React from 'react';
import { productString } from '../../strings/product.strings';
import SortedTh from './SortedTh';
interface TableHeadProps {
reverseSortDirection: boolean
sortBy: string | null
setSorting: (title: string) => void
}
const ProductsTableHead = ({ reverseSortDirection, sortBy, setSorting }: TableHeadProps) => {
return (
<thead>
<tr>
<SortedTh
title={productString.name}
reversed={reverseSortDirection}
sortedName={sortBy}
onSort={setSorting}
/>
<SortedTh
title={productString.cost}
reversed={reverseSortDirection}
sortedName={sortBy}
onSort={setSorting}
/>
<SortedTh
title={productString.image}
reversed={reverseSortDirection}
sortedName={sortBy}
onSort={setSorting}
textProps={{ w: '6rem', truncate: true }}
/>
<SortedTh
title={productString.qty}
reversed={reverseSortDirection}
sortedName={sortBy}
onSort={setSorting}
textProps={{ w: '1rem', truncate: true }}
/>
<SortedTh
title={productString.buy}
reversed={reverseSortDirection}
sortedName={sortBy}
onSort={setSorting}
/>
</tr>
</thead>
);
};
export default ProductsTableHead;

View File

@ -1,8 +0,0 @@
export const headerMenu = {
home:"На главную",
test:"Тест",
settings:"Настройки",
recordings:"Записи",
acessSettings:" Настройка доступа",
hostsConfig:"Серверы Frigate",
}

View File

@ -1,14 +0,0 @@
export const productString = {
name: "Наименование",
cost: "Цена",
image: "Изображение",
qty: "Количество",
buy: "Купить",
number: "Артикул",
manufactory: "Производитель",
oem: "OEM",
stock: "Наличие",
receiptDate: "Дата поступления",
discount:"Скидка",
parameters: "Характеристики",
}

View File

@ -1,121 +0,0 @@
export const strings = {
host: 'Хост',
hostArr: {
name: 'Имя хоста',
url: 'Адрес',
enabled: 'Включен',
},
player: {
startVideo: 'Вкл Видео',
stopVideo: 'Выкл Видео',
object: 'Объект',
duration: 'Длительность',
startTime: 'Начало',
endTime: 'Конец',
doubleClickToFullHint: 'Двойное нажатие мышью для полноэкранного просмотра',
rating: 'Рейтинг',
},
empty: 'Пусто',
pleaseSelectRole: 'Пожалуйста выберите роль',
nothingHere: 'Ни чего нет',
allowed: 'Разрешено',
notAllowed: 'Не разрешено',
camera: 'Камера',
camersDoesNotExist: 'Камер нет',
search: 'Поиск',
recordings: 'Записи',
hour: 'Час',
minute: 'Минута',
second: 'Секунда',
events: 'События',
event: 'Событие',
notHaveEvents: 'Нет событий',
date: 'Дата',
day: 'День',
selectHost:'Выбери хост',
selectCamera: 'Выбери камеру',
selectDate: 'Выбери дату',
selectRange: 'Выбери период',
aboutMe: "Обо мне",
settings: "Настройки",
changeTheme: "Изменить тему",
logout: "Выйти",
userProfile: "Профиль пользователя",
firstName: "Имя",
middleName: "Фамилия",
lastName: "Отчетство",
taxNumber: "ИНН",
phone: "Телефон",
managerName: "Имя менеджера",
managerPhone: "Телефон менеджера",
defaultContract: "Договор по умолчанию",
myShippingAddresses: "Мои адреса доставки:",
myConditions: "Мои условия:",
warehouse: "Склад",
name: "Имя",
schedule: "Расписание",
address: "Адрес",
edit: "Изменить",
delete: "Удалить",
error: "Ошибка",
discounts: "Скидки",
delivery: "Доставка",
selectDeliveryMethod: "Выберите метод доставки:",
pickUpByMyself: "Я заберу самостоятельно",
courierDelivery: "Доставка курьером",
deliveryPoint: "Адрес доставки",
selectYourDeliveryAddress: "Выберите свой адрес доставки:",
deliveryDate: "Дата доставки",
selectDeliveryDate: "Выберите дату доставки:",
enterQuantity: "Введите количество:",
quantity: "Количество",
tooltip_close: "нажмите Enter",
currency: "₽",
category: "Категории:",
collapse: "Свернуть",
hide: "Скрыть",
show: "Показать",
showAll: "Показать всё",
true: "Да",
false: "Нет",
// order section
cart: "Корзина",
order: "Заказ",
confirmOrder: "Подтвердить заказ",
orderParams: "Параметры",
chooseParams: "Выбрать параметры заказа",
payment: "Оплата",
inputPaymentValues: "Ввести платежные данные",
summary: "Общие итоги",
positions: "Позиции:",
weight: "Вес:",
total: "Итого:",
confirm: "Подтвердить",
save: "Сохранить",
discard: "Отменить",
next: "Далее",
back: "Назад",
paymentMethod: "Метод оплаты",
selectPaymentMethod: "Выберите метод оплаты",
cashToCourier: "Наличными курьеру",
bankTransfer: "Банковским переводом",
onlineByCard: "Онлайн банковской картой",
thanksForYourPurchase: "Спасибо за вашу покупку!",
orderConfirmed: (order: string) => `Ордер ${order} подтвержден!`,
goToMainPage: "Вернуться на главную",
goToOrder: "Посмотреть ордер",
retry: "Повторить",
youCanRetryOrGoToMain: "Вы можете повторить или вернуться на главную",
errors: {
somthengGoesWrong: "Что-то пошло не так",
cartIsEmpty: "Корзина пуста",
choosePaymentMethod: "Выберите метод оплаты",
chooseDeliveryMethod: "Выберите метод доставки",
chooseDeliveryPoint: "Выберите адрес доставки",
chooseDate: "Выберите дату",
403: "Извините у вас нет доступа",
404: "Извините мы не можем найти такую страницу",
}
}

View File

@ -7,7 +7,7 @@ import { routesPath } from '../router/routes.path';
import { mapHostToHostname, proxyApi } from '../services/frigate.proxy/frigate.api'; import { mapHostToHostname, proxyApi } from '../services/frigate.proxy/frigate.api';
import { GetCameraWHostWConfig } from '../services/frigate.proxy/frigate.schema'; import { GetCameraWHostWConfig } from '../services/frigate.proxy/frigate.schema';
import AutoUpdatedImage from '../shared/components/images/AutoUpdatedImage'; import AutoUpdatedImage from '../shared/components/images/AutoUpdatedImage';
import { strings } from '../shared/strings/strings'; import { useTranslation } from 'react-i18next';
const useStyles = createStyles((theme) => ({ const useStyles = createStyles((theme) => ({
mainCard: { mainCard: {
@ -41,6 +41,7 @@ interface CameraCardProps {
const CameraCard = ({ const CameraCard = ({
camera camera
}: CameraCardProps) => { }: CameraCardProps) => {
const { t } = useTranslation()
const [renderImage, setRenderImage] = useState<boolean>(false) const [renderImage, setRenderImage] = useState<boolean>(false)
const { classes } = useStyles(); const { classes } = useStyles();
const { ref, entry } = useIntersection({ threshold: 0.5, }) const { ref, entry } = useIntersection({ threshold: 0.5, })
@ -71,7 +72,7 @@ const CameraCard = ({
<Group <Group
className={classes.bottomGroup}> className={classes.bottomGroup}>
<Flex justify='space-evenly' mt='0.5rem' w='100%'> <Flex justify='space-evenly' mt='0.5rem' w='100%'>
<Button size='sm' onClick={handleOpenRecordings}>{strings.recordings}</Button> <Button size='sm' onClick={handleOpenRecordings}>{t('recordings')}</Button>
</Flex> </Flex>
</Group> </Group>
</Card> </Card>

View File

@ -7,10 +7,10 @@ import { GetFrigateHost } from '../services/frigate.proxy/frigate.schema';
import HostSettingsMenu from '../shared/components/menu/HostSettingsMenu'; import HostSettingsMenu from '../shared/components/menu/HostSettingsMenu';
import SortedTh from '../shared/components/table.aps/SortedTh'; import SortedTh from '../shared/components/table.aps/SortedTh';
import { isProduction } from '../shared/env.const'; import { isProduction } from '../shared/env.const';
import { strings } from '../shared/strings/strings';
import StateCell from './hosts.table/StateCell'; 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 { useTranslation } from 'react-i18next';
interface TableProps<T> { interface TableProps<T> {
data: T[], data: T[],
@ -20,6 +20,8 @@ interface TableProps<T> {
} }
const FrigateHostsTable = ({ data, showAddButton = false, saveCallback, changedCallback }: TableProps<GetFrigateHost>) => { const FrigateHostsTable = ({ data, showAddButton = false, saveCallback, changedCallback }: TableProps<GetFrigateHost>) => {
const { t } = useTranslation()
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)
@ -59,9 +61,9 @@ const FrigateHostsTable = ({ data, showAddButton = false, saveCallback, changedC
} }
const headTitle = [ const headTitle = [
{ propertyName: 'name', title: strings.hostArr.name }, { propertyName: 'name', title: t('hostArr.host') },
{ propertyName: 'host', title: strings.hostArr.url }, { propertyName: 'host', title: t('hostArr.url') },
{ propertyName: 'enabled', title: strings.hostArr.enabled }, { propertyName: 'enabled', title: t('hostArr.enabled') },
{ title: '', sorting: false }, { title: '', sorting: false },
] ]

View File

@ -5,8 +5,8 @@ import { frigateQueryKeys, frigateApi } from '../services/frigate.proxy/frigate.
import { Context } from '..'; import { Context } from '..';
import CenterLoader from '../shared/components/loaders/CenterLoader'; import CenterLoader from '../shared/components/loaders/CenterLoader';
import RetryErrorPage from '../pages/RetryErrorPage'; import RetryErrorPage from '../pages/RetryErrorPage';
import { strings } from '../shared/strings/strings';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { useTranslation } from 'react-i18next';
const CameraAccordion = lazy(() => import('../shared/components/accordion/CameraAccordion')); const CameraAccordion = lazy(() => import('../shared/components/accordion/CameraAccordion'));
interface SelectedHostListProps { interface SelectedHostListProps {
@ -16,7 +16,7 @@ interface SelectedHostListProps {
const SelectedHostList = ({ const SelectedHostList = ({
hostId hostId
}: SelectedHostListProps) => { }: SelectedHostListProps) => {
const { t } = useTranslation()
const { recordingsStore: recStore } = useContext(Context) const { recordingsStore: recStore } = useContext(Context)
const [openCameraId, setOpenCameraId] = useState<string | null>(null) const [openCameraId, setOpenCameraId] = useState<string | null>(null)
@ -47,7 +47,7 @@ const SelectedHostList = ({
const camerasItems = camerasQuery.map(camera => { const camerasItems = camerasQuery.map(camera => {
return ( return (
<Accordion.Item key={camera.id + 'Item'} value={camera.id}> <Accordion.Item key={camera.id + 'Item'} value={camera.id}>
<Accordion.Control key={camera.id + 'Control'}>{strings.camera}: {camera.name}</Accordion.Control> <Accordion.Control key={camera.id + 'Control'}>{t('camera')}: {camera.name}</Accordion.Control>
<Accordion.Panel key={camera.id + 'Panel'}> <Accordion.Panel key={camera.id + 'Panel'}>
{openCameraId === camera.id && ( {openCameraId === camera.id && (
<Suspense> <Suspense>
@ -61,7 +61,7 @@ const SelectedHostList = ({
return ( return (
<Flex w='100%' h='100%' direction='column' align='center'> <Flex w='100%' h='100%' direction='column' align='center'>
<Text>{strings.host}: {camerasQuery[0].frigateHost?.name}</Text> <Text>{t('hostArr.host')}: {camerasQuery[0].frigateHost?.name}</Text>
<Accordion <Accordion
mt='1rem' mt='1rem'
variant='separated' variant='separated'

View File

@ -122,15 +122,6 @@ const VideoDownloader = ({
} }
// TODO delete
// const handleCancel = () => {
// clearTimeout(timer)
// setTimer(undefined)
// setCreateName(undefined)
// setLink(undefined)
// }
if (startUnixTime === 0 || endUnixTime === 0) return null if (startUnixTime === 0 || endUnixTime === 0) return null
if (error) return <RetryError onRetry={() => createVideo.mutate()} /> if (error) return <RetryError onRetry={() => createVideo.mutate()} />
if (link && progress && !videoSrc) return ( if (link && progress && !videoSrc) return (

View File

@ -1,11 +0,0 @@
import { routesPath } from "../../router/routes.path";
import { headerMenu } from "../../shared/strings/header.menu.strings";
import { LinkItem } from "./HeaderAction";
export const headerLinks: LinkItem[] = [
{ link: routesPath.MAIN_PATH, label: headerMenu.home },
{ link: routesPath.SETTINGS_PATH, label: headerMenu.settings, admin: true },
{ link: routesPath.RECORDINGS_PATH, label: headerMenu.recordings },
{ link: routesPath.HOSTS_PATH, label: headerMenu.hostsConfig, admin: true },
{ link: routesPath.ACCESS_PATH, label: headerMenu.acessSettings, admin: true },
]

View File

@ -1122,7 +1122,7 @@
resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310"
integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==
"@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.16.3", "@babel/runtime@^7.18.3", "@babel/runtime@^7.23.2", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.16.3", "@babel/runtime@^7.18.3", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.9", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
version "7.24.0" version "7.24.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.0.tgz#584c450063ffda59697021430cb47101b085951e" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.0.tgz#584c450063ffda59697021430cb47101b085951e"
integrity sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw== integrity sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==
@ -5684,6 +5684,13 @@ html-minifier-terser@^6.0.2:
relateurl "^0.2.7" relateurl "^0.2.7"
terser "^5.10.0" terser "^5.10.0"
html-parse-stringify@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz#dfc1017347ce9f77c8141a507f233040c59c55d2"
integrity sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==
dependencies:
void-elements "3.1.0"
html-webpack-plugin@^5.5.0: html-webpack-plugin@^5.5.0:
version "5.6.0" version "5.6.0"
resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.6.0.tgz#50a8fa6709245608cb00e811eacecb8e0d7b7ea0" resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.6.0.tgz#50a8fa6709245608cb00e811eacecb8e0d7b7ea0"
@ -5778,6 +5785,20 @@ human-signals@^2.1.0:
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
i18next-browser-languagedetector@^7.2.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.2.0.tgz#de0321cba6881be37d82e20e4d6f05aa75f6e37f"
integrity sha512-U00DbDtFIYD3wkWsr2aVGfXGAj2TgnELzOX9qv8bT0aJtvPV9CRO77h+vgmHFBMe7LAxdwvT/7VkCWGya6L3tA==
dependencies:
"@babel/runtime" "^7.23.2"
i18next@^23.10.1:
version "23.10.1"
resolved "https://registry.yarnpkg.com/i18next/-/i18next-23.10.1.tgz#217ce93b75edbe559ac42be00a20566b53937df6"
integrity sha512-NDiIzFbcs3O9PXpfhkjyf7WdqFn5Vq6mhzhtkXzj51aOcNuPNcTwuYNuXCpHsanZGHlHKL35G7huoFeVic1hng==
dependencies:
"@babel/runtime" "^7.23.2"
iconv-lite@0.4.24: iconv-lite@0.4.24:
version "0.4.24" version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
@ -8647,6 +8668,14 @@ react-error-overlay@^6.0.11:
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb"
integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg== integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==
react-i18next@^14.1.0:
version "14.1.0"
resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-14.1.0.tgz#44da74fbffd416f5d0c5307ef31735cf10cc91d9"
integrity sha512-3KwX6LHpbvGQ+sBEntjV4sYW3Zovjjl3fpoHbUwSgFHf0uRBcbeCBLR5al6ikncI5+W0EFb71QXZmfop+J6NrQ==
dependencies:
"@babel/runtime" "^7.23.9"
html-parse-stringify "^3.0.1"
react-is@^16.13.1, react-is@^16.7.0: react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1" version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
@ -10271,6 +10300,11 @@ videojs-vtt.js@0.15.5:
dependencies: dependencies:
global "^4.3.1" global "^4.3.1"
void-elements@3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09"
integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==
vscode-jsonrpc@8.2.0: vscode-jsonrpc@8.2.0:
version "8.2.0" version "8.2.0"
resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz#f43dfa35fb51e763d17cd94dcca0c9458f35abf9" resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz#f43dfa35fb51e763d17cd94dcca0c9458f35abf9"