@vixoniccom/aqi 0.0.1-dev.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build.zip ADDED
Binary file
@@ -0,0 +1,75 @@
1
+ import {
2
+ Label,
3
+ SelectAssetKna,
4
+ SelectInput,
5
+ TextFormat,
6
+ TextInput,
7
+ } from '@vixoniccom/modules'
8
+
9
+ export const appeareanceInputs = [
10
+ new Label({ label: 'General' }),
11
+ new TextInput({
12
+ id: 'padding',
13
+ label: 'Márgenes',
14
+ required: false,
15
+ description:
16
+ 'CSS para mover la tarjeta. El formato es arriba/derecha/abajo/izquieda',
17
+ }),
18
+ new SelectAssetKna({
19
+ id: 'backgroundImage',
20
+ label: 'Fondo',
21
+ required: false,
22
+ extensions: ['jpg', 'png'],
23
+ }),
24
+
25
+ new Label({ label: 'Diseño de la tarjeta' }),
26
+ new TextInput({
27
+ id: 'cardWidth',
28
+ label: 'Ancho',
29
+ required: false,
30
+ description: 'Ancho de la tarjeta',
31
+ }),
32
+ new TextInput({
33
+ id: 'cardHeight',
34
+ label: 'Alto',
35
+ required: false,
36
+ description: 'Altura de la tarjeta',
37
+ }),
38
+ new TextInput({
39
+ id: 'cardGap',
40
+ label: 'Separación entre elementos',
41
+ required: false,
42
+ description: 'Separación entre los elementos de la tarjeta',
43
+ }),
44
+
45
+ new Label({ label: 'Textos de la tarjeta' }),
46
+ new TextFormat({
47
+ id: 'aqiFormat',
48
+ label: 'Formato del índice de calidad del aire',
49
+ }),
50
+ new TextFormat({
51
+ id: 'stationFormat',
52
+ label: 'Formato de estación que realizó la medición',
53
+ }),
54
+ new TextFormat({
55
+ id: 'qualityFormat',
56
+ label: 'Formato del mensaje sobre la calidad del aire',
57
+ }),
58
+
59
+ new Label({ label: 'Datos' }),
60
+ new SelectInput({
61
+ id: 'updateData',
62
+ label: 'Frecuencia con la que se piden datos',
63
+ items: [
64
+ { label: '1 minuto', value: 1 },
65
+ { label: '5 minutos', value: 5 },
66
+ { label: '10 minutos', value: 10 },
67
+ { label: '30 minutos', value: 30 },
68
+ { label: '45 minutos', value: 45 },
69
+ { label: '1 hora', value: 60 },
70
+ ],
71
+ defaultValue: 1,
72
+ }),
73
+ new TextInput({ id: 'msj0', label: 'Mensaje sin Datos' }),
74
+ new TextFormat({ id: 'formatMjs', label: 'Formato Mensaje sin Datos' }),
75
+ ]
@@ -0,0 +1,8 @@
1
+ import { Group } from "@vixoniccom/modules";
2
+ import { appeareanceInputs } from "./AppeareanceInputs";
3
+
4
+ export const appeareanceGroup = new Group({
5
+ id: 'appeareanceGroup',
6
+ label: 'Apariencia',
7
+ items: [...appeareanceInputs]
8
+ })
@@ -0,0 +1,8 @@
1
+ import { Configuration } from '@vixoniccom/modules'
2
+ import { appeareanceGroup } from './appeareanceGroup'
3
+
4
+ export const configuration = new Configuration({
5
+ schema: [
6
+ appeareanceGroup
7
+ ]
8
+ })
@@ -0,0 +1,123 @@
1
+ {
2
+ "schema": [
3
+ {
4
+ "id": "appeareanceGroup",
5
+ "label": "Apariencia",
6
+ "type": "group",
7
+ "items": [
8
+ {
9
+ "type": "label",
10
+ "label": "General"
11
+ },
12
+ {
13
+ "id": "padding",
14
+ "label": "Márgenes",
15
+ "type": "text-input",
16
+ "description": "CSS para mover la tarjeta. El formato es arriba/derecha/abajo/izquieda",
17
+ "required": false
18
+ },
19
+ {
20
+ "id": "backgroundImage",
21
+ "label": "Fondo",
22
+ "type": "select-asset-kna-input",
23
+ "required": false,
24
+ "extensions": [
25
+ "jpg",
26
+ "png"
27
+ ],
28
+ "multiple": false
29
+ },
30
+ {
31
+ "type": "label",
32
+ "label": "Diseño de la tarjeta"
33
+ },
34
+ {
35
+ "id": "cardWidth",
36
+ "label": "Ancho",
37
+ "type": "text-input",
38
+ "description": "Ancho de la tarjeta",
39
+ "required": false
40
+ },
41
+ {
42
+ "id": "cardHeight",
43
+ "label": "Alto",
44
+ "type": "text-input",
45
+ "description": "Altura de la tarjeta",
46
+ "required": false
47
+ },
48
+ {
49
+ "id": "cardGap",
50
+ "label": "Separación entre elementos",
51
+ "type": "text-input",
52
+ "description": "Separación entre los elementos de la tarjeta",
53
+ "required": false
54
+ },
55
+ {
56
+ "type": "label",
57
+ "label": "Textos de la tarjeta"
58
+ },
59
+ {
60
+ "id": "aqiFormat",
61
+ "label": "Formato del índice de calidad del aire",
62
+ "type": "text-format"
63
+ },
64
+ {
65
+ "id": "stationFormat",
66
+ "label": "Formato de estación que realizó la medición",
67
+ "type": "text-format"
68
+ },
69
+ {
70
+ "id": "qualityFormat",
71
+ "label": "Formato del mensaje sobre la calidad del aire",
72
+ "type": "text-format"
73
+ },
74
+ {
75
+ "type": "label",
76
+ "label": "Datos"
77
+ },
78
+ {
79
+ "id": "updateData",
80
+ "label": "Frecuencia con la que se piden datos",
81
+ "type": "select-input",
82
+ "items": [
83
+ {
84
+ "label": "1 minuto",
85
+ "value": 1
86
+ },
87
+ {
88
+ "label": "5 minutos",
89
+ "value": 5
90
+ },
91
+ {
92
+ "label": "10 minutos",
93
+ "value": 10
94
+ },
95
+ {
96
+ "label": "30 minutos",
97
+ "value": 30
98
+ },
99
+ {
100
+ "label": "45 minutos",
101
+ "value": 45
102
+ },
103
+ {
104
+ "label": "1 hora",
105
+ "value": 60
106
+ }
107
+ ],
108
+ "defaultValue": 1
109
+ },
110
+ {
111
+ "id": "msj0",
112
+ "label": "Mensaje sin Datos",
113
+ "type": "text-input"
114
+ },
115
+ {
116
+ "id": "formatMjs",
117
+ "label": "Formato Mensaje sin Datos",
118
+ "type": "text-format"
119
+ }
120
+ ]
121
+ }
122
+ ]
123
+ }
package/icon.png ADDED
Binary file
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@vixoniccom/aqi",
3
+ "alias": "Air-Quality-Index",
4
+ "description": "Muestra información sobre la calidad del aire en una ubicación específica",
5
+ "color": "#3395FF",
6
+ "license": "ISC",
7
+ "tags": [],
8
+ "author": {
9
+ "name": "Daniel Alvayay"
10
+ },
11
+ "version": "0.0.1-dev.0",
12
+ "scripts": {
13
+ "prepublishOnly": "vixonic-module-packager --mode=build",
14
+ "watch": "vixonic-module-packager --mode=watch",
15
+ "run": "vixonic-module-packager --mode=run",
16
+ "configuration": "vixonic-module-packager --mode generate-configuration",
17
+ "configuration:validate": "vixonic-module-packager --mode validate-configuration",
18
+ "release": "standard-version",
19
+ "prerelease-beta": "standard-version --prerelease beta",
20
+ "prerelease-dev": "standard-version --prerelease dev"
21
+ },
22
+ "dependencies": {
23
+ "axios": "^0.21.1",
24
+ "localforage": "^1.10.0",
25
+ "react": "^16.14.0",
26
+ "react-dom": "^16.14.0"
27
+ },
28
+ "devDependencies": {
29
+ "@types/react": "^16.14.14",
30
+ "@types/react-dom": "^16.9.14",
31
+ "@vixoniccom/module-packager": "^2.6.0",
32
+ "@vixoniccom/modules": "^2.16.0",
33
+ "standard-version": "^9.3.1"
34
+ }
35
+ }
package/src/App.tsx ADDED
@@ -0,0 +1,114 @@
1
+ import axios from 'axios'
2
+ import React, { useEffect, useState } from 'react'
3
+ import { Card } from './components/Card'
4
+ import { FontLoader } from './components/FontLoader'
5
+ import { FormattedText } from './components/FormattedText'
6
+ import { AirQuality, StorageData } from './types'
7
+ import { API_RESPONSE_STATUS } from './utils'
8
+ import localforage from 'localforage'
9
+
10
+ interface Props {
11
+ data: VixonicData
12
+ start: boolean
13
+ }
14
+
15
+ export const App: React.FunctionComponent<Props> = ({ data, start }) => {
16
+ const { parameters, downloadsPath } = data
17
+ const backgroundImageState = parameters.backgroundImage ? `url('${downloadsPath}/${parameters.backgroundImage.filename}')` : ''
18
+ const [formattedData, setFormattedData] = useState<AirQuality | null>()
19
+ const updateTime = (parameters?.updateData || 1) * 60000
20
+
21
+ const refetchData = (referenceDate: Date): boolean => {
22
+ const now = new Date()
23
+
24
+ const sameYear = referenceDate.getFullYear() === now.getFullYear()
25
+ const sameMonth = referenceDate.getMonth() === now.getMonth()
26
+ const sameDay = referenceDate.getDate() === now.getDate()
27
+
28
+ if (sameYear && sameMonth && sameDay) {
29
+ const differenceMinutes = Math.abs(now.getTime() - referenceDate.getTime())
30
+ return differenceMinutes >= updateTime
31
+ }
32
+
33
+ return true
34
+ }
35
+
36
+ const requestData = async (): Promise<AirQuality | null> => {
37
+ const TOKEN = "9fa7d8df1a621c9d518a351a13ff9b94093b9dac"
38
+ const URL = `https://api.waqi.info/feed/here/?token=${TOKEN}`
39
+ try {
40
+ const response = await axios.get<AirQuality>(URL)
41
+ if (response?.status === 200) {
42
+ return response.data
43
+ }
44
+ return null
45
+ } catch (error) {
46
+ console.log(error)
47
+ return null
48
+ }
49
+ }
50
+
51
+ const getData = async () => {
52
+ const storageData: StorageData | null = await localforage.getItem('aqi')
53
+ let data
54
+ if (!storageData?.item || refetchData(storageData?.date ?? new Date(0))) {
55
+ data = await requestData()
56
+ await localforage.setItem('aqi', { item: data, date: new Date() })
57
+ } else {
58
+ data = storageData.item
59
+ }
60
+ setFormattedData(data)
61
+ }
62
+
63
+ useEffect(() => {
64
+ if (!start) return
65
+ getData()
66
+ const interval = setInterval(() => {
67
+ getData()
68
+ }, updateTime)
69
+ return () => clearInterval(interval)
70
+ }, [start, updateTime])
71
+
72
+ return (
73
+ <div
74
+ style={{
75
+ position: 'absolute',
76
+ top: 0,
77
+ bottom: 0,
78
+ left: 0,
79
+ right: 0,
80
+ backgroundImage: backgroundImageState,
81
+ backgroundSize: '100% 100%',
82
+ padding: parameters?.padding
83
+ }}
84
+ >
85
+ {formattedData?.status === API_RESPONSE_STATUS.OK ? (
86
+ <div>
87
+ <FontLoader paths={['aqiFormat.font', 'stationFormat.font', 'qualityFormat.font']} parameters={parameters} downloadsPath={downloadsPath} />
88
+
89
+ <Card data={formattedData?.data}
90
+ format={{
91
+ cardWidth: parameters?.cardWidth,
92
+ cardHeight: parameters?.cardHeight,
93
+ cardGap: parameters?.cardGap,
94
+ aqiFormat: parameters?.aqiFormat,
95
+ stationFormat: parameters?.stationFormat,
96
+ qualityFormat: parameters?.qualityFormat
97
+ }} />
98
+ </div>
99
+ ) : (
100
+ <div>
101
+ <FontLoader paths={['formatMjs.font']} parameters={parameters} downloadsPath={downloadsPath} />
102
+ <div style={{
103
+ display: 'flex',
104
+ position: 'relative',
105
+ flex: '1 1 0%',
106
+ flexDirection: 'column',
107
+ }}>
108
+ <FormattedText text={parameters?.msj0 || 'No hay datos para mostrar'} format={parameters?.formatMjs} />
109
+ </div>
110
+ </div>
111
+ )}
112
+ </div>
113
+ )
114
+ }
@@ -0,0 +1,43 @@
1
+ import React from 'react'
2
+ import { assingAirQuality } from '../utils'
3
+ import { Data } from '../types';
4
+ import { TextFormat } from '@vixoniccom/modules'
5
+ import { FormattedText } from './FormattedText'
6
+
7
+ interface Props {
8
+ data: Data;
9
+ format: {
10
+ cardWidth?: string;
11
+ cardHeight?: string;
12
+ cardGap?: string;
13
+ aqiFormat?: TextFormat.Value;
14
+ stationFormat?: TextFormat.Value;
15
+ qualityFormat?: TextFormat.Value;
16
+ };
17
+ }
18
+
19
+ export const Card: React.FunctionComponent<Props> = ({ data, format }) => {
20
+ const { color, quality } = assingAirQuality(data?.aqi)
21
+ return (
22
+ <div style={{
23
+ display: 'flex',
24
+ width: format.cardWidth || 300,
25
+ height: format.cardHeight || 200,
26
+ flexDirection: 'column',
27
+ padding: '1rem',
28
+ backgroundColor: color,
29
+ border: '2px solid #000',
30
+ gap: format.cardGap || 2,
31
+ }}>
32
+ <div style={{ textAlign: `${format.aqiFormat?.alignment?.horizontal || 'left'}` }}>
33
+ <FormattedText text={String(data?.aqi)} format={format.aqiFormat} />
34
+ </div>
35
+ <div style={{ textAlign: `${format.stationFormat?.alignment?.horizontal || 'left'}` }}>
36
+ <FormattedText text={String(`Estación: ${data?.city?.name}`)} format={format.stationFormat} />
37
+ </div>
38
+ <div style={{ textAlign: `${format.qualityFormat?.alignment?.horizontal || 'left'}` }}>
39
+ <FormattedText text={data?.aqi ? quality : ''} format={format.qualityFormat} />
40
+ </div>
41
+ </div>
42
+ )
43
+ }
@@ -0,0 +1,41 @@
1
+ import React from 'react'
2
+
3
+ interface Props {
4
+ paths: Array<string>
5
+ parameters: any
6
+ downloadsPath: string
7
+ }
8
+
9
+ export const FontLoader: React.FunctionComponent<Props> = ({ paths, parameters, downloadsPath }) => {
10
+ const _getValueByPath = (path: any, obj: any): any => {
11
+ let pathsArray = path.split('.')
12
+ let propertyName = pathsArray[0]
13
+ if (pathsArray.length === 1) return obj[propertyName]
14
+ else {
15
+ pathsArray.splice(0, 1)
16
+ return _getValueByPath(pathsArray.join('.'), obj[propertyName])
17
+ }
18
+ }
19
+
20
+ const fonts = paths.map((path: any) => {
21
+ try {
22
+ let fontParam = _getValueByPath(path, parameters)
23
+ let font = {
24
+ family: fontParam.filename.replace('.', '-'),
25
+ src: `${downloadsPath}/${fontParam.filename}`
26
+ }
27
+ return `
28
+ @font-face {
29
+ font-family: "${font.family}";
30
+ src: url("${font.src}");
31
+ }
32
+ `
33
+ } catch (err) {
34
+ return ''
35
+ }
36
+ })
37
+
38
+ return (
39
+ <style>{fonts}</style>
40
+ )
41
+ }
@@ -0,0 +1,81 @@
1
+ import React from 'react'
2
+
3
+ const alignments = {
4
+ center: 'center',
5
+ left: 'flex-start',
6
+ right: 'flex-end'
7
+ }
8
+
9
+ type Aligment = {
10
+ horizontal: keyof typeof alignments,
11
+ vertical: keyof typeof alignments
12
+ }
13
+
14
+ interface Props {
15
+ format?: any
16
+ lineHeight?: number
17
+ maxChar?: number
18
+ style?: number
19
+ text: string
20
+ unit?: string
21
+ paddingBottom?: string
22
+ paddingTop?: string
23
+ }
24
+
25
+ export const FormattedText: React.FunctionComponent<Props> = ({ text, format, maxChar, lineHeight, style, unit, paddingBottom, paddingTop }) => {
26
+ const trimText = (text: any, maxChar: any) => {
27
+ const isValid = maxChar && maxChar >= 3
28
+ if (isValid && (text && text.length > maxChar) || false) {
29
+ let returnText = text.substring(0, maxChar - 3)
30
+ returnText.substr(-1, 3)
31
+ return `${returnText.trim()}...`
32
+ }
33
+ return text
34
+ }
35
+
36
+ const checkNested = (obj: any, path: any): any => {
37
+ let arr = path.split('.')
38
+ if (arr.length > 0) {
39
+ if (obj.hasOwnProperty(arr[0])) {
40
+ if (arr.length > 1) return checkNested(obj[arr[0]], arr.splice(1).join('.'))
41
+ else return true
42
+ } else return false
43
+ }
44
+ }
45
+
46
+ const getHorizontalAlignment = (alignment: Aligment) => {
47
+ if (alignment) {
48
+ let hA = alignment.horizontal
49
+ return alignments.hasOwnProperty(hA) ? alignments[alignment.horizontal] : 'flex-start'
50
+ }
51
+ return 'flex-start'
52
+ }
53
+
54
+ const renderText = maxChar ? trimText(text, maxChar) : text
55
+ let containerStyle = Object.assign({
56
+ display: 'inline-flex',
57
+ justifyContent: getHorizontalAlignment(format.alignment)
58
+ }, style)
59
+
60
+ return (
61
+ <div style={containerStyle}>
62
+ <span style={{
63
+ color: format?.fontColor,
64
+ fontFamily: checkNested(format, 'font.filename') ? `"${format?.font?.filename?.replace('.', '-')}"` : '',
65
+ fontSize: `${format?.fontSize}${unit}`,
66
+ textAlign: checkNested(format, 'alignment.horizontal') ? format?.alignment?.horizontal : 'left',
67
+ lineHeight: lineHeight,
68
+ paddingBottom: paddingBottom,
69
+ paddingTop: paddingTop,
70
+ display: 'inline-flex'
71
+ }}>{renderText}</span>
72
+ </div>
73
+ )
74
+ }
75
+
76
+ FormattedText.defaultProps = {
77
+ format: {},
78
+ lineHeight: 1,
79
+ unit: 'vh',
80
+ maxChar: undefined
81
+ }
package/src/index.html ADDED
@@ -0,0 +1,11 @@
1
+ <!DOCTYPE html>
2
+ <html style="position: absolute; height:100%; width: 100%; overflow: hidden;">
3
+ <head>
4
+ <title></title>
5
+ <meta charset="utf-8">
6
+ </head>
7
+ <body style="margin:0; overflow: hidden;">
8
+ <div id="root" style="position: absolute; top: 0; right: 0; bottom: 0; left: 0; overflow: hidden;">
9
+ </div>
10
+ </body>
11
+ </html>
package/src/main.ts ADDED
@@ -0,0 +1,27 @@
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom';
3
+ import { App } from './App';
4
+
5
+ const { ipcRenderer } = require('electron')
6
+ let start: boolean = false
7
+
8
+ ipcRenderer.on('preload', (_event: any, data: VixonicData) => {
9
+ render(data)
10
+ })
11
+
12
+ ipcRenderer.on('start', (_event: any, data: VixonicData) => {
13
+ start = true
14
+ render(data)
15
+ })
16
+
17
+ ipcRenderer.on('update', (_event: any, data: VixonicData) => {
18
+ render(data)
19
+ })
20
+
21
+ ipcRenderer.on('finish', (_event: VixonicData) => {
22
+ console.log('finish')
23
+ })
24
+
25
+ function render(data: VixonicData) {
26
+ ReactDOM.render(React.createElement(App, { data, start }), document.getElementById('root'))
27
+ }
@@ -0,0 +1,9 @@
1
+ declare type VixonicData = {
2
+ downloadsPath: string
3
+ services: { [key: string]: { data?: any, updatedAt?: number } }
4
+ parameters: VixonicParameters
5
+ }
6
+
7
+ declare type VixonicParameters = Partial<{
8
+ padding: string, backgroundImage: {id?: string, filename?: string, extension?: string}, cardWidth: string, cardHeight: string, cardGap: string, aqiFormat: { fontSize?: number, fontColor?: string, alignment?: { horizontal?: 'left' | 'right' | 'center' }, font?: { filename: string, id: string, __isAsset: true } }, stationFormat: { fontSize?: number, fontColor?: string, alignment?: { horizontal?: 'left' | 'right' | 'center' }, font?: { filename: string, id: string, __isAsset: true } }, qualityFormat: { fontSize?: number, fontColor?: string, alignment?: { horizontal?: 'left' | 'right' | 'center' }, font?: { filename: string, id: string, __isAsset: true } }, updateData: 1 | 5 | 10 | 30 | 45 | 60, msj0: string, formatMjs: { fontSize?: number, fontColor?: string, alignment?: { horizontal?: 'left' | 'right' | 'center' }, font?: { filename: string, id: string, __isAsset: true } }
9
+ }>
Binary file
@@ -0,0 +1,34 @@
1
+ {
2
+ "parameters": {
3
+ "padding": "200px 100px 100px 600px",
4
+ "backgroundImage": {
5
+ "id": "earthquakes",
6
+ "filename": "Earthquakes.png",
7
+ "__isAsset": true
8
+ },
9
+ "cardWidth": "700px",
10
+ "cardHeight": "600px",
11
+ "cardGap": "5rem",
12
+ "aqiFormat": {
13
+ "fontSize": 18,
14
+ "fontColor": "#000000",
15
+ "alignment": {
16
+ "horizontal": "center"
17
+ }
18
+ },
19
+ "stationFormat": {
20
+ "fontSize": 8,
21
+ "fontColor": "#000000",
22
+ "alignment": {
23
+ "horizontal": "center"
24
+ }
25
+ },
26
+ "qualityFormat": {
27
+ "fontSize": 4,
28
+ "fontColor": "#000000",
29
+ "alignment": {
30
+ "horizontal": "center"
31
+ }
32
+ }
33
+ }
34
+ }