add locales
This commit is contained in:
parent
acaf99c878
commit
edf8bf7bc9
@ -1,6 +1,6 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
# Build commands:
|
||||
# - $VERSION=0.6
|
||||
# - $VERSION=0.7
|
||||
# - rm build -r -Force ; rm ./node_modules/.cache/babel-loader -r -Force ; yarn build
|
||||
# - docker build --pull --rm -t oncharterliz/multi-frigate:latest -t oncharterliz/multi-frigate:$VERSION "."
|
||||
# - docker image push --all-tags oncharterliz/multi-frigate
|
||||
|
||||
@ -27,6 +27,8 @@
|
||||
"date-fns": "^3.3.1",
|
||||
"dayjs": "^1.11.9",
|
||||
"embla-carousel-react": "^8.0.0-rc10",
|
||||
"i18next": "^23.10.1",
|
||||
"i18next-browser-languagedetector": "^7.2.0",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"mantine-react-table": "^1.0.0-beta.25",
|
||||
@ -39,6 +41,7 @@
|
||||
"react": "^18.2.0",
|
||||
"react-device-detect": "^2.2.3",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^14.1.0",
|
||||
"react-oidc-context": "^2.2.2",
|
||||
"react-router-dom": "^6.14.1",
|
||||
"react-scripts": "5.0.1",
|
||||
|
||||
@ -62,6 +62,7 @@ function App() {
|
||||
urlParams.delete('state');
|
||||
urlParams.delete('session_state');
|
||||
urlParams.delete('code');
|
||||
urlParams.delete('iss');
|
||||
navigate(`${location.pathname}${urlParams.toString() ? '?' + urlParams.toString() : ''}`, { replace: true })
|
||||
}
|
||||
|
||||
|
||||
@ -1,14 +1,24 @@
|
||||
import React, { useContext, useState } from 'react';
|
||||
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 { AppShell, useMantineTheme, } from "@mantine/core";
|
||||
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 { HeaderAction } from './widgets/header/HeaderAction';
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ import RootStore from './shared/stores/root.store';
|
||||
import { AuthProvider, AuthProviderProps } from 'react-oidc-context';
|
||||
import { isProduction, oidpSettings } from './shared/env.const';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import './services/i18n';
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
@ -23,6 +24,7 @@ export const keycloakConfig: AuthProviderProps = {
|
||||
params.delete('state');
|
||||
params.delete('session_state');
|
||||
params.delete('code');
|
||||
params.delete('iss');
|
||||
const newUrl = `${window.location.pathname}${params.toString() ? '?' + params.toString() : ''}`
|
||||
window.history.replaceState({}, document.title, newUrl)
|
||||
}
|
||||
|
||||
63
src/locales/en.ts
Normal file
63
src/locales/en.ts
Normal 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
64
src/locales/ru.ts
Normal 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
|
||||
@ -1,12 +1,13 @@
|
||||
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 { 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 { t } = useTranslation()
|
||||
const executed = useRef(false)
|
||||
const { sideBarsStore } = useContext(Context)
|
||||
|
||||
@ -25,9 +26,9 @@ const Forbidden = () => {
|
||||
|
||||
return (
|
||||
<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' />
|
||||
<Button onClick={handleGoToMain}>{strings.goToMainPage}</Button>
|
||||
<Button onClick={handleGoToMain}>{t('goToMainPage')}</Button>
|
||||
</Flex>
|
||||
|
||||
);
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
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 { useTranslation } from 'react-i18next';
|
||||
|
||||
const NotFound = () => {
|
||||
const { t } = useTranslation()
|
||||
const executed = useRef(false)
|
||||
const { sideBarsStore } = useContext(Context)
|
||||
|
||||
@ -25,9 +26,9 @@ const NotFound = () => {
|
||||
|
||||
return (
|
||||
<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' />
|
||||
<Button onClick={handleGoToMain}>{strings.goToMainPage}</Button>
|
||||
<Button onClick={handleGoToMain}>{t('goToMainPage')}</Button>
|
||||
</Flex>
|
||||
|
||||
);
|
||||
|
||||
@ -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 { OneSelectItem } from '../shared/components/filters/OneSelectFilter';
|
||||
import { useMediaQuery } from '@mantine/hooks';
|
||||
import { dimensions } from '../shared/dimensions/dimensions';
|
||||
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 { useQuery } from '@tanstack/react-query';
|
||||
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 Forbidden from './403';
|
||||
import RetryErrorPage from './RetryErrorPage';
|
||||
|
||||
const AccessSettings = () => {
|
||||
const { t } = useTranslation()
|
||||
const executed = useRef(false)
|
||||
const { data, isPending, isError, refetch } = useQuery({
|
||||
queryKey: [frigateQueryKeys.getRoles],
|
||||
@ -49,7 +50,7 @@ const AccessSettings = () => {
|
||||
if (!isProduction) console.log('AccessSettings rendered')
|
||||
return (
|
||||
<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%'>
|
||||
{!isMobile ? <Group w='40%' /> : <></>}
|
||||
<Select
|
||||
|
||||
@ -1,22 +1,24 @@
|
||||
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 { 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 { GetFrigateHost, deleteFrigateHostSchema, putFrigateHostSchema } from '../services/frigate.proxy/frigate.schema';
|
||||
import CenterLoader from '../shared/components/loaders/CenterLoader';
|
||||
import { isProduction } from '../shared/env.const';
|
||||
import { strings } from '../shared/strings/strings';
|
||||
import FrigateHostsTable from '../widgets/FrigateHostsTable';
|
||||
import Forbidden from './403';
|
||||
import RetryErrorPage from './RetryErrorPage';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { IconAlertCircle } from '@tabler/icons-react';
|
||||
|
||||
|
||||
const FrigateHostsPage = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const executed = useRef(false)
|
||||
const queryClient = useQueryClient()
|
||||
const { isPending: hostsPending, error: hostsError, data } = useQuery({
|
||||
@ -106,8 +108,8 @@ const FrigateHostsPage = () => {
|
||||
<Flex w='100%' h='100%' direction='column'>
|
||||
<FrigateHostsTable data={pageData} showAddButton changedCallback={handleChange} />
|
||||
<Flex justify='center'>
|
||||
<Button m='0.5rem' onClick={handleDiscard}>{strings.discard}</Button>
|
||||
<Button m='0.5rem' onClick={handleSave}>{strings.save}</Button>
|
||||
<Button m='0.5rem' onClick={handleDiscard}>{t('discard')}</Button>
|
||||
<Button m='0.5rem' onClick={handleSave}>{t('save')}</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@ -8,11 +8,12 @@ import CenterLoader from '../shared/components/loaders/CenterLoader';
|
||||
import RetryErrorPage from './RetryErrorPage';
|
||||
import Player from '../widgets/Player';
|
||||
import { Button, Flex, Text } from '@mantine/core';
|
||||
import { strings } from '../shared/strings/strings';
|
||||
import { routesPath } from '../router/routes.path';
|
||||
import { recordingsPageQuery } from './RecordingsPage';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const LiveCameraPage = () => {
|
||||
const { t } = useTranslation()
|
||||
const executed = useRef(false)
|
||||
const navigate = useNavigate()
|
||||
let { id: cameraId } = useParams<'id'>()
|
||||
@ -48,9 +49,9 @@ const LiveCameraPage = () => {
|
||||
return (
|
||||
<Flex w='100%' h='100%' justify='center' align='center' direction='column'>
|
||||
<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 ? <></> :
|
||||
<Button onClick={handleOpenRecordings}>{strings.recordings}</Button>
|
||||
<Button onClick={handleOpenRecordings}>{t('recordings')}</Button>
|
||||
}
|
||||
</Flex>
|
||||
<Player camera={camera} />
|
||||
|
||||
@ -8,11 +8,13 @@ import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.
|
||||
import { GetCameraWHostWConfig } from '../services/frigate.proxy/frigate.schema';
|
||||
import HostSelect from '../shared/components/filters/HostSelect';
|
||||
import CenterLoader from '../shared/components/loaders/CenterLoader';
|
||||
import { strings } from '../shared/strings/strings';
|
||||
import CameraCard from '../widgets/CameraCard';
|
||||
import RetryErrorPage from './RetryErrorPage';
|
||||
import ClearableTextInput from '../shared/components/inputs/ClearableTextInput';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const MainPage = () => {
|
||||
const { t } = useTranslation()
|
||||
const executed = useRef(false)
|
||||
const { sideBarsStore } = useContext(Context)
|
||||
const [searchQuery, setSearchQuery] = useState<string>()
|
||||
@ -88,10 +90,11 @@ const MainPage = () => {
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<TextInput
|
||||
<ClearableTextInput
|
||||
clerable
|
||||
maw={400}
|
||||
style={{ flexGrow: 1 }}
|
||||
placeholder={strings.search}
|
||||
placeholder={t('search')}
|
||||
icon={<IconSearch size="0.9rem" stroke={1.5} />}
|
||||
value={searchQuery}
|
||||
onChange={(event) => setSearchQuery(event.currentTarget.value)}
|
||||
@ -101,7 +104,7 @@ const MainPage = () => {
|
||||
onChange={handleSelectHost}
|
||||
ml='1rem'
|
||||
spaceBetween='0px'
|
||||
placeholder={strings.selectHost}
|
||||
placeholder={t('selectHost')}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
@ -10,6 +10,7 @@ import RecordingsFiltersRightSide from '../widgets/RecordingsFiltersRightSide';
|
||||
import SelectedCameraList from '../widgets/SelectedCameraList';
|
||||
import SelectedDayList from '../widgets/SelectedDayList';
|
||||
import SelectedHostList from '../widgets/SelectedHostList';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
|
||||
export const recordingsPageQuery = {
|
||||
@ -21,6 +22,7 @@ export const recordingsPageQuery = {
|
||||
}
|
||||
|
||||
const RecordingsPage = () => {
|
||||
const { t } = useTranslation()
|
||||
const executed = useRef(false)
|
||||
const { sideBarsStore, recordingsStore: recStore } = useContext(Context)
|
||||
|
||||
@ -116,10 +118,10 @@ const RecordingsPage = () => {
|
||||
return (
|
||||
<Flex w='100%' h='100%' direction='column' justify='center' align='center'>
|
||||
{!hostId ?
|
||||
<Text size='xl'>Please select host</Text>
|
||||
<Text size='xl'>{t('pleaseSelectHost')}</Text>
|
||||
: <></>}
|
||||
{hostId && !(startDay && endDay) ?
|
||||
<Text size='xl'>Please select date</Text>
|
||||
<Text size='xl'>{t('pleaseSelectDate')}</Text>
|
||||
: <></>
|
||||
}
|
||||
</Flex>
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { Flex, Button, Text } from '@mantine/core';
|
||||
import React, { useContext, useEffect, useRef } from 'react';
|
||||
import { routesPath } from '../router/routes.path';
|
||||
import { strings } from '../shared/strings/strings';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { ExclamationCogWheel } from '../shared/components/svg/ExclamationCogWheel';
|
||||
import { Context } from '..';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface RetryErrorPageProps {
|
||||
repeatVisible?: boolean
|
||||
@ -20,6 +20,7 @@ const RetryErrorPage = ({
|
||||
mainVisible = true,
|
||||
onRetry
|
||||
}: RetryErrorPageProps) => {
|
||||
const { t } = useTranslation()
|
||||
const executed = useRef(false)
|
||||
|
||||
const navigate = useNavigate()
|
||||
@ -49,13 +50,13 @@ const RetryErrorPage = ({
|
||||
|
||||
return (
|
||||
<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}
|
||||
<Text fz='lg' fw={700}>{strings.youCanRetryOrGoToMain}</Text>
|
||||
<Text fz='lg' fw={700}>{t('youCanRetryOrGoToMain')}</Text>
|
||||
<Flex>
|
||||
{repeatVisible ? <Button ml='1rem' onClick={handleRetry}>{strings.retry}</Button> : null}
|
||||
{ backVisible ? <Button ml='1rem' onClick={handleGoBack}>{strings.back}</Button> : null }
|
||||
{ mainVisible ? <Button ml='1rem' onClick={handleGoToMain}>{strings.goToMainPage}</Button> : null }
|
||||
{repeatVisible ? <Button ml='1rem' onClick={handleRetry}>{t('retry')}</Button> : null}
|
||||
{ backVisible ? <Button ml='1rem' onClick={handleGoBack}>{t('back')}</Button> : null }
|
||||
{ mainVisible ? <Button ml='1rem' onClick={handleGoToMain}>{t('goToMainPage')}</Button> : null }
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@ -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 { FloatingLabelInput } from '../shared/components/inputs/FloatingLabelInput';
|
||||
import { strings } from '../shared/strings/strings';
|
||||
import { dimensions } from '../shared/dimensions/dimensions';
|
||||
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 { useAdminRole } from '../hooks/useAdminRole';
|
||||
import Forbidden from './403';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
|
||||
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 Forbidden from './403';
|
||||
import RetryErrorPage from './RetryErrorPage';
|
||||
|
||||
const SettingsPage = () => {
|
||||
const { t } = useTranslation()
|
||||
const executed = useRef(false)
|
||||
const queryClient = useQueryClient()
|
||||
const { isPending: configPending, error: configError, data, refetch } = useQuery({
|
||||
@ -126,8 +127,8 @@ const SettingsPage = () => {
|
||||
))}
|
||||
<Space h='2%' />
|
||||
<Flex w='100%' justify='stretch' wrap='nowrap' align='center'>
|
||||
<Button w='100%' onClick={handleDiscard} m='0.5rem'>{strings.discard}</Button>
|
||||
<Button w='100%' type="submit" m='0.5rem'>{strings.confirm}</Button>
|
||||
<Button w='100%' onClick={handleDiscard} m='0.5rem'>{t('discard')}</Button>
|
||||
<Button w='100%' type="submit" m='0.5rem'>{t('confirm')}</Button>
|
||||
</Flex>
|
||||
</form>
|
||||
</Flex>
|
||||
|
||||
@ -21,7 +21,7 @@ export const getToken = (): string | undefined => {
|
||||
|
||||
const instanceApi = axios.create({
|
||||
baseURL: proxyURL.toString(),
|
||||
timeout: 60 * 1000,
|
||||
timeout: 20 * 1000,
|
||||
})
|
||||
|
||||
instanceApi.interceptors.request.use(
|
||||
|
||||
21
src/services/i18n.ts
Normal file
21
src/services/i18n.ts
Normal 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
|
||||
@ -1,12 +1,11 @@
|
||||
import { Button, Flex, Text, TransferList, TransferListData, TransferListItem } from '@mantine/core';
|
||||
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 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 RetryError from './RetryError';
|
||||
import CogwheelLoader from './loaders/CogwheelLoader';
|
||||
|
||||
interface CamerasTransferListProps {
|
||||
roleId: string
|
||||
@ -15,6 +14,7 @@ interface CamerasTransferListProps {
|
||||
const CamerasTransferList = ({
|
||||
roleId,
|
||||
}: CamerasTransferListProps) => {
|
||||
const { t } = useTranslation()
|
||||
const queryClient = useQueryClient()
|
||||
const { data: cameras, isPending, isError, refetch } = useQuery({
|
||||
queryKey: [frigateQueryKeys.getCamerasWHost, roleId],
|
||||
@ -50,7 +50,7 @@ const CamerasTransferList = ({
|
||||
|
||||
if (isPending) return <CogwheelLoader />
|
||||
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 = () => {
|
||||
@ -65,8 +65,8 @@ const CamerasTransferList = ({
|
||||
return (
|
||||
<>
|
||||
<Flex w='100%' justify='center'>
|
||||
<Button mt='1rem' miw='6rem' mr='1rem' onClick={handleDiscard}>{strings.discard}</Button>
|
||||
<Button mt='1rem' miw='5rem' onClick={handleSave}>{strings.save}</Button>
|
||||
<Button mt='1rem' miw='6rem' mr='1rem' onClick={handleDiscard}>{t('discard')}</Button>
|
||||
<Button mt='1rem' miw='5rem' onClick={handleSave}>{t('save')}</Button>
|
||||
</Flex>
|
||||
<TransferList
|
||||
transferAllMatchingFilter
|
||||
@ -74,9 +74,9 @@ const CamerasTransferList = ({
|
||||
mt='1rem'
|
||||
value={lists}
|
||||
onChange={handleChange}
|
||||
searchPlaceholder={strings.search}
|
||||
nothingFound={strings.nothingHere}
|
||||
titles={[strings.notAllowed, strings.allowed]}
|
||||
searchPlaceholder={t('search')}
|
||||
nothingFound={t('nothingHere')}
|
||||
titles={[t('notAllowed'), t('allowed')]}
|
||||
breakpoint="sm"
|
||||
/>
|
||||
</>
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { Aside, Button, Navbar, createStyles } from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
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 { dimensions } from '../dimensions/dimensions';
|
||||
import { strings } from '../strings/strings';
|
||||
import { useMantineSize } from '../utils/mantine.size.convertor';
|
||||
import { SideButton } from './SideButton';
|
||||
|
||||
@ -29,6 +29,7 @@ const useStyles = createStyles((theme,
|
||||
|
||||
|
||||
const SideBar = ({ isHidden, side, children }: SideBarProps) => {
|
||||
const { t } = useTranslation()
|
||||
const hideSizePx = useMantineSize(dimensions.hideSidebarsSize)
|
||||
const initialVisible = () => {
|
||||
const savedVisibility = localStorage.getItem(`sidebarVisible_${side}`);
|
||||
@ -97,7 +98,7 @@ const SideBar = ({ isHidden, side, children }: SideBarProps) => {
|
||||
p={dimensions.hideSidebarsSize}
|
||||
width={{ sm: 200, lg: 300, }}
|
||||
>
|
||||
<Button onClick={() => handleClickVisible(false)}>{strings.hide}</Button>
|
||||
<Button onClick={() => handleClickVisible(false)}>{t('hide')}</Button>
|
||||
{leftChildren}
|
||||
</Navbar>
|
||||
:
|
||||
@ -105,7 +106,7 @@ const SideBar = ({ isHidden, side, children }: SideBarProps) => {
|
||||
className={classes.aside}
|
||||
p={dimensions.hideSidebarsSize}
|
||||
width={{ sm: 200, lg: 300 }}>
|
||||
<Button onClick={() => handleClickVisible(false)}>{strings.hide}</Button>
|
||||
<Button onClick={() => handleClickVisible(false)}>{t('hide')}</Button>
|
||||
{rightChildren}
|
||||
</Aside>
|
||||
}
|
||||
|
||||
@ -1,17 +1,25 @@
|
||||
import React from 'react';
|
||||
import { Avatar, Group, Menu, Text, Button, Flex } from "@mantine/core";
|
||||
import { useAuth } from 'react-oidc-context';
|
||||
import { strings } from '../strings/strings';
|
||||
import { Avatar, Button, Flex, Group, Menu, Text } from "@mantine/core";
|
||||
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 ColorSchemeToggle from './buttons/ColorSchemeToggle';
|
||||
import { keycloakConfig } from '../..';
|
||||
|
||||
interface UserMenuProps {
|
||||
user: { name: string; image: string }
|
||||
}
|
||||
|
||||
const UserMenu = ({ user }: UserMenuProps) => {
|
||||
|
||||
const { t, i18n } = useTranslation()
|
||||
|
||||
const languages = [
|
||||
{ lng: 'en', name: 'Eng' },
|
||||
{ lng: 'ru', name: 'Rus' },
|
||||
]
|
||||
|
||||
const auth = useAuth()
|
||||
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 })
|
||||
}
|
||||
|
||||
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 (
|
||||
<Menu
|
||||
width={260}
|
||||
@ -43,20 +69,17 @@ const UserMenu = ({ user }: UserMenuProps) => {
|
||||
{
|
||||
isMiddleScreen ?
|
||||
<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 />
|
||||
</Flex>
|
||||
:
|
||||
<></>
|
||||
}
|
||||
{/* <Menu.Item>
|
||||
{strings.settings}
|
||||
</Menu.Item>
|
||||
<Menu.Item onClick={handleAboutMe}>
|
||||
{strings.aboutMe}
|
||||
</Menu.Item> */}
|
||||
{
|
||||
languageSelector()
|
||||
}
|
||||
<Menu.Item onClick={handleLogout}>
|
||||
{strings.logout}
|
||||
{t('logout')}
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
|
||||
@ -1,17 +1,18 @@
|
||||
import { Accordion, Center, Loader } from '@mantine/core';
|
||||
import React, { useContext, useMemo } from 'react';
|
||||
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 { useContext, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Context } from '../../..';
|
||||
import { getResolvedTimeZone, parseQueryDateToDate } from '../../utils/dateUtil';
|
||||
import RetryError from '../RetryError';
|
||||
import { strings } from '../../strings/strings';
|
||||
import { frigateQueryKeys, mapHostToHostname, proxyApi } from '../../../services/frigate.proxy/frigate.api';
|
||||
import { RecordSummary } from '../../../types/record';
|
||||
import { isProduction } from '../../env.const';
|
||||
import { getResolvedTimeZone, parseQueryDateToDate } from '../../utils/dateUtil';
|
||||
import RetryError from '../RetryError';
|
||||
import DayAccordion from './DayAccordion';
|
||||
|
||||
const CameraAccordion = () => {
|
||||
const { t } = useTranslation()
|
||||
const { recordingsStore: recStore } = useContext(Context)
|
||||
|
||||
const camera = recStore.openedCamera || recStore.filteredCamera
|
||||
@ -30,7 +31,7 @@ const CameraAccordion = () => {
|
||||
|
||||
const recodItem = (record: RecordSummary) => (
|
||||
<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'}>
|
||||
<DayAccordion key={record.day + 'day'} recordSummary={record} />
|
||||
</Accordion.Panel>
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import { Accordion, Flex, Group, Text } from '@mantine/core';
|
||||
import { IconExternalLink } from '@tabler/icons-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { routesPath } from '../../../router/routes.path';
|
||||
import { proxyApi } from '../../../services/frigate.proxy/frigate.api';
|
||||
import { RecordHour, RecordSummary } from '../../../types/record';
|
||||
import { isProduction } from '../../env.const';
|
||||
import { strings } from '../../strings/strings';
|
||||
import { getResolvedTimeZone, mapDateHourToUnixTime } from '../../utils/dateUtil';
|
||||
import AccordionControlButton from '../buttons/AccordionControlButton';
|
||||
import AccordionShareButton from '../buttons/AccordionShareButton';
|
||||
@ -30,6 +30,7 @@ const DayAccordionItem = ({
|
||||
played,
|
||||
openPlayer
|
||||
}: DayAccordionItemProps) => {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const [playedURL, setPlayedUrl] = useState<string>()
|
||||
|
||||
@ -72,11 +73,11 @@ const DayAccordionItem = ({
|
||||
<Accordion.Control key={hour + 'Control'}>
|
||||
<Flex justify='space-between'>
|
||||
<Group>
|
||||
<Text>{strings.hour}: {hour}:00</Text>
|
||||
<Text>{t('hour')}: {hour}:00</Text>
|
||||
{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>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Accordion, Text } from '@mantine/core';
|
||||
import React, { Suspense, lazy, useState } from 'react';
|
||||
import { strings } from '../../strings/strings';
|
||||
import { Suspense, lazy, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
const EventsAccordion = lazy(() => import('./EventsAccordion'))
|
||||
|
||||
interface DayEventsAccordionProps {
|
||||
@ -14,6 +14,7 @@ const DayEventsAccordion = ({
|
||||
hour,
|
||||
qty,
|
||||
}: DayEventsAccordionProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [openedItem, setOpenedItem] = useState<string>()
|
||||
|
||||
const handleClick = (value: string | null) => {
|
||||
@ -22,7 +23,7 @@ const DayEventsAccordion = ({
|
||||
return (
|
||||
<Accordion onChange={handleClick}>
|
||||
<Accordion.Item value={hour}>
|
||||
<Accordion.Control><Text>{strings.events}: {qty}</Text></Accordion.Control>
|
||||
<Accordion.Control><Text>{t('events')}: {qty}</Text></Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
{openedItem === hour ?
|
||||
<Suspense>
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { Accordion, Center, Flex, Text } from '@mantine/core';
|
||||
import VideoDownloader from '../../../widgets/VideoDownloader';
|
||||
import { strings } from '../../strings/strings';
|
||||
import VideoPlayer from '../players/VideoPlayer';
|
||||
import DayEventsAccordion from './DayEventsAccordion';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface DayPanelProps {
|
||||
day: string,
|
||||
@ -27,6 +27,7 @@ const DayPanel = ({
|
||||
startUnixTime,
|
||||
endUnixTime,
|
||||
}: DayPanelProps) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<Accordion.Panel key={hour + 'Panel'}>
|
||||
{playedURL && playedURL === videoURL ? <VideoPlayer videoUrl={playedURL} /> : <></>}
|
||||
@ -43,7 +44,7 @@ const DayPanel = ({
|
||||
{events > 0 ?
|
||||
<DayEventsAccordion day={day} hour={hour} qty={events} />
|
||||
:
|
||||
<Center><Text>{strings.notHaveEvents}</Text></Center>
|
||||
<Center><Text>{t('notHaveEvents')}</Text></Center>
|
||||
}
|
||||
</Accordion.Panel>
|
||||
);
|
||||
|
||||
@ -2,10 +2,10 @@ import { Button, Flex, Text } from '@mantine/core';
|
||||
import { IconExternalLink } from '@tabler/icons-react';
|
||||
import { proxyApi } from '../../../services/frigate.proxy/frigate.api';
|
||||
import { EventFrigate } from '../../../types/event';
|
||||
import { strings } from '../../strings/strings';
|
||||
import { getDurationFromTimestamps, unixTimeToDate } from '../../utils/dateUtil';
|
||||
import BlobImage from '../images/BlobImage';
|
||||
import VideoPlayer from '../players/VideoPlayer';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface EventPanelProps {
|
||||
event: EventFrigate
|
||||
@ -20,6 +20,7 @@ const EventPanel = ({
|
||||
videoURL,
|
||||
playedURL,
|
||||
}: EventPanelProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -45,12 +46,12 @@ const EventPanel = ({
|
||||
</Button>
|
||||
</Flex>
|
||||
}
|
||||
<Text mt='1rem'>{strings.camera}: {event.camera}</Text>
|
||||
<Text>{strings.player.object}: {event.label}</Text>
|
||||
<Text>{strings.player.startTime}: {unixTimeToDate(event.start_time)}</Text>
|
||||
<Text>{strings.player.duration}: {getDurationFromTimestamps(event.start_time, event.end_time)}</Text>
|
||||
<Text mt='1rem'>{t('camera')}: {event.camera}</Text>
|
||||
<Text>{t('player.object')}: {event.label}</Text>
|
||||
<Text>{t('player.startTime')}: {unixTimeToDate(event.start_time)}</Text>
|
||||
<Text>{t('player.duration')}: {getDurationFromTimestamps(event.start_time, event.end_time)}</Text>
|
||||
{!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>
|
||||
|
||||
@ -6,11 +6,11 @@ import AccordionShareButton from '../buttons/AccordionShareButton';
|
||||
import PlayControl from '../buttons/PlayControl';
|
||||
import EventPanel from './EventPanel';
|
||||
import { EventFrigate } from '../../../types/event';
|
||||
import { strings } from '../../strings/strings';
|
||||
import { unixTimeToDate, getDurationFromTimestamps } from '../../utils/dateUtil';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { routesPath } from '../../../router/routes.path';
|
||||
import { proxyApi } from '../../../services/frigate.proxy/frigate.api';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
|
||||
interface EventsAccordionItemProps {
|
||||
@ -25,8 +25,8 @@ const EventsAccordionItem = ({
|
||||
hostName,
|
||||
played,
|
||||
openPlayer,
|
||||
} : EventsAccordionItemProps) => {
|
||||
|
||||
}: EventsAccordionItemProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [playedURL, setPlayedUrl] = useState<string>()
|
||||
|
||||
const navigate = useNavigate()
|
||||
@ -44,7 +44,7 @@ const EventsAccordionItem = ({
|
||||
const duration = getDurationFromTimestamps(event.start_time, event.end_time)
|
||||
return (
|
||||
<Group>
|
||||
<Text>{strings.player.object}: {event.label}</Text>
|
||||
<Text>{t('player.object')}: {event.label}</Text>
|
||||
<Text>{time}</Text>
|
||||
{duration ?
|
||||
<Text>{duration}</Text>
|
||||
@ -84,7 +84,7 @@ const EventsAccordionItem = ({
|
||||
<IconExternalLink />
|
||||
</AccordionControlButton>
|
||||
<PlayControl
|
||||
played={played ? played : false }
|
||||
played={played ? played : false}
|
||||
onClick={handleOpenPlayer} />
|
||||
</Group>
|
||||
</Flex>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Button, ButtonProps, Group, UnstyledButton, createStyles } from '@mantine/core';
|
||||
import { ButtonProps, Group, createStyles } from '@mantine/core';
|
||||
import React from 'react';
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { Tooltip, CloseButton, CloseButtonProps } from '@mantine/core';
|
||||
import React from 'react';
|
||||
import { strings } from '../../strings/strings';
|
||||
|
||||
|
||||
interface CloseWithTooltipProps {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Flex, createStyles } from '@mantine/core';
|
||||
import { IconPlayerPlayFilled, IconPlayerStopFilled } from '@tabler/icons-react';
|
||||
import { strings } from '../../strings/strings';
|
||||
import AccordionControlButton from './AccordionControlButton';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
@ -22,6 +22,7 @@ const PlayControl = ({
|
||||
played,
|
||||
onClick
|
||||
}: PlayControlProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { classes } = useStyles();
|
||||
|
||||
const handleClick = () => {
|
||||
@ -32,7 +33,7 @@ const PlayControl = ({
|
||||
onClick={() => { handleClick() }}
|
||||
>
|
||||
<Flex align='center'>
|
||||
{played ? strings.player.stopVideo : strings.player.startVideo}
|
||||
{played ? t('player.stopVideo') : t('player.startVideo')}
|
||||
{played ?
|
||||
<IconPlayerStopFilled
|
||||
className={classes.iconStop} />
|
||||
|
||||
@ -6,9 +6,9 @@ import { frigateApi, frigateQueryKeys } from '../../../services/frigate.proxy/fr
|
||||
import CogwheelLoader from '../loaders/CogwheelLoader';
|
||||
import { Center, Loader, Text } from '@mantine/core';
|
||||
import OneSelectFilter, { OneSelectItem } from './OneSelectFilter';
|
||||
import { strings } from '../../strings/strings';
|
||||
import RetryError from '../RetryError';
|
||||
import { isProduction } from '../../env.const';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface CameraSelectFilterProps {
|
||||
selectedHostId: string,
|
||||
@ -17,6 +17,7 @@ interface CameraSelectFilterProps {
|
||||
const CameraSelectFilter = ({
|
||||
selectedHostId,
|
||||
}: CameraSelectFilterProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { recordingsStore: recStore } = useContext(Context)
|
||||
|
||||
const { data, isError, isPending, isSuccess, refetch } = useQuery({
|
||||
@ -53,7 +54,7 @@ const CameraSelectFilter = ({
|
||||
return (
|
||||
<OneSelectFilter
|
||||
id='frigate-cameras'
|
||||
label={strings.selectCamera}
|
||||
label={t('selectCamera')}
|
||||
spaceBetween='1rem'
|
||||
value={recStore.filteredCamera?.id || ''}
|
||||
defaultValue={recStore.filteredCamera?.id || ''}
|
||||
|
||||
@ -2,15 +2,16 @@ import { Box, Flex, Indicator, Text } from '@mantine/core';
|
||||
import { DatePickerInput } from '@mantine/dates';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useContext } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Context } from '../../..';
|
||||
import { isProduction } from '../../env.const';
|
||||
import { strings } from '../../strings/strings';
|
||||
|
||||
interface DateRangeSelectFilterProps {}
|
||||
|
||||
const DateRangeSelectFilter = ({
|
||||
|
||||
}: DateRangeSelectFilterProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { recordingsStore: recStore } = useContext(Context)
|
||||
|
||||
const handlePick = (value: [Date | null, Date | null]) => {
|
||||
@ -23,7 +24,7 @@ const DateRangeSelectFilter = ({
|
||||
<Flex
|
||||
mt='1rem'
|
||||
justify='space-between'>
|
||||
<Text>{strings.selectRange}</Text>
|
||||
<Text>{t('selectRange')}</Text>
|
||||
</Flex>
|
||||
<DatePickerInput
|
||||
w='100%'
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import React, { useEffect } from 'react';
|
||||
import { frigateQueryKeys, frigateApi } from '../../../services/frigate.proxy/frigate.api';
|
||||
import { strings } from '../../strings/strings';
|
||||
import RetryError from '../RetryError';
|
||||
import CogwheelLoader from '../loaders/CogwheelLoader';
|
||||
import OneSelectFilter, { OneSelectItem } from './OneSelectFilter';
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { SystemProp, SpacingValue, Box, Flex, CloseButton, MultiSelect, SelectItem, MultiSelectProps, Text, Tooltip } from '@mantine/core';
|
||||
import React, { CSSProperties, useState } from 'react';
|
||||
import { strings } from '../../strings/strings';
|
||||
import { Box, Flex, MultiSelect, MultiSelectProps, SelectItem, SpacingValue, SystemProp, Text } from '@mantine/core';
|
||||
import { CSSProperties } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import CloseWithTooltip from '../buttons/CloseWithTooltip';
|
||||
|
||||
interface MultiSelectFilterProps {
|
||||
@ -22,6 +22,7 @@ const MultiSelectFilter = ({
|
||||
label, defaultValue, textClassName,
|
||||
selectProps, display, showClose, changedState, onClose
|
||||
}: MultiSelectFilterProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleOnChange = (value: string[]) => {
|
||||
if (changedState) {
|
||||
@ -34,7 +35,7 @@ const MultiSelectFilter = ({
|
||||
<Flex justify='space-between'>
|
||||
<Text className={textClassName}>{label}</Text>
|
||||
{showClose ?
|
||||
<CloseWithTooltip label={strings.hide} onClose={onClose} />
|
||||
<CloseWithTooltip label={t('hide')} onClose={onClose} />
|
||||
: null}
|
||||
</Flex>
|
||||
<MultiSelect
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { SelectItem, SystemProp, SpacingValue, SelectProps, Box, Flex, CloseButton, Text, Select } from '@mantine/core';
|
||||
import React, { CSSProperties } from 'react';
|
||||
import { Box, Flex, Select, SelectProps, SpacingValue, SystemProp, Text } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import CloseWithTooltip from '../buttons/CloseWithTooltip';
|
||||
import { strings } from '../../strings/strings';
|
||||
|
||||
|
||||
export interface OneSelectItem {
|
||||
@ -30,6 +29,7 @@ const OneSelectFilter = ({
|
||||
label, defaultValue, textClassName,
|
||||
showClose, value, onChange: onChange, onClose, ...selectProps
|
||||
}: OneSelectFilterProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleOnChange = (value: string) => {
|
||||
if (onChange) onChange(value, id,)
|
||||
@ -44,7 +44,7 @@ const OneSelectFilter = ({
|
||||
{!label ? null :
|
||||
<Flex justify='space-between'>
|
||||
<Text className={textClassName}>{label}</Text>
|
||||
{showClose ? <CloseWithTooltip label={strings.hide} onClose={handleOnClose} />
|
||||
{showClose ? <CloseWithTooltip label={t('hide')} onClose={handleOnClose} />
|
||||
: null}
|
||||
</Flex>
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { SystemProp, SpacingValue, SliderProps, Box, RangeSlider, RangeSliderProps, Text, Flex, CloseButton } from '@mantine/core';
|
||||
import React, { CSSProperties, useState } from 'react';
|
||||
import { Box, Flex, RangeSlider, RangeSliderProps, SpacingValue, SystemProp, Text } from '@mantine/core';
|
||||
import { CSSProperties } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import CloseWithTooltip from '../buttons/CloseWithTooltip';
|
||||
import { strings } from '../../strings/strings';
|
||||
|
||||
interface SliderFilterProps {
|
||||
id: string
|
||||
@ -16,13 +16,14 @@ interface SliderFilterProps {
|
||||
display?: SystemProp<CSSProperties['display']>
|
||||
showClose?: boolean,
|
||||
changedState?(id: string, value: [number, number]): void
|
||||
onClose?():void
|
||||
onClose?(): void
|
||||
}
|
||||
|
||||
|
||||
const RangeSliderFilter = ({ id, min, max, value, spaceBetween,
|
||||
label, defaultValue, textClassName,
|
||||
sliderProps, display, showClose, changedState, onClose }: SliderFilterProps) => {
|
||||
const { t } = useTranslation()
|
||||
const handleOnChange = (value: [number, number]) => {
|
||||
if (changedState) {
|
||||
changedState(id, value)
|
||||
@ -33,7 +34,7 @@ const RangeSliderFilter = ({ id, min, max, value, spaceBetween,
|
||||
<Box display={display} mt={spaceBetween}>
|
||||
<Flex justify='space-between'>
|
||||
<Text className={textClassName}>{label}</Text>
|
||||
{showClose? <CloseWithTooltip label={strings.hide} onClose={onClose} /> : null}
|
||||
{showClose ? <CloseWithTooltip label={t('hide')} onClose={onClose} /> : null}
|
||||
</Flex>
|
||||
<RangeSlider {...sliderProps} value={value} onChangeEnd={handleOnChange} min={min} max={max} defaultValue={defaultValue} pl='1rem' mt='0.5rem' />
|
||||
</Box>
|
||||
|
||||
@ -3,10 +3,11 @@ import { observer } from 'mobx-react-lite';
|
||||
import { useContext } from 'react';
|
||||
import { Context } from '../../..';
|
||||
import { frigateApi, frigateQueryKeys } from '../../../services/frigate.proxy/frigate.api';
|
||||
import { strings } from '../../strings/strings';
|
||||
import HostSelect from './HostSelect';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const RecordingsHostFilter = () => {
|
||||
const { t } = useTranslation()
|
||||
const { recordingsStore: recStore } = useContext(Context)
|
||||
|
||||
const { data: hosts } = useQuery({
|
||||
@ -38,7 +39,7 @@ const RecordingsHostFilter = () => {
|
||||
|
||||
return (
|
||||
<HostSelect
|
||||
label={strings.selectHost}
|
||||
label={t('selectHost')}
|
||||
valueId={recStore.filteredHost?.id}
|
||||
defaultId={recStore.filteredHost?.id}
|
||||
onChange={handleSelect}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Box, CloseButton, Flex, Slider, SliderProps, SpacingValue, SystemProp, Text } from '@mantine/core';
|
||||
import React, { CSSProperties, useState, } from 'react';
|
||||
import { Box, Flex, Slider, SliderProps, SpacingValue, SystemProp, Text } from '@mantine/core';
|
||||
import { CSSProperties } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import CloseWithTooltip from '../buttons/CloseWithTooltip';
|
||||
import { strings } from '../../strings/strings';
|
||||
|
||||
interface SliderFilterProps {
|
||||
id: string
|
||||
@ -16,11 +16,12 @@ interface SliderFilterProps {
|
||||
display?: SystemProp<CSSProperties['display']>
|
||||
showClose?: boolean,
|
||||
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 { t } = useTranslation()
|
||||
|
||||
const handleOnChange = (value: number) => {
|
||||
if (changedState) {
|
||||
@ -32,7 +33,7 @@ const SliderFilter = ({ id, min, max, value, spaceBetween, label, defaultValue,
|
||||
<Box display={display} mt={spaceBetween}>
|
||||
<Flex justify='space-between'>
|
||||
<Text className={textClassName}>{label}</Text>
|
||||
{showClose ? <CloseWithTooltip label={strings.hide} onClose={onClose} /> : null}
|
||||
{showClose ? <CloseWithTooltip label={t('hide')} onClose={onClose} /> : null}
|
||||
</Flex>
|
||||
<Slider {...sliderProps}
|
||||
onChangeEnd={handleOnChange}
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import { SystemProp, SpacingValue, Flex, Switch, Text, CloseButton, Group, Box } from '@mantine/core';
|
||||
import React, { CSSProperties, ChangeEvent } from 'react';
|
||||
import { boolean } from 'zod';
|
||||
import { Box, Flex, SpacingValue, Switch, SystemProp, Text } from '@mantine/core';
|
||||
import { CSSProperties, ChangeEvent } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import CloseWithTooltip from '../buttons/CloseWithTooltip';
|
||||
import { strings } from '../../strings/strings';
|
||||
|
||||
interface SwitchFilterProps {
|
||||
id: string
|
||||
@ -14,7 +13,7 @@ interface SwitchFilterProps {
|
||||
display?: SystemProp<CSSProperties['display']>
|
||||
showClose?: boolean
|
||||
changedState?(id: string, value: boolean): void
|
||||
onClose?():void
|
||||
onClose?(): void
|
||||
}
|
||||
|
||||
export interface SwitchChangeState {
|
||||
@ -23,6 +22,7 @@ export interface SwitchChangeState {
|
||||
}
|
||||
|
||||
const SwitchFilter = ({ id, value, defaultValue, spaceBetween, label, textClassName, display, showClose, changedState, onClose }: SwitchFilterProps) => {
|
||||
const { t } = useTranslation()
|
||||
const handleChange = (event: ChangeEvent<HTMLInputElement> | undefined) => {
|
||||
const checked = event?.currentTarget.checked
|
||||
if (changedState && typeof checked === 'boolean') {
|
||||
@ -35,7 +35,7 @@ const SwitchFilter = ({ id, value, defaultValue, spaceBetween, label, textClassN
|
||||
<Flex align='center' w='100%'>
|
||||
<Text className={textClassName}>{label}</Text>
|
||||
<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>
|
||||
</Box>
|
||||
);
|
||||
|
||||
43
src/shared/components/inputs/ClearableTextInput.tsx
Normal file
43
src/shared/components/inputs/ClearableTextInput.tsx
Normal 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;
|
||||
@ -1,9 +1,9 @@
|
||||
import { ActionIcon, CloseButton, Flex, Modal, NumberInput, TextInput, Tooltip, createStyles, } from '@mantine/core';
|
||||
import { getHotkeyHandler, useMediaQuery } from '@mantine/hooks';
|
||||
import React, { ReactEventHandler, useState, FocusEvent, useRef, Ref } from 'react';
|
||||
import { strings } from '../../strings/strings';
|
||||
import { IconAlertCircle, IconX } from '@tabler/icons-react';
|
||||
import { dimensions } from '../../dimensions/dimensions';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
rightSection: {
|
||||
@ -21,6 +21,7 @@ interface InputModalProps {
|
||||
}
|
||||
|
||||
const InputModal = ({ inValue, putValue, opened, open, close }: InputModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { classes } = useStyles()
|
||||
const [value, setValue] = useState(inValue)
|
||||
const isMobile = useMediaQuery(dimensions.mobileSize)
|
||||
@ -54,7 +55,7 @@ const InputModal = ({ inValue, putValue, opened, open, close }: InputModalProps)
|
||||
fullScreen={isMobile}
|
||||
>
|
||||
<Flex justify="space-between">
|
||||
<div>{strings.enterQuantity}</div>
|
||||
<div>{t('enterQuantity')}</div>
|
||||
<CloseButton size="lg" onClick={handleClose} />
|
||||
</Flex>
|
||||
<NumberInput
|
||||
@ -64,7 +65,7 @@ const InputModal = ({ inValue, putValue, opened, open, close }: InputModalProps)
|
||||
value={value}
|
||||
onChange={handleSetValue}
|
||||
data-autofocus
|
||||
placeholder={strings.quantity}
|
||||
placeholder={t('quantity')}
|
||||
hideControls
|
||||
min={0}
|
||||
onFocus={handeLoaded}
|
||||
@ -73,9 +74,9 @@ const InputModal = ({ inValue, putValue, opened, open, close }: InputModalProps)
|
||||
['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'>
|
||||
<Tooltip label={strings.tooltip_close} position="top-end" withArrow>
|
||||
<Tooltip label={t('tooltipСlose')} position="top-end" withArrow>
|
||||
<div>
|
||||
<IconAlertCircle size="1.4rem" style={{ display: 'block', opacity: 0.5 }} />
|
||||
</div>
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
// @ts-ignore we know this doesn't have types
|
||||
import JSMpeg from "@cycjimmy/jsmpeg-player";
|
||||
import { Flex } from "@mantine/core";
|
||||
import { useViewportSize } from "@mantine/hooks";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { strings } from "../../strings/strings";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type JSMpegPlayerProps = {
|
||||
wsUrl: string;
|
||||
@ -18,6 +17,7 @@ const JSMpegPlayer = (
|
||||
cameraHeight = 800,
|
||||
}: JSMpegPlayerProps
|
||||
) => {
|
||||
const { t } = useTranslation()
|
||||
const playerRef = useRef<HTMLDivElement>(null);
|
||||
const [playerInitialized, setPlayerInitialized] = useState(false)
|
||||
|
||||
@ -28,7 +28,7 @@ const JSMpegPlayer = (
|
||||
playerRef.current,
|
||||
wsUrl,
|
||||
{},
|
||||
{protocols: [], audio: false, videoBufferSize: 1024*1024*4}
|
||||
{ protocols: [], audio: false, videoBufferSize: 1024 * 1024 * 4 }
|
||||
);
|
||||
|
||||
const toggleFullscreen = () => {
|
||||
@ -54,7 +54,7 @@ const JSMpegPlayer = (
|
||||
}
|
||||
};
|
||||
|
||||
video.els.canvas.addEventListener('dblclick',toggleFullscreen);
|
||||
video.els.canvas.addEventListener('dblclick', toggleFullscreen);
|
||||
|
||||
return () => {
|
||||
video.destroy();
|
||||
@ -66,8 +66,8 @@ const JSMpegPlayer = (
|
||||
<div
|
||||
ref={playerRef}
|
||||
key={wsUrl}
|
||||
title={strings.player.doubleClickToFullHint}
|
||||
style={{width:cameraWidth, height:cameraHeight, maxWidth:maxWidth, maxHeight: maxHeight-100, }} />
|
||||
title={t('player.doubleClickToFullHint')}
|
||||
style={{ width: cameraWidth, height: cameraHeight, maxWidth: maxWidth, maxHeight: maxHeight - 100, }} />
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
@ -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;
|
||||
@ -1,8 +0,0 @@
|
||||
export const headerMenu = {
|
||||
home:"На главную",
|
||||
test:"Тест",
|
||||
settings:"Настройки",
|
||||
recordings:"Записи",
|
||||
acessSettings:" Настройка доступа",
|
||||
hostsConfig:"Серверы Frigate",
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
export const productString = {
|
||||
name: "Наименование",
|
||||
cost: "Цена",
|
||||
image: "Изображение",
|
||||
qty: "Количество",
|
||||
buy: "Купить",
|
||||
number: "Артикул",
|
||||
manufactory: "Производитель",
|
||||
oem: "OEM",
|
||||
stock: "Наличие",
|
||||
receiptDate: "Дата поступления",
|
||||
discount:"Скидка",
|
||||
parameters: "Характеристики",
|
||||
}
|
||||
@ -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: "Извините мы не можем найти такую страницу",
|
||||
|
||||
}
|
||||
}
|
||||
@ -7,7 +7,7 @@ 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 { useTranslation } from 'react-i18next';
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
mainCard: {
|
||||
@ -41,6 +41,7 @@ interface CameraCardProps {
|
||||
const CameraCard = ({
|
||||
camera
|
||||
}: CameraCardProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [renderImage, setRenderImage] = useState<boolean>(false)
|
||||
const { classes } = useStyles();
|
||||
const { ref, entry } = useIntersection({ threshold: 0.5, })
|
||||
@ -71,7 +72,7 @@ const CameraCard = ({
|
||||
<Group
|
||||
className={classes.bottomGroup}>
|
||||
<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>
|
||||
</Group>
|
||||
</Card>
|
||||
|
||||
@ -7,10 +7,10 @@ import { GetFrigateHost } from '../services/frigate.proxy/frigate.schema';
|
||||
import HostSettingsMenu from '../shared/components/menu/HostSettingsMenu';
|
||||
import SortedTh from '../shared/components/table.aps/SortedTh';
|
||||
import { isProduction } from '../shared/env.const';
|
||||
import { strings } from '../shared/strings/strings';
|
||||
import StateCell from './hosts.table/StateCell';
|
||||
import SwitchCell from './hosts.table/SwitchCell';
|
||||
import TextInputCell from './hosts.table/TextInputCell';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface TableProps<T> {
|
||||
data: T[],
|
||||
@ -20,6 +20,8 @@ interface TableProps<T> {
|
||||
}
|
||||
|
||||
const FrigateHostsTable = ({ data, showAddButton = false, saveCallback, changedCallback }: TableProps<GetFrigateHost>) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [tableData, setTableData] = useState(data)
|
||||
const [reversed, setReversed] = useState(false)
|
||||
const [sortedName, setSortedName] = useState<string | null>(null)
|
||||
@ -59,9 +61,9 @@ const FrigateHostsTable = ({ data, showAddButton = false, saveCallback, changedC
|
||||
}
|
||||
|
||||
const headTitle = [
|
||||
{ propertyName: 'name', title: strings.hostArr.name },
|
||||
{ propertyName: 'host', title: strings.hostArr.url },
|
||||
{ propertyName: 'enabled', title: strings.hostArr.enabled },
|
||||
{ propertyName: 'name', title: t('hostArr.host') },
|
||||
{ propertyName: 'host', title: t('hostArr.url') },
|
||||
{ propertyName: 'enabled', title: t('hostArr.enabled') },
|
||||
{ title: '', sorting: false },
|
||||
]
|
||||
|
||||
|
||||
@ -5,8 +5,8 @@ import { frigateQueryKeys, frigateApi } from '../services/frigate.proxy/frigate.
|
||||
import { Context } from '..';
|
||||
import CenterLoader from '../shared/components/loaders/CenterLoader';
|
||||
import RetryErrorPage from '../pages/RetryErrorPage';
|
||||
import { strings } from '../shared/strings/strings';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
const CameraAccordion = lazy(() => import('../shared/components/accordion/CameraAccordion'));
|
||||
|
||||
interface SelectedHostListProps {
|
||||
@ -16,7 +16,7 @@ interface SelectedHostListProps {
|
||||
const SelectedHostList = ({
|
||||
hostId
|
||||
}: SelectedHostListProps) => {
|
||||
|
||||
const { t } = useTranslation()
|
||||
const { recordingsStore: recStore } = useContext(Context)
|
||||
const [openCameraId, setOpenCameraId] = useState<string | null>(null)
|
||||
|
||||
@ -47,7 +47,7 @@ const SelectedHostList = ({
|
||||
const camerasItems = camerasQuery.map(camera => {
|
||||
return (
|
||||
<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'}>
|
||||
{openCameraId === camera.id && (
|
||||
<Suspense>
|
||||
@ -61,7 +61,7 @@ const SelectedHostList = ({
|
||||
|
||||
return (
|
||||
<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
|
||||
mt='1rem'
|
||||
variant='separated'
|
||||
|
||||
@ -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 (error) return <RetryError onRetry={() => createVideo.mutate()} />
|
||||
if (link && progress && !videoSrc) return (
|
||||
|
||||
@ -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 },
|
||||
]
|
||||
36
yarn.lock
36
yarn.lock
@ -1122,7 +1122,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.0.tgz#584c450063ffda59697021430cb47101b085951e"
|
||||
integrity sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==
|
||||
@ -5684,6 +5684,13 @@ html-minifier-terser@^6.0.2:
|
||||
relateurl "^0.2.7"
|
||||
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:
|
||||
version "5.6.0"
|
||||
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"
|
||||
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:
|
||||
version "0.4.24"
|
||||
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"
|
||||
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:
|
||||
version "16.13.1"
|
||||
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:
|
||||
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:
|
||||
version "8.2.0"
|
||||
resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz#f43dfa35fb51e763d17cd94dcca0c9458f35abf9"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user