@vixoniccom/menu-daily 0.1.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/CHANGELOG.md +21 -0
- package/assets/framed.ai +3330 -4
- package/assets/modern.ai +421 -0
- package/build.zip +0 -0
- package/configuration.json +207 -0
- package/icon.png +0 -0
- package/package.json +31 -0
- package/src/dataLoader.ts +168 -0
- package/src/global.d.ts +59 -0
- package/src/index.html +33 -0
- package/src/logger.ts +11 -0
- package/src/main.ts +47 -0
- package/src/scenes/App.tsx +103 -0
- package/src/scenes/components/FontLoader.tsx +52 -0
- package/src/scenes/components/FormattedText.tsx +56 -0
- package/src/scenes/components/Grid/Grid.tsx +161 -0
- package/src/scenes/components/Grid/GridItem.tsx +79 -0
- package/src/scenes/components/Grid/animation.ts +105 -0
- package/src/scenes/components/Grid/index.ts +2 -0
- package/src/scenes/components/MealContainer/components/OptionItem.tsx +25 -0
- package/src/scenes/components/MealContainer/components/Title/index.tsx +82 -0
- package/src/scenes/components/MealContainer/components/Title/styles/Framed.tsx +52 -0
- package/src/scenes/components/MealContainer/components/Title/styles/Modern.tsx +47 -0
- package/src/scenes/components/MealContainer/components/Title/styles/index.tsx +13 -0
- package/src/scenes/components/MealContainer/components/index.ts +2 -0
- package/src/scenes/components/MealContainer/index.tsx +59 -0
- package/src/static/menu-daily-example.xlsx +0 -0
- package/src/test/downloads/1234.ttf +0 -0
- package/src/test/downloads/background.jpg +0 -0
- package/src/test/downloads/futura-font.ttf +0 -0
- package/src/test/parameters.json +37 -0
- package/tsconfig.json +36 -0
- package/tslint.json +5 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": [
|
|
3
|
+
{
|
|
4
|
+
"type": "group",
|
|
5
|
+
"id": "data",
|
|
6
|
+
"label": "Datos",
|
|
7
|
+
"items": [
|
|
8
|
+
{
|
|
9
|
+
"type": "text-input",
|
|
10
|
+
"id": "url",
|
|
11
|
+
"label": "Enlace",
|
|
12
|
+
"description": "Dirección donde se encuentran los datos en formato excel.",
|
|
13
|
+
"required": true
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"type": "select-input",
|
|
17
|
+
"id": "pollingInterval",
|
|
18
|
+
"label": "Actualización",
|
|
19
|
+
"description": "Frecuencia de consulta de los datos.",
|
|
20
|
+
"items": [
|
|
21
|
+
{
|
|
22
|
+
"label": "5 minutos",
|
|
23
|
+
"value": 300000
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"label": "15 minutos",
|
|
27
|
+
"value": 900000
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"label": "30 minutos",
|
|
31
|
+
"value": 1.8e+6
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"label": "1 hora",
|
|
35
|
+
"value": 3.6e+6
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"label": "6 horas",
|
|
39
|
+
"value": 2.16e+7
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"label": "12 horas",
|
|
43
|
+
"value": 4.32e+7
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"label": "Diaria",
|
|
47
|
+
"value": 8.64e+7
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
"defaultValue": 900000,
|
|
51
|
+
"required": true
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"type": "text-input",
|
|
55
|
+
"id": "mealType",
|
|
56
|
+
"label": "Tipo de comida",
|
|
57
|
+
"description": "El nombre de la pestaña en el excel. No requerido."
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"type": "text-input",
|
|
61
|
+
"id": "msj0",
|
|
62
|
+
"label": "Sin menú",
|
|
63
|
+
"description": "Mensaje cuando no hay menu."
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"type": "group",
|
|
69
|
+
"id": "design",
|
|
70
|
+
"label": "Apariencia",
|
|
71
|
+
"items": [
|
|
72
|
+
{
|
|
73
|
+
"type": "label",
|
|
74
|
+
"label": "Animación"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"type": "number-input",
|
|
78
|
+
"id": "animationDuration",
|
|
79
|
+
"label": "Duración",
|
|
80
|
+
"description": "En segundos. Por defecto 15s."
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"type": "select-input",
|
|
84
|
+
"id": "animationMode",
|
|
85
|
+
"label": "Modo",
|
|
86
|
+
"items": [
|
|
87
|
+
{
|
|
88
|
+
"label": "Fundido",
|
|
89
|
+
"value": "fade"
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"label": "Izquieda-derecha",
|
|
93
|
+
"value": "slideRight"
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"label": "Derecha-izquierda",
|
|
97
|
+
"value": "slideLeft"
|
|
98
|
+
}
|
|
99
|
+
],
|
|
100
|
+
"defaultValue": "fade"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"type": "label",
|
|
104
|
+
"label": "Contenedor General"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"type": "number-input",
|
|
108
|
+
"id": "containerGridColumns",
|
|
109
|
+
"label": "Columnas",
|
|
110
|
+
"range": "[1:99]"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"type": "number-input",
|
|
114
|
+
"id": "containerGridColumnsGap",
|
|
115
|
+
"label": "Separación",
|
|
116
|
+
"description": "En porcentaje",
|
|
117
|
+
"range": "[0:100]"
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"type": "number-input",
|
|
121
|
+
"id": "containerGridRows",
|
|
122
|
+
"label": "Filas",
|
|
123
|
+
"range": "[1:99]"
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"type": "number-input",
|
|
127
|
+
"id": "containerGridRowsGap",
|
|
128
|
+
"label": "Separación",
|
|
129
|
+
"description": "En porcentaje",
|
|
130
|
+
"range": "[0:100]"
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"type": "text-input",
|
|
134
|
+
"id": "containerGridMargins",
|
|
135
|
+
"label": "Margenes",
|
|
136
|
+
"description": "En formato CSS."
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"type": "select-asset-kna-input",
|
|
140
|
+
"id": "backgroundImage",
|
|
141
|
+
"label": "Imagen de fondo"
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"type": "label",
|
|
145
|
+
"label": "Contenedor de comida"
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
"type": "number-input",
|
|
149
|
+
"id": "itemGridRows",
|
|
150
|
+
"label": "Filas"
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"type": "text-input",
|
|
154
|
+
"id": "itemGridMargins",
|
|
155
|
+
"label": "Margenes",
|
|
156
|
+
"description": "En formato CSS."
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
"type": "number-input",
|
|
160
|
+
"id": "itemAnimationDuration",
|
|
161
|
+
"label": "Duración de animación",
|
|
162
|
+
"description": "En segundos. Por defecto calcula automaticamente."
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"type": "label",
|
|
166
|
+
"label": "Comida"
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
"type": "text-format",
|
|
170
|
+
"id": "itemTitleTextFormat",
|
|
171
|
+
"label": "Formato del tíltulo"
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"type": "color-picker",
|
|
175
|
+
"id": "itemTitleBackgroundColor",
|
|
176
|
+
"label": "Fondo",
|
|
177
|
+
"description": "Color de fondo del título"
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
"type": "text-format",
|
|
181
|
+
"id": "itemOptionsTextFormat",
|
|
182
|
+
"label": "Formato de las opciones"
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
"type": "select-input",
|
|
186
|
+
"id": "itemStyle",
|
|
187
|
+
"label": "Estilo",
|
|
188
|
+
"items": [
|
|
189
|
+
{
|
|
190
|
+
"label": "Estandard",
|
|
191
|
+
"value": "standard"
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
"label": "Moderno",
|
|
195
|
+
"value": "modern"
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
"label": "Enmarcado",
|
|
199
|
+
"value": "framed"
|
|
200
|
+
}
|
|
201
|
+
],
|
|
202
|
+
"defaultValue": "standard"
|
|
203
|
+
}
|
|
204
|
+
]
|
|
205
|
+
}
|
|
206
|
+
]
|
|
207
|
+
}
|
package/icon.png
ADDED
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vixoniccom/menu-daily",
|
|
3
|
+
"alias": "Menú diario",
|
|
4
|
+
"description": "App que muestra el menú del día.",
|
|
5
|
+
"color": "#3395FF",
|
|
6
|
+
"tags": [
|
|
7
|
+
"menu",
|
|
8
|
+
"app"
|
|
9
|
+
],
|
|
10
|
+
"author": {
|
|
11
|
+
"name": ""
|
|
12
|
+
},
|
|
13
|
+
"version": "0.1.0",
|
|
14
|
+
"scripts": {
|
|
15
|
+
"prepublish": "vixonic-module-packager --mode build",
|
|
16
|
+
"watch": "vixonic-module-packager --mode watch",
|
|
17
|
+
"run": "vixonic-module-packager --mode run"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/node": "^8.0.14",
|
|
21
|
+
"@types/react": "^15.0.38",
|
|
22
|
+
"@types/react-dom": "^15.5.1"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"animejs": "^2.0.2",
|
|
26
|
+
"localforage": "^1.5.0",
|
|
27
|
+
"react": "^15.6.1",
|
|
28
|
+
"react-dom": "^15.6.1",
|
|
29
|
+
"xlsx": "^0.10.8"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import xlsx from 'xlsx'
|
|
2
|
+
import localforage from 'localforage'
|
|
3
|
+
import { logger } from './logger'
|
|
4
|
+
|
|
5
|
+
const getStoreKey = (id: string) => (`urlrequest-menu:${id}`)
|
|
6
|
+
|
|
7
|
+
export class DataLoader {
|
|
8
|
+
onUpdate?: (data: MenuData) => void
|
|
9
|
+
onError?: (e: Error) => void
|
|
10
|
+
|
|
11
|
+
private _url: string | undefined
|
|
12
|
+
get url (): string | undefined { return this._url }
|
|
13
|
+
|
|
14
|
+
private _data: MenuData | undefined
|
|
15
|
+
get data (): MenuData | undefined { return this._data }
|
|
16
|
+
|
|
17
|
+
private _mealType?: string
|
|
18
|
+
private _pollingInterval: number = 60000
|
|
19
|
+
private _pollingTimer: number | undefined
|
|
20
|
+
|
|
21
|
+
private _req: XMLHttpRequest | undefined
|
|
22
|
+
|
|
23
|
+
private _started: boolean = false
|
|
24
|
+
|
|
25
|
+
constructor (url?: string, interval?: number, mealType?: string) {
|
|
26
|
+
this.setup(url, interval, mealType)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
setup (url?: string, interval: number = 300000, mealType?: string) {
|
|
30
|
+
if (!url) {
|
|
31
|
+
this.stop()
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
this._pollingInterval = interval >= 60000 ? interval : 60000
|
|
36
|
+
this._mealType = mealType
|
|
37
|
+
if (this._url !== url) {
|
|
38
|
+
this.stop()
|
|
39
|
+
this._url = url
|
|
40
|
+
// Get cache data
|
|
41
|
+
if (!this._data) {
|
|
42
|
+
localforage.getItem<CacheData>(getStoreKey(this._url)).then(
|
|
43
|
+
(data) => {
|
|
44
|
+
if (!data || !data.updateTime || !data.menuData) {
|
|
45
|
+
this.startPolling()
|
|
46
|
+
} else {
|
|
47
|
+
logger.log('Got cached data!', data.updateTime)
|
|
48
|
+
this.onUpdate && this.onUpdate(data.menuData)
|
|
49
|
+
let timeDiff = Date.now() - data.updateTime
|
|
50
|
+
if (timeDiff >= this._pollingInterval) {
|
|
51
|
+
this.startPolling()
|
|
52
|
+
} else {
|
|
53
|
+
this.startPollingTimer(this._pollingInterval - timeDiff)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
(_e) => {
|
|
58
|
+
this.startPolling()
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
} else {
|
|
62
|
+
this.startPolling()
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private startPolling () {
|
|
68
|
+
if (this._started) return
|
|
69
|
+
if (!this._url) return
|
|
70
|
+
this._started = true
|
|
71
|
+
this.load()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private stop (): void {
|
|
75
|
+
window.clearTimeout(this._pollingTimer)
|
|
76
|
+
this._started = false
|
|
77
|
+
if (this._req) this._req.abort()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private load (): void {
|
|
81
|
+
if (!this._url) return
|
|
82
|
+
|
|
83
|
+
this._req = new XMLHttpRequest()
|
|
84
|
+
this._req.open('GET', this._url, true)
|
|
85
|
+
this._req.responseType = 'arraybuffer'
|
|
86
|
+
|
|
87
|
+
this._req.onload = this.onLoad.bind(this)
|
|
88
|
+
|
|
89
|
+
this._req.onerror = (e) => {
|
|
90
|
+
if (e) {
|
|
91
|
+
this.onError && this.onError(e as any)
|
|
92
|
+
this.startPollingTimer(30000)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
this._req.send()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private onLoad (_ev: Event) {
|
|
100
|
+
if (!(this._req && this._url)) return
|
|
101
|
+
|
|
102
|
+
logger.log('Data loaded', Date.now())
|
|
103
|
+
let arraybuffer = this._req.response
|
|
104
|
+
|
|
105
|
+
/* convert data to binary string */
|
|
106
|
+
let data = new Uint8Array(arraybuffer)
|
|
107
|
+
let arr = new Array()
|
|
108
|
+
for (let i = 0; i !== data.length; ++i) arr[i] = String.fromCharCode(data[i])
|
|
109
|
+
let bstr = arr.join('')
|
|
110
|
+
|
|
111
|
+
/* Call XLSX */
|
|
112
|
+
let workbook = xlsx.read(bstr, { type: 'binary' })
|
|
113
|
+
this._pollingTimer = window.setTimeout(() => { this.load() }, this._pollingInterval)
|
|
114
|
+
|
|
115
|
+
let menuData = this.parseData(workbook)
|
|
116
|
+
if (menuData) {
|
|
117
|
+
this._data = menuData
|
|
118
|
+
// Save cache
|
|
119
|
+
localforage.setItem<CacheData>(getStoreKey(this._url), {
|
|
120
|
+
updateTime: Date.now(),
|
|
121
|
+
menuData: this._data
|
|
122
|
+
})
|
|
123
|
+
// Dispatch update
|
|
124
|
+
this.onUpdate && this.onUpdate(this._data)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private startPollingTimer (interval?: number) {
|
|
129
|
+
if (this._pollingTimer) window.clearTimeout(this._pollingInterval)
|
|
130
|
+
this._pollingTimer = window.setTimeout(() => { this.load() }, interval || this._pollingInterval)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private parseData (workbook: xlsx.WorkBook): MenuData | undefined {
|
|
134
|
+
try {
|
|
135
|
+
if (this._mealType) workbook.SheetNames.find((name) => name.toUpperCase() === this._mealType!.toUpperCase())
|
|
136
|
+
let menuData: MenuData = workbook.SheetNames.reduce((menu: MenuData, name) => {
|
|
137
|
+
let newMenu = {...menu}
|
|
138
|
+
let dailyMenu = this.parseSheet(workbook.Sheets[name])
|
|
139
|
+
if (dailyMenu) newMenu[name.toUpperCase()] = dailyMenu
|
|
140
|
+
return newMenu
|
|
141
|
+
}, {})
|
|
142
|
+
logger.log(menuData)
|
|
143
|
+
return menuData
|
|
144
|
+
} catch (e) {
|
|
145
|
+
this.onError && this.onError(e)
|
|
146
|
+
return undefined
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private parseSheet (sheet: xlsx.WorkSheet): MenuByDate | undefined {
|
|
151
|
+
if (!sheet) return undefined
|
|
152
|
+
let meals: MenuByDate | {} = xlsx.utils.sheet_to_json(sheet, {raw: true}).reduce((menu: MenuByDate, row: any) => {
|
|
153
|
+
let newMenu = {...menu}
|
|
154
|
+
let keys = Object.keys(row)
|
|
155
|
+
let date: {d: number, m: number, y: number} = xlsx.SSF.parse_date_code(row[keys[0]] as any)
|
|
156
|
+
if (date.d && date.m && date.y) {
|
|
157
|
+
newMenu[new Date(date.y,date.m - 1,date.d).getTime()] = keys.slice(1).map((key) => {
|
|
158
|
+
return {
|
|
159
|
+
type: key,
|
|
160
|
+
options: row[key] && row[key].split(';') || []
|
|
161
|
+
} as Meal
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
return newMenu
|
|
165
|
+
}, {})
|
|
166
|
+
return meals
|
|
167
|
+
}
|
|
168
|
+
}
|
package/src/global.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
declare type VixonicData = {
|
|
2
|
+
downloadsPath: string
|
|
3
|
+
parameters: VixonicParameters
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
declare type VixonicFile = Partial<{
|
|
7
|
+
filename: string
|
|
8
|
+
}>
|
|
9
|
+
|
|
10
|
+
declare type VixonicTextFormat = Partial<{
|
|
11
|
+
fontSize: number,
|
|
12
|
+
fontColor: string,
|
|
13
|
+
alignment: {
|
|
14
|
+
horizontal: 'left' | 'right' | 'center'
|
|
15
|
+
}
|
|
16
|
+
font: VixonicFile
|
|
17
|
+
}>
|
|
18
|
+
|
|
19
|
+
declare type VixonicParameters = Partial<{
|
|
20
|
+
url: string
|
|
21
|
+
pollingInterval: number
|
|
22
|
+
mealType: string
|
|
23
|
+
animationDuration: number
|
|
24
|
+
animationMode: 'fade' | 'slideLeft' | 'slideRight'
|
|
25
|
+
containerGridColumns: number
|
|
26
|
+
containerGridColumnsGap: number
|
|
27
|
+
containerGridRows: number
|
|
28
|
+
containerGridRowsGap: number
|
|
29
|
+
containerGridMargins: number
|
|
30
|
+
backgroundImage: VixonicFile
|
|
31
|
+
msj0: string
|
|
32
|
+
itemStyle: 'standard' | 'modern' | 'framed'
|
|
33
|
+
itemTitleTextFormat: VixonicTextFormat
|
|
34
|
+
itemTitleBackgroundColor: string
|
|
35
|
+
itemAnimationDuration: number
|
|
36
|
+
itemGridMargins: string
|
|
37
|
+
itemGridRows: number
|
|
38
|
+
itemOptionsTextFormat: VixonicTextFormat
|
|
39
|
+
}>
|
|
40
|
+
|
|
41
|
+
declare type CacheData = {
|
|
42
|
+
updateTime: number
|
|
43
|
+
menuData: MenuData
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
declare type MenuData = {
|
|
47
|
+
[type: string]: MenuByDate
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
declare type MenuByDate = {
|
|
51
|
+
[date: number]: Menu
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
declare type Menu = Meal[]
|
|
55
|
+
|
|
56
|
+
declare type Meal = {
|
|
57
|
+
type: string
|
|
58
|
+
options: string[]
|
|
59
|
+
}
|
package/src/index.html
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title></title>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<style>
|
|
7
|
+
@font-face {
|
|
8
|
+
font-family: 'FrutigerLTStdBoldCn';
|
|
9
|
+
src: url('./static/fonts/FrutigerLTStdBoldCn.ttf');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@font-face {
|
|
13
|
+
font-family: 'FrutigerLTStdCn';
|
|
14
|
+
src: url('./static/fonts/FrutigerLTStdCn.otf');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@font-face {
|
|
18
|
+
font-family: 'FrutigerLTStdBlackCn';
|
|
19
|
+
src: url('./static/fonts/FrutigerLTStdBlackCn.ttf');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@font-face {
|
|
23
|
+
font-family: 'FrutigerLTStdLightCn';
|
|
24
|
+
src: url('./static/fonts/FrutigerLTStdLightCn.ttf');
|
|
25
|
+
}
|
|
26
|
+
</style>
|
|
27
|
+
</head>
|
|
28
|
+
<body>
|
|
29
|
+
<div id="root" style="position: absolute; top: 0; left: 0;
|
|
30
|
+
bottom: 0; right: 0; display: flex; justify-content: center;
|
|
31
|
+
align-items: center; overflow: hidden;"></div>
|
|
32
|
+
</body>
|
|
33
|
+
</html>
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const LOGGER_PROPERTY = 'vxAppLogger'
|
|
2
|
+
class Logger {
|
|
3
|
+
log (message: any, ...optionalParams: any[]) {
|
|
4
|
+
if ((window as any)[LOGGER_PROPERTY] === true) {
|
|
5
|
+
if (optionalParams && optionalParams.length > 0) console.log(message, optionalParams)
|
|
6
|
+
else console.log(message)
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export let logger = new Logger()
|
package/src/main.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import ReactDOM from 'react-dom'
|
|
4
|
+
|
|
5
|
+
import App, { AppProps } from './scenes/App'
|
|
6
|
+
import { logger } from './logger'
|
|
7
|
+
|
|
8
|
+
let { ipcRenderer } = require('electron')
|
|
9
|
+
|
|
10
|
+
let start = false
|
|
11
|
+
let vixonicData: VixonicData
|
|
12
|
+
|
|
13
|
+
type VixonicData = {
|
|
14
|
+
downloadsPath: string,
|
|
15
|
+
parameters: VixonicParameters
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
ipcRenderer.on('preload', (_event: any, _data: VixonicData) => {
|
|
19
|
+
// Preload command
|
|
20
|
+
logger.log('Preload', _data)
|
|
21
|
+
vixonicData = _data
|
|
22
|
+
doRender()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
ipcRenderer.on('start', (_event: any, _data: VixonicData) => {
|
|
26
|
+
// Start command
|
|
27
|
+
logger.log('Start', _data)
|
|
28
|
+
vixonicData = _data
|
|
29
|
+
start = true
|
|
30
|
+
doRender()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
ipcRenderer.on('update', (_event: any, _data: VixonicData) => {
|
|
34
|
+
// Update command
|
|
35
|
+
logger.log('Update', _data)
|
|
36
|
+
vixonicData = _data
|
|
37
|
+
doRender()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
ipcRenderer.on('finish', (_event: any) => {
|
|
41
|
+
// Finish command.
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
function doRender () {
|
|
45
|
+
if (!vixonicData) return
|
|
46
|
+
ReactDOM.render(React.createElement<AppProps>(App as any, { start, vixonicData }), document.getElementById('root'))
|
|
47
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
|
|
3
|
+
import { DataLoader } from '../dataLoader'
|
|
4
|
+
import Grid from './components/Grid'
|
|
5
|
+
import MealContainer from './components/MealContainer'
|
|
6
|
+
import FontLoader, { fontParser } from './components/FontLoader'
|
|
7
|
+
import FormattedText from './components/FormattedText'
|
|
8
|
+
|
|
9
|
+
export type AppProps = {
|
|
10
|
+
start: boolean,
|
|
11
|
+
vixonicData: VixonicData
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type AppState = {
|
|
15
|
+
loading: boolean
|
|
16
|
+
updateTime?: number
|
|
17
|
+
data?: Menu
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
class App extends React.Component<AppProps, AppState> {
|
|
21
|
+
private readonly dataLoader: DataLoader
|
|
22
|
+
|
|
23
|
+
constructor (props: AppProps) {
|
|
24
|
+
super(props)
|
|
25
|
+
let { parameters } = props.vixonicData
|
|
26
|
+
this.state = {
|
|
27
|
+
loading: true
|
|
28
|
+
}
|
|
29
|
+
this.dataLoader = new DataLoader(parameters.url, parameters.pollingInterval)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
componentDidMount () {
|
|
33
|
+
this.dataLoader.onUpdate = (data) => {
|
|
34
|
+
const { mealType } = this.props.vixonicData.parameters
|
|
35
|
+
const today = new Date()
|
|
36
|
+
today.setHours(0,0,0,0)
|
|
37
|
+
const menu = mealType && data[mealType.toUpperCase()] || data[Object.keys(data)[0]]
|
|
38
|
+
this.setState({loading: false, data: menu[today.getTime()]})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
this.dataLoader.onError = (e) => {
|
|
42
|
+
console.error(e)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
componentWillReceiveProps (nextProps: AppProps) {
|
|
47
|
+
if (this.props.vixonicData.parameters.url !== nextProps.vixonicData.parameters.url) {
|
|
48
|
+
let { parameters } = nextProps.vixonicData
|
|
49
|
+
parameters.url && this.dataLoader.setup(parameters.url, parameters.pollingInterval)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
render () {
|
|
54
|
+
const { parameters, downloadsPath } = this.props.vixonicData
|
|
55
|
+
let cycle = parameters.animationDuration && parameters.animationDuration * 1000 || 15000
|
|
56
|
+
return <div style={{
|
|
57
|
+
position: 'absolute',
|
|
58
|
+
top: 0, right: 0, bottom: 0, left: 0,
|
|
59
|
+
backgroundImage: parameters.backgroundImage &&
|
|
60
|
+
`url("${downloadsPath + '/' + parameters.backgroundImage.filename}")` || undefined,
|
|
61
|
+
backgroundSize: '100% 100%',
|
|
62
|
+
padding: parameters.containerGridMargins
|
|
63
|
+
}}>
|
|
64
|
+
{
|
|
65
|
+
this.state.loading || !this.state.data || !this.state.data[0] ?
|
|
66
|
+
<div style={{width: '100%', height: '100%', display: 'flex', fontSize: `${100 / (parameters.itemGridRows || 1)}vmin`, justifyContent: 'center', alignItems: 'center'}}>
|
|
67
|
+
<FormattedText
|
|
68
|
+
text={this.state.loading ? 'Cargando...' : parameters.msj0 || 'No hay menu.'}
|
|
69
|
+
downloadsPath={downloadsPath}
|
|
70
|
+
defaults={{
|
|
71
|
+
alignment: 'center',
|
|
72
|
+
fontColor: 'black',
|
|
73
|
+
fontSize: 10
|
|
74
|
+
}}
|
|
75
|
+
style={{width: '100%'}}
|
|
76
|
+
format={parameters.itemOptionsTextFormat || {}}
|
|
77
|
+
unit='%'/>
|
|
78
|
+
</div> :
|
|
79
|
+
<Grid
|
|
80
|
+
id='container'
|
|
81
|
+
animate={this.props.start}
|
|
82
|
+
style={{width: '100%', height: '100%'}}
|
|
83
|
+
items={this.state.data.map((meal) => (<MealContainer data={meal} interval={cycle} vixonicData={this.props.vixonicData} />))}
|
|
84
|
+
animation={{
|
|
85
|
+
mode: parameters.animationMode || 'fade',
|
|
86
|
+
duration: cycle
|
|
87
|
+
}} layout={{
|
|
88
|
+
alignment: {
|
|
89
|
+
h: 'start',
|
|
90
|
+
v: 'center'
|
|
91
|
+
},
|
|
92
|
+
columns: parameters.containerGridColumns || 1,
|
|
93
|
+
columnsGap: parameters.containerGridColumnsGap || 0,
|
|
94
|
+
rows: parameters.containerGridRows || 1,
|
|
95
|
+
rowsGap: parameters.containerGridRowsGap || 0
|
|
96
|
+
}}/>
|
|
97
|
+
}
|
|
98
|
+
<FontLoader downloadPath={downloadsPath} fonts={fontParser(parameters)}/>
|
|
99
|
+
</div>
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export default App
|