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 0000000..dae755a Binary files /dev/null and b/src/public/cameras.png differ 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')}