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
# 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

View File

@ -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",

View File

@ -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 })
}

View File

@ -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)

View File

@ -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
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 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>
);

View File

@ -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>
);

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 { 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

View File

@ -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>
);

View File

@ -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} />

View File

@ -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>

View File

@ -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>

View File

@ -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>
);

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 { 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>

View File

@ -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
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 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"
/>
</>

View File

@ -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>
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>
);

View File

@ -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>

View File

@ -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 {
@ -26,7 +26,7 @@ const EventsAccordionItem = ({
played,
openPlayer,
}: 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>

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';
const useStyles = createStyles((theme) => ({

View File

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

View File

@ -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} />

View File

@ -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 || ''}

View File

@ -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%'

View File

@ -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';

View File

@ -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

View File

@ -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>
}

View File

@ -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
@ -23,6 +23,7 @@ interface SliderFilterProps {
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>

View File

@ -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}

View File

@ -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
@ -21,6 +21,7 @@ interface SliderFilterProps {
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}

View File

@ -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
@ -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>
);

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 { 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>

View File

@ -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)
@ -66,7 +66,7 @@ const JSMpegPlayer = (
<div
ref={playerRef}
key={wsUrl}
title={strings.player.doubleClickToFullHint}
title={t('player.doubleClickToFullHint')}
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 { 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>

View File

@ -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 },
]

View File

@ -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'

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 (error) return <RetryError onRetry={() => createVideo.mutate()} />
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"
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"