From 65a2a8499851c04fbd5f18b63d1ec260d5b6225b Mon Sep 17 00:00:00 2001 From: GermanBluefox Date: Fri, 20 Sep 2024 16:09:09 +0800 Subject: [PATCH] Replace craco with vite --- README.md | 2 +- src-widgets/package.json | 1 - src-widgets/src/SnapshotCamera.jsx | 13 +- src/eslint.config.mjs | 24 ++ src/index.html | 48 ++++ src/package.json | 24 +- src/prettier.config.mjs | 3 + src/public/cameras.png | Bin 0 -> 43236 bytes src/public/index.html | 30 --- src/src/{App.js => App.tsx} | 116 +++++---- src/src/Tabs/{Cameras.js => Cameras.tsx} | 243 ++++++++++-------- src/src/Tabs/{Options.js => Options.tsx} | 145 ++++++----- src/src/Types/GenericConfig.tsx | 44 ++++ src/src/Types/{RTSPEufy.js => RTSPEufy.tsx} | 64 ++--- src/src/Types/{RTSPHiKam.js => RTSPHiKam.tsx} | 54 ++-- src/src/Types/{RTSPImage.js => RTSPImage.tsx} | 93 ++++--- .../{RTSPReolinkE1.js => RTSPReolinkE1.tsx} | 53 ++-- ...asicAuthImage.js => URLBasicAuthImage.tsx} | 45 ++-- src/src/Types/{URLImage.js => URLImage.tsx} | 33 ++- src/src/i18n/de.json | 4 +- src/src/i18n/en.json | 4 +- src/src/i18n/es.json | 4 +- src/src/i18n/fr.json | 4 +- src/src/i18n/it.json | 4 +- src/src/i18n/nl.json | 4 +- src/src/i18n/pl.json | 4 +- src/src/i18n/pt.json | 4 +- src/src/i18n/ru.json | 4 +- src/src/i18n/uk.json | 4 +- src/src/i18n/zh-cn.json | 4 +- src/src/{index.js => index.tsx} | 8 +- .../{serviceWorker.js => serviceWorker.ts} | 19 +- src/src/types.d.ts | 27 ++ src/tsconfig.json | 26 ++ src/vite.config.ts | 19 ++ tasks.js | 16 +- widgets/cameras/customWidgets.js | 2 +- 37 files changed, 726 insertions(+), 470 deletions(-) create mode 100644 src/eslint.config.mjs create mode 100644 src/index.html create mode 100644 src/prettier.config.mjs create mode 100644 src/public/cameras.png delete mode 100644 src/public/index.html rename src/src/{App.js => App.tsx} (66%) rename src/src/Tabs/{Cameras.js => Cameras.tsx} (74%) rename src/src/Tabs/{Options.js => Options.tsx} (78%) create mode 100644 src/src/Types/GenericConfig.tsx rename src/src/Types/{RTSPEufy.js => RTSPEufy.tsx} (70%) rename src/src/Types/{RTSPHiKam.js => RTSPHiKam.tsx} (72%) rename src/src/Types/{RTSPImage.js => RTSPImage.tsx} (79%) rename src/src/Types/{RTSPReolinkE1.js => RTSPReolinkE1.tsx} (72%) rename src/src/Types/{URLBasicAuthImage.js => URLBasicAuthImage.tsx} (68%) rename src/src/Types/{URLImage.js => URLImage.tsx} (57%) rename src/src/{index.js => index.tsx} (78%) rename src/src/{serviceWorker.js => serviceWorker.ts} (89%) create mode 100644 src/src/types.d.ts create mode 100644 src/tsconfig.json create mode 100644 src/vite.config.ts diff --git a/README.md b/README.md index 83365c0..dd07d80 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ Here is an example of how to add Reolink E1: To add a new camera, you must create a Pull Request on GitHub with the following changes: - Add new file into `cameras` folder. This is a backend to read the single image from the camera. - Add GUI file in the `src/src/Types/` folder. This is the configuration dialog for the camera -- Add this dialog in `src/src/Tabs/Cameras.js` file analogical as other cameras are added. Only two lines should be added: +- Add this dialog in `src/src/Tabs/Cameras.tsx` file analogical as other cameras are added. Only two lines should be added: - Import new configuration dialog like `import RTSPMyCamConfig from '../Types/RTSPMyCam';` - Extend `TYPES` structure with the new camera like `mycam: { Config: RTSPMyCamConfig, name: 'MyCam' },` The attribute name must be the same as the name of the file in the `cameras` folder. diff --git a/src-widgets/package.json b/src-widgets/package.json index 9490bc8..58eb3d0 100644 --- a/src-widgets/package.json +++ b/src-widgets/package.json @@ -3,7 +3,6 @@ "private": true, "version": "2.1.2", "dependencies": { - "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@craco/craco": "^7.1.0", "@iobroker/adapter-react-v5": "^7.1.4", "@iobroker/vis-2-widgets-react-dev": "^4.0.3", diff --git a/src-widgets/src/SnapshotCamera.jsx b/src-widgets/src/SnapshotCamera.jsx index c1c2776..a18427a 100644 --- a/src-widgets/src/SnapshotCamera.jsx +++ b/src-widgets/src/SnapshotCamera.jsx @@ -1,5 +1,11 @@ import React from 'react'; -import { Button, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material'; +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, +} from '@mui/material'; import { Close } from '@mui/icons-material'; @@ -354,7 +360,10 @@ class SnapshotCamera extends Generic { left: 0, }} > -
{Generic.t('Cannot load URL')}:
+
+ {Generic.t('Cannot load URL')} + : +
{this.getUrl(true)}
) : null} diff --git a/src/eslint.config.mjs b/src/eslint.config.mjs new file mode 100644 index 0000000..53b7a8c --- /dev/null +++ b/src/eslint.config.mjs @@ -0,0 +1,24 @@ +import config, { reactConfig } from '@iobroker/eslint-config'; + +// disable temporary the rule 'jsdoc/require-param' and enable 'jsdoc/require-jsdoc' +config.forEach(rule => { + if (rule?.plugins?.jsdoc) { + rule.rules['jsdoc/require-jsdoc'] = 'off'; + rule.rules['jsdoc/require-param'] = 'off'; + } +}); + +export default [ + ...config, + ...reactConfig, + { + languageOptions: { + parserOptions: { + projectService: { + allowDefaultProject: ['*.js', '*.mjs'], + }, + tsconfigRootDir: import.meta.dirname, + }, + }, + }, +]; diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..06062dd --- /dev/null +++ b/src/index.html @@ -0,0 +1,48 @@ + + + + + + + + + + ioBroker.cameras + + + + +
+ + diff --git a/src/package.json b/src/package.json index 2e55792..09191a3 100644 --- a/src/package.json +++ b/src/package.json @@ -3,25 +3,27 @@ "version": "2.1.2", "private": true, "dependencies": { - "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@iobroker/adapter-react-v5": "^7.1.4", - "@material-ui/icons": "^4.11.3", + "@iobroker/eslint-config": "^0.1.6", + "@iobroker/types": "^6.0.11", "@mui/icons-material": "^6.1.1", "@mui/material": "^6.1.1", - "babel-eslint": "^10.1.0", + "@types/react": "^18.3.7", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", "react": "^18.3.1", "react-dom": "^18.3.1", "react-icons": "^5.3.0", - "react-scripts": "^5.0.1", - "eslint": "^8.56.0", - "eslint-plugin-import": "^2.29.1" + "typescript": "^5.6.2", + "vite": "^5.4.6", + "vite-tsconfig-paths": "^5.0.1" }, "scripts": { - "start": "set DANGEROUSLY_DISABLE_HOST_CHECK=true&& react-scripts start", - "build": "react-scripts build", - "test": "react-scripts test", - "eject": "react-scripts eject", - "lint": "eslint -c eslint.config.mjs src" + "start": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "lint": "eslint -c eslint.config.mjs src", + "tsc": "tsc -p tsconfig.json src" }, "eslintConfig": { "extends": "react-app" diff --git a/src/prettier.config.mjs b/src/prettier.config.mjs new file mode 100644 index 0000000..2f00708 --- /dev/null +++ b/src/prettier.config.mjs @@ -0,0 +1,3 @@ +import prettierConfig from '@iobroker/eslint-config/prettier.config.mjs'; + +export default prettierConfig; diff --git a/src/public/cameras.png b/src/public/cameras.png new file mode 100644 index 0000000000000000000000000000000000000000..dae755a6e6cb90104af89c603eb0fdd78104c350 GIT binary patch literal 43236 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 literal 0 HcmV?d00001 diff --git a/src/public/index.html b/src/public/index.html deleted file mode 100644 index bbbcc5a..0000000 --- a/src/public/index.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - ioBroker.cameras - - - -
- - diff --git a/src/src/App.js b/src/src/App.tsx similarity index 66% rename from src/src/App.js rename to src/src/App.tsx index 874ceac..1bb3221 100644 --- a/src/src/App.js +++ b/src/src/App.tsx @@ -1,24 +1,40 @@ -import React from 'react'; +import React, { type JSX } from 'react'; import { ThemeProvider, StyledEngineProvider } from '@mui/material/styles'; -import { AppBar, Tabs, Tab } from '@mui/material'; +import { AppBar, Tabs, Tab, type Theme } from '@mui/material'; -import { Loader, I18n, GenericApp } from '@iobroker/adapter-react-v5'; +import { + Loader, + I18n, + GenericApp, + type GenericAppState, + type GenericAppProps, + type GenericAppSettings, +} from '@iobroker/adapter-react-v5'; import TabOptions from './Tabs/Options'; import TabCameras from './Tabs/Cameras'; -import langEn from './i18n/en'; -import langDe from './i18n/de'; -import langRu from './i18n/ru'; -import langPt from './i18n/pt'; -import langNl from './i18n/nl'; -import langFr from './i18n/fr'; -import langIt from './i18n/it'; -import langEs from './i18n/es'; -import langPl from './i18n/pl'; -import langUk from './i18n/uk'; -import langZhCn from './i18n/zh-cn'; +import langEn from './i18n/en.json'; +import langDe from './i18n/de.json'; +import langRu from './i18n/ru.json'; +import langPt from './i18n/pt.json'; +import langNl from './i18n/nl.json'; +import langFr from './i18n/fr.json'; +import langIt from './i18n/it.json'; +import langEs from './i18n/es.json'; +import langPl from './i18n/pl.json'; +import langUk from './i18n/uk.json'; +import langZhCn from './i18n/zh-cn.json'; +import type { CamerasInstanceNative } from '@/types'; + +function inIframe(): boolean { + try { + return window.self !== window.top; + } catch { + return true; + } +} const styles = { tabContent: { @@ -31,17 +47,25 @@ const styles = { height: 'calc(100% - 64px - 48px - 20px - 38px)', overflow: 'auto', }, - selected: theme => ({ + selected: (theme: Theme) => ({ color: theme.palette.mode === 'dark' ? undefined : '#FFF !important', }), - indicator: theme => ({ + indicator: (theme: Theme) => ({ backgroundColor: theme.palette.mode === 'dark' ? theme.palette.secondary.main : '#FFF', }), }; -class App extends GenericApp { - constructor(props) { - const extendedProps = {}; +interface AppState extends GenericAppState { + alive: boolean; + isIFrame: boolean; +} + +class App extends GenericApp { + private subscribed: string = ''; + private readonly isIFrame: boolean = inIframe(); + + constructor(props: GenericAppProps) { + const extendedProps: GenericAppSettings = {}; extendedProps.adapterName = 'cameras'; extendedProps.doNotLoadAllObjects = true; extendedProps.translations = { @@ -69,20 +93,20 @@ class App extends GenericApp { super(props, extendedProps); } - onAliveChanged = (id, state) => { + onAliveChanged = (id: string, state: ioBroker.State | null | undefined): void => { if (id && this.state.alive !== !!state?.val) { this.setState({ alive: !!state?.val }); } }; - componentWillUnmount() { + componentWillUnmount(): void { this.subscribed && this.socket.unsubscribeState(this.subscribed, this.onAliveChanged); super.componentWillUnmount(); } // called when connected with admin and loaded instance object - onConnectionReady() { - this.socket.getState(`${this.instanceId}.alive`).then(state => { + onConnectionReady(): void { + void this.socket.getState(`${this.instanceId}.alive`).then(state => { if (this.state.alive !== !!state?.val) { this.setState({ alive: !!state?.val }); } @@ -94,22 +118,11 @@ class App extends GenericApp { ); } this.subscribed = `${this.instanceId}.alive`; - this.socket.subscribeState(this.subscribed, this.onAliveChanged); + return this.socket.subscribeState(this.subscribed, this.onAliveChanged); }); } - getSelectedTab() { - const tab = this.state.selectedTab; - - if (!tab || tab === 'options') { - return 0; - } - if (tab === 'cameras') { - return 1; - } - } - - render() { + render(): JSX.Element { if (!this.state.loaded) { return ( @@ -129,19 +142,19 @@ class App extends GenericApp { > this.selectTab(e.target.dataset.name, index)} + value={this.state.selectedTab} + onChange={(_e, selectedTab: string): void => this.setState({ selectedTab })} sx={{ '& .MuiTabs-indicator': styles.indicator }} > @@ -151,19 +164,20 @@ class App extends GenericApp {
{(this.state.selectedTab === 'options' || !this.state.selectedTab) && ( cb(this.encrypt(value))} - decrypt={(value, cb) => cb(this.decrypt(value))} + native={this.state.native as CamerasInstanceNative} onError={text => this.setState({ errorText: text })} onLoad={native => this.onLoadConfig(native)} instance={this.instance} theme={this.state.theme} - getIpAddresses={() => this.socket.getIpAddresses(this.common.host)} + getIpAddresses={() => + this.common?.host + ? this.socket.getIpAddresses(this.common.host) + : Promise.resolve([]) + } getExtendableInstances={() => this.getExtendableInstances()} - onConfigError={configError => this.setConfigurationError(configError)} adapterName={this.adapterName} onChange={(attr, value, cb) => this.updateNativeValue(attr, value, cb)} instanceAlive={this.state.alive} @@ -177,10 +191,14 @@ class App extends GenericApp { themeType={this.state.themeType} adapterName={this.adapterName} instance={this.instance} - encrypt={(value, cb) => cb(this.encrypt(value))} - decrypt={(value, cb) => cb(this.decrypt(value))} + encrypt={(textToEncrypt: string, cb: (encryptedText: string) => void): void => + cb(this.encrypt(textToEncrypt)) + } + decrypt={(textToDecrypt: string, cb: (decryptedText: string) => void): void => + cb(this.decrypt(textToDecrypt)) + } instanceAlive={this.state.alive} - native={this.state.native} + native={this.state.native as CamerasInstanceNative} onChange={(attr, value, cb) => this.updateNativeValue(attr, value, cb)} /> )} diff --git a/src/src/Tabs/Cameras.js b/src/src/Tabs/Cameras.tsx similarity index 74% rename from src/src/Tabs/Cameras.js rename to src/src/Tabs/Cameras.tsx index 2e94bd3..883bf03 100644 --- a/src/src/Tabs/Cameras.js +++ b/src/src/Tabs/Cameras.tsx @@ -1,5 +1,4 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; +import React, { Component, type JSX } from 'react'; import { Fab, @@ -27,25 +26,41 @@ import { CameraAlt as IconTest, } from '@mui/icons-material'; -import { I18n, Message as MessageDialog } from '@iobroker/adapter-react-v5'; - -import URLImage from '../Types/URLImage'; -import URLBasicAuthImage from '../Types/URLBasicAuthImage'; +import { + type AdminConnection, + I18n, + type IobTheme, + Message as MessageDialog, + type ThemeType, +} from '@iobroker/adapter-react-v5'; + +import type GenericConfig from '@/Types/GenericConfig'; +import type { CameraType, GenericCameraSettings, GenericConfigProps } from '../Types/GenericConfig'; +import URLImageConfig from '../Types/URLImage'; +import URLBasicAuthImageConfig from '../Types/URLBasicAuthImage'; import RTSPImageConfig from '../Types/RTSPImage'; import RTSPReolinkE1Config from '../Types/RTSPReolinkE1'; import RTSPEufyConfig from '../Types/RTSPEufy'; import RTSPHiKamConfig from '../Types/RTSPHiKam'; +import type { CameraSettings, CamerasInstanceNative } from '../types'; -const TYPES = { - url: { Config: URLImage, name: 'URL' }, - urlBasicAuth: { Config: URLBasicAuthImage, name: 'URL with basic auth' }, - rtsp: { Config: RTSPImageConfig, name: 'RTSP Snapshot' }, - reolinkE1: { Config: RTSPReolinkE1Config, name: 'Reolink E1 Snapshot' }, - eufy: { Config: RTSPEufyConfig, name: 'Eufy Security' }, - hikam: { Config: RTSPHiKamConfig, name: 'HiKam / WiWiCam' }, +const TYPES: Record = { + url: { Config: URLImageConfig as any as GenericConfig, name: 'URL' }, + urlBasicAuth: { + Config: URLBasicAuthImageConfig as unknown as GenericConfig, + name: 'URL with basic auth', + }, + rtsp: { Config: RTSPImageConfig as unknown as GenericConfig, name: 'RTSP Snapshot' }, + reolinkE1: { + Config: RTSPReolinkE1Config as unknown as GenericConfig, + name: 'Reolink E1 Snapshot', + rtsp: true, + }, + eufy: { Config: RTSPEufyConfig as unknown as GenericConfig, name: 'Eufy Security' }, + hikam: { Config: RTSPHiKamConfig as unknown as GenericConfig, name: 'HiKam / WiWiCam', rtsp: true }, }; -const styles = { +const styles: Record = { tab: { width: '100%', height: '100%', @@ -148,47 +163,73 @@ const styles = { }, }; -class Server extends Component { - constructor(props) { +interface ServerProps { + decrypt: (textToDecrypt: string, cb: (decryptedText: string) => void) => void; + encrypt: (textToEncrypt: string, cb: (encryptedText: string) => void) => void; + native: CamerasInstanceNative; + instance: number; + adapterName: string; + onChange: (attr: string, value: any, cb?: () => void) => void; + socket: AdminConnection; + themeType: ThemeType; + instanceAlive: boolean; + theme: IobTheme; +} + +interface ServerState { + editCam: string; + editChanged: boolean; + requesting: boolean; + instanceAlive: boolean; + webInstanceHost: string; + message: string | undefined; + editedSettings: string; + editedSettingsOld: string; + testImg: string | null; +} + +class Server extends Component { + constructor(props: ServerProps) { super(props); this.state = { - editCam: false, + editCam: '', editChanged: false, requesting: false, instanceAlive: this.props.instanceAlive, webInstanceHost: '', + message: '', + testImg: null, + editedSettings: '', + editedSettingsOld: '', }; // translate all names once - Object.keys(TYPES).forEach(type => { - if (TYPES[type].name && !TYPES[type].translated) { - TYPES[type].translated = true; - TYPES[type].name = I18n.t(TYPES[type].name); - if (TYPES[type].Config.getRtsp && TYPES[type].Config.getRtsp()) { - TYPES[type].rtsp = true; - } + Object.keys(TYPES).forEach((type: string) => { + if (TYPES[type as CameraType].name && !TYPES[type as CameraType].translated) { + TYPES[type as CameraType].translated = true; + TYPES[type as CameraType].name = I18n.t(TYPES[type as CameraType].name); } }); } - componentDidMount() { + componentDidMount(): void { this.getWebInstances(); } - static ip2int(ip) { + static ip2int(ip: string): number { return ip.split('.').reduce((ipInt, octet) => (ipInt << 8) + parseInt(octet, 10), 0) >>> 0; } - static findNetworkAddressOfHost(obj, localIp) { + static findNetworkAddressOfHost(obj: ioBroker.HostObject | null | undefined, localIp: string): string | null { const networkInterfaces = obj?.native?.hardware?.networkInterfaces; if (!networkInterfaces) { return null; } - let hostIp; + let hostIp: string | null = null; Object.keys(networkInterfaces).forEach(inter => { - networkInterfaces[inter].forEach(ip => { + networkInterfaces[inter]?.forEach(ip => { if (ip.internal) { return; } else if (localIp.includes(':') && ip.family !== 'IPv6') { @@ -216,7 +257,7 @@ class Server extends Component { if (!hostIp) { Object.keys(networkInterfaces).forEach(inter => { - networkInterfaces[inter].forEach(ip => { + networkInterfaces[inter]?.forEach(ip => { if (ip.internal) { return; } else if (localIp.includes(':') && ip.family !== 'IPv6') { @@ -236,7 +277,7 @@ class Server extends Component { if (!hostIp) { Object.keys(networkInterfaces).forEach(inter => { - networkInterfaces[inter].forEach(ip => { + networkInterfaces[inter]?.forEach(ip => { if (ip.internal) { return; } @@ -248,8 +289,8 @@ class Server extends Component { return hostIp; } - getWebInstances() { - this.props.socket.getAdapterInstances('web').then(async list => { + getWebInstances(): void { + void this.props.socket.getAdapterInstances('web').then(async list => { let webInstance; if (this.props.native.webInstance === '*') { webInstance = list[0]; @@ -261,7 +302,9 @@ class Server extends Component { webInstance.native = webInstance.native || {}; if (!webInstance.native.bind || webInstance.native.bind === '0.0.0.0') { // get current host - const host = await this.props.socket.getObject(`system.host.${webInstance.common.host}`); + const host: ioBroker.HostObject | null | undefined = await this.props.socket.getObject( + `system.host.${webInstance.common.host}`, + ); // get ips on this host const ip = Server.findNetworkAddressOfHost(host, window.location.hostname); @@ -275,39 +318,42 @@ class Server extends Component { }); } - renderMessage() { + renderMessage(): JSX.Element | null { if (this.state.message) { const text = this.state.message.split('\n').map((item, i) =>

{item}

); return ( this.setState({ message: '' })} /> ); - } else { - return null; } + + return null; } - static getDerivedStateFromProps(props, state) { + static getDerivedStateFromProps(props: ServerProps, state: ServerState): Partial | null { if (state.instanceAlive !== props.instanceAlive) { return { instanceAlive: props.instanceAlive }; - } else { - return null; } + return null; } - onTest() { + onTest(): void { const settings = JSON.parse(this.state.editedSettings || this.state.editedSettingsOld); - let timeout = setTimeout(() => { - timeout = null; - this.setState({ message: 'Timeout', requesting: false }); - }, settings.timeout || this.props.native.defaultTimeout); + let timeout: ReturnType | null = setTimeout( + () => { + timeout = null; + this.setState({ message: 'Timeout', requesting: false }); + }, + parseInt(settings.timeout || this.props.native.defaultTimeout, 10) || 1000, + ); this.setState({ requesting: true, testImg: null }, () => { - this.props.socket + void this.props.socket .sendTo(`${this.props.adapterName}.${this.props.instance}`, 'test', settings) .then(result => { timeout && clearTimeout(timeout); @@ -327,30 +373,30 @@ class Server extends Component { }); } - onCameraSettingsChanged(settings) { - const oldSettings = JSON.parse(this.state.editedSettingsOld); + onCameraSettingsChanged(settings: CameraSettings): void { + const oldSettings: CameraSettings = JSON.parse(this.state.editedSettingsOld); // apply changes settings = Object.assign(oldSettings, settings); - const editedSettings = JSON.stringify(settings); + const editedSettings: string = JSON.stringify(settings); if (this.state.editedSettingsOld === editedSettings) { - this.setState({ editChanged: false, editedSettings: null }); + this.setState({ editChanged: false, editedSettings: '' }); } else if (this.state.editedSettingsOld !== editedSettings) { this.setState({ editChanged: true, editedSettings }); } } - renderConfigDialog() { - if (this.state.editCam !== false) { - const cam = JSON.parse(this.state.editedSettings || this.state.editedSettingsOld); - let Config = (TYPES[cam.type] || TYPES.url).Config; + renderConfigDialog(): JSX.Element | null { + if (this.state.editCam) { + const cam: CameraSettings = JSON.parse(this.state.editedSettings || this.state.editedSettingsOld); + const Config: React.FC = TYPES[cam.type].Config; return ( this.state.editCam !== null && this.setState({ editCam: false, editChanged: false })} + onClose={() => this.state.editCam && this.setState({ editCam: '', editChanged: false })} > {I18n.t('Edit camera %s [%s]', cam.name, cam.type)} - {cam.desc} @@ -359,13 +405,20 @@ class Server extends Component {
this.onCameraSettingsChanged(settings)} - encrypt={(value, cb) => this.props.encrypt(value, cb)} - decrypt={(value, cb) => this.props.decrypt(value, cb)} + onChange={(settings: Record) => + this.onCameraSettingsChanged(settings as CameraSettings) + } + encrypt={(value: string, cb: (text: string) => void) => + this.props.encrypt(value, cb) + } + decrypt={(value: string, cb: (text: string) => void) => + this.props.decrypt(value, cb) + } />
{ - const cameras = JSON.parse(JSON.stringify(this.props.native.cameras)); + const cameras: CameraSettings[] = JSON.parse(JSON.stringify(this.props.native.cameras)); if (this.state.editedSettings) { - cameras[this.state.editCam] = JSON.parse(this.state.editedSettings); + cameras[parseInt(this.state.editCam, 10)] = JSON.parse(this.state.editedSettings); this.props.onChange('cameras', cameras, () => - this.setState({ editCam: false, editChanged: false }), + this.setState({ editCam: '', editChanged: false }), ); } else { - this.setState({ editCam: false, editChanged: false }); + this.setState({ editCam: '', editChanged: false }); } }} color="primary" @@ -491,28 +544,26 @@ class Server extends Component {
); - } else { - return null; } + return null; } - renderCameraButtons(cam, i) { + renderCameraButtons(i: number): JSX.Element[] { return [ { - let editedSettingsOld = JSON.parse(JSON.stringify(this.props.native.cameras[i])); - editedSettingsOld = JSON.stringify(editedSettingsOld); - this.setState({ editCam: i, editedSettingsOld, editedSettings: null, testImg: null }); + const editedSettingsOld = JSON.stringify(this.props.native.cameras[i]); + this.setState({ editCam: i.toString(), editedSettingsOld, editedSettings: '', testImg: '' }); }} > @@ -524,7 +575,7 @@ class Server extends Component { key="up" style={styles.lineUp} onClick={() => { - const cameras = JSON.parse(JSON.stringify(this.props.native.cameras)); + const cameras: CameraSettings[] = JSON.parse(JSON.stringify(this.props.native.cameras)); const cam = cameras[i]; cameras.splice(i, 1); cameras.splice(i - 1, 0, cam); @@ -548,7 +599,7 @@ class Server extends Component { key="down" style={styles.lineDown} onClick={() => { - const cameras = JSON.parse(JSON.stringify(this.props.native.cameras)); + const cameras: CameraSettings[] = JSON.parse(JSON.stringify(this.props.native.cameras)); const cam = cameras[i]; cameras.splice(i, 1); cameras.splice(i + 1, 0, cam); @@ -571,7 +622,7 @@ class Server extends Component { key="delete" style={styles.lineDelete} onClick={() => { - const cameras = JSON.parse(JSON.stringify(this.props.native.cameras)); + const cameras: CameraSettings[] = JSON.parse(JSON.stringify(this.props.native.cameras)); cameras.splice(i, 1); this.props.onChange('cameras', cameras); }} @@ -581,9 +632,9 @@ class Server extends Component { ]; } - renderCamera(cam, i) { - const error = this.props.native.cameras.find((c, ii) => c.name === cam.name && ii !== i); - this.props.native.cameras.forEach((cam, i) => { + renderCamera(cam: CameraSettings, i: number): JSX.Element { + const error = !!this.props.native.cameras.find((c, ii: number) => c.name === cam.name && ii !== i); + this.props.native.cameras.forEach((cam, i: number) => { if (!cam.id) { cam.id = Date.now() + i; } @@ -601,15 +652,14 @@ class Server extends Component { return (
{ - const cameras = JSON.parse(JSON.stringify(this.props.native.cameras)); + const cameras: CameraSettings[] = JSON.parse(JSON.stringify(this.props.native.cameras)); cameras[i].enabled = cameras[i].enabled === undefined ? false : !cameras[i].enabled; this.props.onChange('cameras', cameras); }} @@ -624,7 +674,7 @@ class Server extends Component { value={cam.name || ''} helperText={error ? I18n.t('Duplicate name') : ''} onChange={e => { - const cameras = JSON.parse(JSON.stringify(this.props.native.cameras)); + const cameras: CameraSettings[] = JSON.parse(JSON.stringify(this.props.native.cameras)); cameras[i].name = e.target.value.replace(/[^-_\da-zA-Z]/g, '_'); this.props.onChange('cameras', cameras); }} @@ -637,7 +687,7 @@ class Server extends Component { label={I18n.t('Description')} value={cam.desc || ''} onChange={e => { - const cameras = JSON.parse(JSON.stringify(this.props.native.cameras)); + const cameras: CameraSettings[] = JSON.parse(JSON.stringify(this.props.native.cameras)); cameras[i].desc = e.target.value; this.props.onChange('cameras', cameras); }} @@ -653,15 +703,18 @@ class Server extends Component { variant="standard" value={cam.type || ''} onChange={e => { - const cameras = JSON.parse(JSON.stringify(this.props.native.cameras)); + const cameras: GenericCameraSettings[] = JSON.parse( + JSON.stringify(this.props.native.cameras), + ); const camera = cameras[i]; cameras[i] = { - type: e.target.value, + id: camera.id, + type: e.target.value as CameraType, desc: camera.desc, name: camera.name, enabled: camera.enabled, ip: camera.ip, - rtsp: TYPES[e.target.value].rtsp, + rtsp: TYPES[e.target.value as CameraType].rtsp, }; this.props.onChange('cameras', cameras); }} @@ -671,28 +724,27 @@ class Server extends Component { key={type} value={type} > - {TYPES[type].name || type} + {TYPES[type as CameraType].name || type} ))}
- {this.renderCameraButtons(cam, i)} + {this.renderCameraButtons(i)} {description ?
{description}
: null}
); } - render() { + render(): JSX.Element { return (
{ - const cameras = JSON.parse(JSON.stringify(this.props.native.cameras)); + const cameras: GenericCameraSettings[] = JSON.parse(JSON.stringify(this.props.native.cameras)); let i = 1; - // eslint-disable-next-line while (cameras.find(cam => cam.name === `cam${i}`)) { i++; } @@ -703,7 +755,7 @@ class Server extends Component { {this.props.native.cameras - ? this.props.native.cameras.map((cam, i) => this.renderCamera(cam, i)) + ? this.props.native.cameras.map((cam, i: number) => this.renderCamera(cam, i)) : null} {this.renderConfigDialog()} {this.renderMessage()} @@ -712,17 +764,4 @@ class Server extends Component { } } -Server.propTypes = { - decrypt: PropTypes.func.isRequired, - encrypt: PropTypes.func.isRequired, - native: PropTypes.object.isRequired, - instance: PropTypes.number.isRequired, - adapterName: PropTypes.string.isRequired, - onError: PropTypes.func, - onLoad: PropTypes.func, - onChange: PropTypes.func, - socket: PropTypes.object.isRequired, - themeType: PropTypes.string.isRequired, -}; - export default Server; diff --git a/src/src/Tabs/Options.js b/src/src/Tabs/Options.tsx similarity index 78% rename from src/src/Tabs/Options.js rename to src/src/Tabs/Options.tsx index 8757f1f..d07b8dc 100644 --- a/src/src/Tabs/Options.js +++ b/src/src/Tabs/Options.tsx @@ -1,13 +1,21 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; +import React, { Component, type JSX } from 'react'; import { TextField, Snackbar, IconButton, FormControl, Select, Button, MenuItem, InputLabel } from '@mui/material'; import { MdClose as IconClose, MdCheck as IconTest } from 'react-icons/md'; -import { I18n, Logo, Message, Error as DialogError } from '@iobroker/adapter-react-v5'; +import { + I18n, + Logo, + Message, + Error as DialogError, + type AdminConnection, + type ThemeType, + type IobTheme, +} from '@iobroker/adapter-react-v5'; +import type { CamerasInstanceNative } from '../types'; -const styles = { +const styles: Record = { tab: { width: '100%', minHeight: '100%', @@ -31,8 +39,34 @@ const styles = { }, }; -class Options extends Component { - constructor(props) { +interface OptionsProps { + common: ioBroker.InstanceCommon | null; + native: CamerasInstanceNative; + instanceAlive: boolean; + instance: number; + adapterName: string; + onError: (text: string) => void; + onLoad: (native: Record) => void; + onChange: (attr: string, value: any, cb?: () => void) => void; + socket: AdminConnection; + themeType: ThemeType; + theme: IobTheme; + getIpAddresses: (host?: string, update?: boolean) => Promise; + getExtendableInstances: () => Promise; +} + +interface OptionsState { + showHint: boolean; + toast: string; + ips: string[]; + requesting: boolean; + webInstances: string[]; + errorText: string; + messageText: string; +} + +class Options extends Component { + constructor(props: OptionsProps) { super(props); this.state = { @@ -41,29 +75,22 @@ class Options extends Component { ips: [], requesting: true, webInstances: [], + errorText: '', + messageText: '', }; } - componentDidMount() { - let ips; - this.props - .getIpAddresses() - .then(_ips => (ips = _ips)) - .then(() => this.props.getExtendableInstances()) - .then(webInstances => - this.setState({ - requesting: false, - ips, - webInstances: webInstances.map(item => item._id.replace('system.adapter.', '')), - }), - ); - } - - showError(text) { - this.setState({ errorText: text }); + async componentDidMount(): Promise { + const ips = await this.props.getIpAddresses(); + const webInstances: ioBroker.InstanceObject[] = await this.props.getExtendableInstances(); + this.setState({ + requesting: false, + ips, + webInstances: webInstances.map(item => item._id.replace('system.adapter.', '')), + }); } - renderError() { + renderError(): JSX.Element | null { if (!this.state.errorText) { return null; } @@ -76,7 +103,7 @@ class Options extends Component { ); } - renderToast() { + renderToast(): JSX.Element | null { if (!this.state.toast) { return null; } @@ -108,7 +135,7 @@ class Options extends Component { ); } - renderHint() { + renderHint(): JSX.Element | null { if (this.state.showHint) { return ( this.setState({ showHint: false })} /> ); - } else { - return null; } + return null; } - onTestFfmpeg() { - let timeout = setTimeout(() => { + onTestFfmpeg(): void { + let timeout: ReturnType | null = setTimeout(() => { timeout = null; this.setState({ toast: 'Timeout', requesting: false }); }, 30000); this.setState({ requesting: true }, () => { - this.props.socket + void this.props.socket .sendTo(`${this.props.adapterName}.${this.props.instance}`, 'ffmpeg', { path: this.props.native.ffmpegPath, }) - .then(result => { + .then((result: { version?: string; error?: string }) => { timeout && clearTimeout(timeout); if (!result?.version || result.error) { let error = result?.error ? result.error : I18n.t('No answer'); @@ -147,7 +173,7 @@ class Options extends Component { }); } - renderSettings() { + renderSettings(): JSX.Element[] { return [ this.state.ips && this.state.ips.length ? ( this.props.onChange('defaultCacheTimeout', e.target.value)} - helperText={I18n.t( - 'How often the cameras will be ascked for new snapshot. If 0, then by every request', - )} + helperText={I18n.t('How often the cameras will be asked for new snapshot. If 0, then by every request')} />,
, this.setState({ messageText: '' })} - > - {this.state.messageText} -
+ /> ); } - render() { + render(): JSX.Element { return (
) => void; + native: Record; + decrypt: (textToDecrypt: string, cb: (decryptedText: string) => void) => void; + encrypt: (textToEncrypt: string, cb: (encryptedText: string) => void) => void; + themeType: ThemeType; + settings: Record; + theme: IobTheme; +} + +class GenericConfig extends Component { + protected constructor(props: GenericConfigProps) { + super(props); + + this.state = JSON.parse(JSON.stringify(this.props.settings)); + } + + // eslint-disable-next-line class-methods-use-this,react/no-unused-class-component-methods + reportSettings(): void {} +} + +export default GenericConfig; diff --git a/src/src/Types/RTSPEufy.js b/src/src/Types/RTSPEufy.tsx similarity index 70% rename from src/src/Types/RTSPEufy.js rename to src/src/Types/RTSPEufy.tsx index 2c111a7..31e42fb 100644 --- a/src/src/Types/RTSPEufy.js +++ b/src/src/Types/RTSPEufy.tsx @@ -1,11 +1,11 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; +import React, { type JSX } from 'react'; import { Button, Switch, TextField } from '@mui/material'; import { I18n, SelectID } from '@iobroker/adapter-react-v5'; +import GenericConfig, { type GenericCameraSettings, type GenericConfigProps } from '../Types/GenericConfig'; -const styles = { +const styles: Record = { page: { width: '100%', }, @@ -28,24 +28,30 @@ const styles = { }, }; -class RTSPEufyConfig extends Component { - constructor(props) { - super(props); +export interface RTSPEufySettings extends GenericCameraSettings { + ip: string; + oid: string; + useOid: boolean; + eusecInstalled: boolean; + showSelectId: boolean; +} - const state = JSON.parse(JSON.stringify(this.props.settings)); +class RTSPEufyConfig extends GenericConfig { + constructor(props: GenericConfigProps) { + super(props); // set default values - state.ip = state.ip || ''; - state.oid = state.oid || ''; - state.useOid = state.useOid || false; - state.eusecInstalled = false; - - this.state = state; + Object.assign(this.state, { + ip: this.state.ip || '', + oid: this.state.oid || '', + useOid: this.state.useOid || false, + eusecInstalled: false, + }); } - componentDidMount() { + componentDidMount(): void { // read if eusec adapter is installed - this.props.socket.getAdapterInstances('eusec').then(instances => { + void this.props.socket.getAdapterInstances('eusec').then(instances => { if (this.state.useOid && !instances.length) { this.setState({ useOid: false }); } else { @@ -54,7 +60,7 @@ class RTSPEufyConfig extends Component { }); } - reportSettings() { + reportSettings(): void { this.props.onChange({ ip: this.state.ip, oid: this.state.oid, @@ -62,28 +68,34 @@ class RTSPEufyConfig extends Component { }); } - renderSelectID() { + renderSelectID(): JSX.Element | null { if (!this.state.showSelectId) { return null; } return ( obj._id.startsWith('eusec.') && obj._id.endsWith('.rtsp_stream_url')} - statesOnly={true} onClose={() => this.setState({ showSelectId: false })} - onOk={oid => { - this.setState({ oid, showSelectId: false }, () => this.reportSettings()); + onOk={(oid: string | string[] | undefined) => { + if (oid && typeof oid === 'object') { + this.setState({ oid: oid[0], showSelectId: false }, () => this.reportSettings()); + } else if (oid) { + this.setState({ oid, showSelectId: false }, () => this.reportSettings()); + } else { + this.setState({ showSelectId: false }, () => this.reportSettings()); + } }} /> ); } - render() { + render(): JSX.Element { return (
{this.renderSelectID()} @@ -130,14 +142,4 @@ class RTSPEufyConfig extends Component { } } -RTSPEufyConfig.propTypes = { - socket: PropTypes.object, - onChange: PropTypes.func, - native: PropTypes.object, - defaultTimeout: PropTypes.number, - decode: PropTypes.func, - encode: PropTypes.func, - themeType: PropTypes.string, -}; - export default RTSPEufyConfig; diff --git a/src/src/Types/RTSPHiKam.js b/src/src/Types/RTSPHiKam.tsx similarity index 72% rename from src/src/Types/RTSPHiKam.js rename to src/src/Types/RTSPHiKam.tsx index 4a2399f..2e64863 100644 --- a/src/src/Types/RTSPHiKam.js +++ b/src/src/Types/RTSPHiKam.tsx @@ -1,11 +1,12 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; +import React, { type JSX } from 'react'; import { MenuItem, Select, TextField } from '@mui/material'; import { I18n } from '@iobroker/adapter-react-v5'; -const styles = { +import GenericConfig, { type GenericCameraSettings, type GenericConfigProps } from '../Types/GenericConfig'; + +const styles: Record = { page: { width: '100%', }, @@ -28,30 +29,31 @@ const styles = { }, }; -class RTSPHiKamConfig extends Component { - constructor(props) { - super(props); +export interface RTSPHiKamSettings extends GenericCameraSettings { + ip: string; + password: string; + username: string; + quality: 'high' | 'low'; +} - const state = JSON.parse(JSON.stringify(this.props.settings)); +class RTSPHiKamConfig extends GenericConfig { + constructor(props: GenericConfigProps) { + super(props); // set default values - state.ip = state.ip || ''; - state.password = state.password || ''; - state.username = state.username === undefined ? 'admin' : state.username || ''; - state.quality = state.quality || 'low'; - - this.state = state; - } - - static getRtsp() { - return true; // this camera can be used in RTSP snapshot + Object.assign(this.state, { + ip: this.state.ip || '', + password: this.state.password || '', + username: this.state.username === undefined ? 'admin' : this.state.username || '', + quality: this.state.quality || 'low', + }); } - componentDidMount() { + componentDidMount(): void { this.props.decrypt(this.state.password, password => this.setState({ password })); } - reportSettings() { + reportSettings(): void { this.props.encrypt(this.state.password, password => { this.props.onChange({ ip: this.state.ip, @@ -62,7 +64,7 @@ class RTSPHiKamConfig extends Component { }); } - render() { + render(): JSX.Element { return (
@@ -97,7 +99,9 @@ class RTSPHiKamConfig extends Component { variant="standard" value={this.state.quality} label={I18n.t('Quality')} - onChange={e => this.setState({ quality: e.target.value }, () => this.reportSettings())} + onChange={e => + this.setState({ quality: e.target.value as 'high' | 'low' }, () => this.reportSettings()) + } > {I18n.t('low quality')} {I18n.t('high quality')} @@ -108,12 +112,4 @@ class RTSPHiKamConfig extends Component { } } -RTSPHiKamConfig.propTypes = { - onChange: PropTypes.func, - native: PropTypes.object, - defaultTimeout: PropTypes.number, - decode: PropTypes.func, - encode: PropTypes.func, -}; - export default RTSPHiKamConfig; diff --git a/src/src/Types/RTSPImage.js b/src/src/Types/RTSPImage.tsx similarity index 79% rename from src/src/Types/RTSPImage.js rename to src/src/Types/RTSPImage.tsx index 80f88d5..005fe0e 100644 --- a/src/src/Types/RTSPImage.js +++ b/src/src/Types/RTSPImage.tsx @@ -1,11 +1,11 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; +import React, { type JSX } from 'react'; import { TextField, Checkbox, FormControlLabel, Select, MenuItem, FormControl, InputLabel } from '@mui/material'; import { I18n } from '@iobroker/adapter-react-v5'; +import GenericConfig, { type GenericCameraSettings, type GenericConfigProps } from '../Types/GenericConfig'; -const styles = { +const styles: Record = { page: { width: '100%', }, @@ -31,7 +31,7 @@ const styles = { }, urlPath: { marginTop: 16, - marginBotton: `24px !important`, + marginBottom: `24px !important`, width: 408, }, width: { @@ -68,37 +68,50 @@ const styles = { }, }; -class RTSPImageConfig extends Component { - constructor(props) { - super(props); - - const state = JSON.parse(JSON.stringify(this.props.settings)); +export interface RTSPImageSettings extends GenericCameraSettings { + ip: string; + port: string; + urlPath: string; + password: string; + username: string; + url: string; + originalWidth: string; + originalHeight: string; + prefix: string; + suffix: string; + protocol: 'tcp' | 'udp'; +} - // set default values - state.ip = state.ip || ''; - state.port = state.port || '554'; - state.urlPath = state.urlPath || ''; - state.password = state.password || ''; - state.username = state.username === undefined ? 'admin' : state.username || ''; - state.url = `rtsp://${state.username ? `${state.username}:***@` : ''}${state.ip}:${state.port}${state.urlPath ? (state.urlPath.startsWith('/') ? state.urlPath : `/${state.urlPath}`) : ''}`; - state.originalWidth = state.originalWidth || ''; - state.originalHeight = state.originalHeight || ''; - state.prefix = state.prefix || ''; - state.suffix = state.suffix || ''; - state.protocol = state.protocol || 'udp'; +interface RTSPImageConfigState extends RTSPImageSettings { + expertMode: boolean; +} - this.state = state; - } +class RTSPImageConfig extends GenericConfig { + constructor(props: GenericConfigProps) { + super(props); - static getRtsp() { - return true; // this camera can be used in RTSP snapshot + // set default values + Object.assign(this.state, { + ip: this.state.ip || '', + port: this.state.port || '554', + urlPath: this.state.urlPath || '', + password: this.state.password || '', + username: this.state.username === undefined ? 'admin' : this.state.username || '', + url: `rtsp://${this.state.username ? `${this.state.username}:***@` : ''}${this.state.ip}:${this.state.port}${this.state.urlPath ? (this.state.urlPath.startsWith('/') ? this.state.urlPath : `/${this.state.urlPath}`) : ''}`, + originalWidth: this.state.originalWidth || '', + originalHeight: this.state.originalHeight || '', + prefix: this.state.prefix || '', + suffix: this.state.suffix || '', + protocol: this.state.protocol || 'udp', + expertMode: false, + }); } - componentDidMount() { + componentDidMount(): void { this.props.decrypt(this.state.password, password => this.setState({ password })); } - reportSettings() { + reportSettings(): void { this.props.encrypt(this.state.password, password => { this.props.onChange({ ip: this.state.ip, @@ -115,7 +128,7 @@ class RTSPImageConfig extends Component { }); } - buildCommand(options) { + buildCommand(options: RTSPImageSettings): string[] { const parameters = ['-y']; options.prefix && parameters.push(options.prefix); parameters.push('-rtsp_transport'); @@ -138,7 +151,7 @@ class RTSPImageConfig extends Component { return parameters; } - render() { + render(): JSX.Element { return (
@@ -159,13 +172,17 @@ class RTSPImageConfig extends Component { /> {I18n.t('Protocol')}