html2apk 0.1.0 → 0.3.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/README.md +318 -3
- package/examples/minimal/app.json +5 -0
- package/examples/minimal/dist/MeuApp-1.0.0-debug.apk +0 -0
- package/package.json +3 -2
- package/src/cli/index.js +22 -1
- package/src/cordova/config-xml.js +98 -6
- package/src/core/build-apk.js +150 -1
- package/src/core/config.js +74 -5
- package/src/core/defaults.js +15 -1
- package/src/desktop/main.js +30 -4
- package/src/desktop/preload.js +1 -0
- package/src/desktop/renderer/index.html +69 -2
- package/src/desktop/renderer/renderer.js +583 -2
- package/src/desktop/renderer/styles.css +131 -4
- package/src/templates/cordova-plugin-html2apk-bridge/plugin.xml +6 -0
- package/src/templates/cordova-plugin-html2apk-bridge/src/android/FloatingIconService.java +141 -0
- package/src/templates/cordova-plugin-html2apk-bridge/src/android/Html2ApkBridge.java +1674 -45
- package/src/templates/cordova-plugin-html2apk-bridge/src/android/NotificationReceiver.java +19 -5
- package/src/templates/cordova-plugin-html2apk-bridge/src/android/NotificationStore.java +13 -1
- package/src/templates/cordova-plugin-html2apk-bridge/www/html2apk-bridge.js +525 -1
- package/src/templates/html2apk-auto-theme.js +144 -0
- package/src/templates/html2apk-onesignal.js +155 -0
package/README.md
CHANGED
|
@@ -179,10 +179,14 @@ html2apk build --debug
|
|
|
179
179
|
html2apk build --release
|
|
180
180
|
html2apk build --mode fullscreen
|
|
181
181
|
html2apk build --mode standalone
|
|
182
|
+
html2apk build --mode floating
|
|
183
|
+
html2apk build --orientation vertical
|
|
184
|
+
html2apk build --min-sdk 24
|
|
182
185
|
html2apk build --entry-file index.html
|
|
183
186
|
html2apk build --web-root .
|
|
184
187
|
html2apk build --app-name MeuApp
|
|
185
188
|
html2apk build --package-id com.seuapp.meuapp
|
|
189
|
+
html2apk build --theme auto
|
|
186
190
|
html2apk build --android-platform android@15.0.0
|
|
187
191
|
```
|
|
188
192
|
|
|
@@ -203,9 +207,24 @@ Exemplo completo:
|
|
|
203
207
|
"packageId": "com.seuapp.meuapp",
|
|
204
208
|
"version": "1.0.0",
|
|
205
209
|
"mode": "fullscreen",
|
|
210
|
+
"orientation": "default",
|
|
211
|
+
"minSdkVersion": 24,
|
|
212
|
+
"themeColor": "#126fff",
|
|
213
|
+
"themeMode": "fixed",
|
|
214
|
+
"oneSignalAppId": "",
|
|
206
215
|
"icon": "",
|
|
207
216
|
"splash": "",
|
|
208
|
-
"
|
|
217
|
+
"deepLinks": {
|
|
218
|
+
"schemes": ["meuapp"],
|
|
219
|
+
"appLinks": [
|
|
220
|
+
{
|
|
221
|
+
"host": "meusite.com",
|
|
222
|
+
"paths": ["/produto/*", "/pedido/*"],
|
|
223
|
+
"autoVerify": false
|
|
224
|
+
}
|
|
225
|
+
]
|
|
226
|
+
},
|
|
227
|
+
"permissions": ["INTERNET", "CAMERA", "RECORD_AUDIO", "POST_NOTIFICATIONS", "VIBRATE"],
|
|
209
228
|
"plugins": ["cordova-plugin-camera"],
|
|
210
229
|
"release": false,
|
|
211
230
|
"androidPlatform": "android@15.0.0",
|
|
@@ -229,7 +248,13 @@ Campos principais:
|
|
|
229
248
|
| `appName` | Nome visivel do app. |
|
|
230
249
|
| `packageId` | Identificador Android. Precisa ter formato como `com.empresa.app`. |
|
|
231
250
|
| `version` | Versao do app. |
|
|
232
|
-
| `mode` | `fullscreen` para tela cheia
|
|
251
|
+
| `mode` | `fullscreen` para tela cheia, `standalone` para modo normal ou `floating` para icone flutuante. |
|
|
252
|
+
| `orientation` | `default`, `vertical`, `horizontal`, `portrait` ou `landscape`. |
|
|
253
|
+
| `minSdkVersion` | Versao minima do Android em API level. Padrao: `24`. |
|
|
254
|
+
| `themeColor` | Cor base do tema/splash Android, em hexadecimal. Tambem vira fallback do modo automatico. |
|
|
255
|
+
| `themeMode` | `fixed` usa a cor fixa. `auto` adapta as barras Android a cor visivel na tela do APK. Tambem aceita `theme: "auto"`. |
|
|
256
|
+
| `oneSignalAppId` | Opcional. App ID publico do OneSignal para push remoto. Nao coloque REST API Key no APK. |
|
|
257
|
+
| `deepLinks` | URLs que podem abrir o APK, como `meuapp://rota` ou `https://meusite.com/produto/1`. |
|
|
233
258
|
| `entryFile` | Arquivo HTML inicial. Normalmente `index.html`. |
|
|
234
259
|
| `webRoot` | Pasta onde estao os arquivos web. Normalmente `"."`. |
|
|
235
260
|
| `permissions` | Permissoes Android adicionadas ao app. |
|
|
@@ -245,6 +270,33 @@ Prioridade de configuracao:
|
|
|
245
270
|
2. `app.json` ou `config.json`.
|
|
246
271
|
3. Valores padrao do html2apk.
|
|
247
272
|
|
|
273
|
+
### Tema Automatico Do APK
|
|
274
|
+
|
|
275
|
+
Use `themeMode: "auto"` ou `theme: "auto"` para o APK adaptar as barras nativas do Android a cor que esta visivel na tela. O html2apk observa a tela do WebView e ajusta status bar/navigation bar em tempo real.
|
|
276
|
+
|
|
277
|
+
```json
|
|
278
|
+
{
|
|
279
|
+
"themeMode": "auto",
|
|
280
|
+
"themeColor": "#126fff"
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
`themeColor` continua importante: ela e usada no splash e como fallback quando o app ainda nao conseguiu detectar uma cor visivel.
|
|
285
|
+
|
|
286
|
+
### OneSignal Opcional
|
|
287
|
+
|
|
288
|
+
Se quiser receber notificacoes remotas pelo OneSignal, preencha `oneSignalAppId` no `app.json` ou no campo OneSignal App ID da interface grafica:
|
|
289
|
+
|
|
290
|
+
```json
|
|
291
|
+
{
|
|
292
|
+
"oneSignalAppId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Quando esse campo esta vazio, nada do OneSignal e instalado no APK. Quando ele esta preenchido, o html2apk instala `onesignal-cordova-plugin`, injeta a inicializacao no HTML e mantem as notificacoes locais existentes (`notificar`, `agendarNotificacao`, loops etc.).
|
|
297
|
+
|
|
298
|
+
Importante: o App ID do OneSignal pode ficar no APK. A REST API Key nao deve ir no app, porque e segredo de servidor. Envie pushes pelo painel do OneSignal ou por um backend seu.
|
|
299
|
+
|
|
248
300
|
## Exemplo Minimo
|
|
249
301
|
|
|
250
302
|
Este repositorio ja inclui um exemplo em:
|
|
@@ -301,7 +353,59 @@ Retorno:
|
|
|
301
353
|
|
|
302
354
|
## Bridge Nativa
|
|
303
355
|
|
|
304
|
-
A v0.1 instala um plugin Cordova local com uma API global simples para recursos Android.
|
|
356
|
+
A v0.1 instala um plugin Cordova local com uma API global simples para recursos Android. Todas as funcoes retornam `Promise`, exceto os ouvintes `aoEvento`/atalhos, que retornam uma funcao para cancelar a escuta.
|
|
357
|
+
|
|
358
|
+
Cada funcao em portugues tambem tem alias em ingles. A interface grafica mostra a sintaxe PT-BR quando o idioma esta em portugues e mostra a sintaxe em ingles quando o usuario troca o idioma para English.
|
|
359
|
+
|
|
360
|
+
Exemplos de aliases:
|
|
361
|
+
|
|
362
|
+
| PT-BR | English |
|
|
363
|
+
| --- | --- |
|
|
364
|
+
| `notificar()` | `notify()` |
|
|
365
|
+
| `agendarNotificacao()` | `scheduleNotification()` |
|
|
366
|
+
| `agendarNotificacoes()` | `scheduleNotifications()` |
|
|
367
|
+
| `agendarLoopNotificacoes()` | `scheduleNotificationLoop()` |
|
|
368
|
+
| `cancelarNotificacao()` | `cancelNotification()` |
|
|
369
|
+
| `solicitarPermissaoNotificacoes()` | `requestNotificationPermission()` |
|
|
370
|
+
| `solicitarPermissaoPush()` | `requestPushPermission()` |
|
|
371
|
+
| `aoClicarPush()` | `onPushClick()` |
|
|
372
|
+
| `identificarUsuarioPush()` | `loginPushUser()` |
|
|
373
|
+
| `adicionarTagPush()` | `addPushTag()` |
|
|
374
|
+
| `solicitarPermissaoMicrofone()` | `requestMicrophonePermission()` |
|
|
375
|
+
| `statusMicrofone()` | `microphoneStatus()` |
|
|
376
|
+
| `ouvirMic()` | `listenMic()` / `startMic()` |
|
|
377
|
+
| `pararMic()` | `stopMic()` |
|
|
378
|
+
| `lanterna()` | `flashlight()` |
|
|
379
|
+
| `alternarLanterna()` | `toggleFlashlight()` |
|
|
380
|
+
| `escolherArquivo()` | `pickFile()` |
|
|
381
|
+
| `escolherImagem()` | `pickImage()` |
|
|
382
|
+
| `salvarArquivo()` | `saveFile()` |
|
|
383
|
+
| `compartilhar()` | `share()` |
|
|
384
|
+
| `copiarTexto()` | `copyText()` |
|
|
385
|
+
| `lerTextoCopiado()` | `readText()` |
|
|
386
|
+
| `abrirNoApp()` | `openInApp()` |
|
|
387
|
+
| `abrirForaDoApp()` | `openOutsideApp()` |
|
|
388
|
+
| `abrirUrlExterno()` | `openExternalUrl()` |
|
|
389
|
+
| `manterTelaLigada()` | `keepScreenOn()` |
|
|
390
|
+
| `brilhoTela()` | `setScreenBrightness()` |
|
|
391
|
+
| `definirCorTema()` | `setThemeColor()` |
|
|
392
|
+
| `infoMemoria()` | `memoryInfo()` |
|
|
393
|
+
| `infoArmazenamento()` | `storageInfo()` |
|
|
394
|
+
| `infoDesempenho()` | `performanceInfo()` |
|
|
395
|
+
| `appsAbertos()` | `openAppsMemory()` |
|
|
396
|
+
| `aoEvento()` | `onEvent()` |
|
|
397
|
+
| `aoMinimizar()` | `onMinimize()` |
|
|
398
|
+
| `obterLinkInicial()` | `getInitialLink()` |
|
|
399
|
+
|
|
400
|
+
Os eventos tambem aceitam aliases em ingles em `onEvent()`: `app:ready`, `app:background`, `app:resumed`, `back:button`, `link:opened`, `network:changed`, `battery:changed`, `notification:received` e `notification:clicked`.
|
|
401
|
+
|
|
402
|
+
Como tratar retornos:
|
|
403
|
+
|
|
404
|
+
- Use `await` dentro de `try/catch` quando a acao depender de Android, permissao ou outro app instalado.
|
|
405
|
+
- Seletores de arquivo podem retornar `null`, array vazio ou objeto vazio quando o usuario cancela.
|
|
406
|
+
- APIs de permissao retornam objetos com `granted`; se vier `false`, desative o recurso ou mostre uma etapa de permissao.
|
|
407
|
+
- Eventos retornam uma funcao de cancelamento; guarde essa funcao e chame quando a tela/componente sair.
|
|
408
|
+
- Medidas de memoria, armazenamento e apps abertos sao diagnosticas. Android moderno pode limitar dados de outros apps por privacidade.
|
|
305
409
|
|
|
306
410
|
No seu JavaScript do app:
|
|
307
411
|
|
|
@@ -314,6 +418,10 @@ vibrar(250);
|
|
|
314
418
|
notificar({
|
|
315
419
|
titulo: "Pedido aprovado",
|
|
316
420
|
texto: "Toque para abrir os detalhes",
|
|
421
|
+
acoes: [
|
|
422
|
+
{ id: "abrir", titulo: "Abrir" },
|
|
423
|
+
{ id: "cancelar", titulo: "Cancelar" }
|
|
424
|
+
],
|
|
317
425
|
aoClicar: {
|
|
318
426
|
acao: "abrir-rota",
|
|
319
427
|
rota: "/pedido/123",
|
|
@@ -331,9 +439,216 @@ agendarNotificacao({
|
|
|
331
439
|
}
|
|
332
440
|
});
|
|
333
441
|
|
|
442
|
+
await agendarNotificacoes([
|
|
443
|
+
{ titulo: "Primeiro aviso", texto: "Daqui 1 minuto", quando: Date.now() + 60000 },
|
|
444
|
+
{ titulo: "Segundo aviso", texto: "Daqui 2 minutos", quando: Date.now() + 120000 }
|
|
445
|
+
]);
|
|
446
|
+
|
|
447
|
+
const loop = await agendarLoopNotificacoes({
|
|
448
|
+
aCada: "12h",
|
|
449
|
+
notificacoes: [
|
|
450
|
+
{
|
|
451
|
+
titulo: "Hidrate-se",
|
|
452
|
+
texto: "Beba agua agora",
|
|
453
|
+
aoClicar: { acao: "abrir-hidratacao" }
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
titulo: "Alongamento",
|
|
457
|
+
texto: "Pausa rapida para alongar",
|
|
458
|
+
aoClicar: { acao: "abrir-alongamento" }
|
|
459
|
+
}
|
|
460
|
+
]
|
|
461
|
+
});
|
|
462
|
+
|
|
334
463
|
fullscreen(true);
|
|
335
464
|
```
|
|
336
465
|
|
|
466
|
+
`agendarNotificacao()` agenda uma notificacao. Se voce passar um array para ela, ou usar `agendarNotificacoes()`, o html2apk agenda varias em sequencia. Cada item recebe `id` automatico se voce nao informar um.
|
|
467
|
+
|
|
468
|
+
`agendarLoopNotificacoes()` cria um loop recorrente que funciona com o app fechado. Use `aCada`, `intervalo`, `every` ou `interval` em milissegundos ou como texto (`"30min"`, `"12h"`, `"1d"`). A cada disparo, o Android mostra a proxima notificacao da lista. Para parar:
|
|
469
|
+
|
|
470
|
+
```js
|
|
471
|
+
await cancelarNotificacao(loop.id);
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
Cliques continuam chegando normalmente:
|
|
475
|
+
|
|
476
|
+
```js
|
|
477
|
+
aoClicarNotificacao((evento) => {
|
|
478
|
+
if ((evento.aoClicar || evento.onClick).acao === "abrir-hidratacao") {
|
|
479
|
+
abrirNoApp("#/hidratacao");
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
As notificacoes usam o icone do APK gerado como icone nativo. No Android, esse icone pode aparecer monocromatico na barra de notificacoes por regra do sistema.
|
|
485
|
+
|
|
486
|
+
OneSignal, quando `oneSignalAppId` esta preenchido:
|
|
487
|
+
|
|
488
|
+
```js
|
|
489
|
+
const permitido = await solicitarPermissaoPush();
|
|
490
|
+
|
|
491
|
+
identificarUsuarioPush("user-123");
|
|
492
|
+
adicionarTagPush("plano", "premium");
|
|
493
|
+
|
|
494
|
+
const pararPush = aoClicarPush((evento) => {
|
|
495
|
+
console.log("Push clicado", evento);
|
|
496
|
+
abrirNoApp("#/notificacoes");
|
|
497
|
+
});
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
Use OneSignal para pushes enviados remotamente pelo painel/API do OneSignal. Use `notificar()` e `agendarNotificacao()` para notificacoes locais criadas dentro do APK.
|
|
501
|
+
|
|
502
|
+
Arquivos, galeria e compartilhamento:
|
|
503
|
+
|
|
504
|
+
```js
|
|
505
|
+
const imagem = await escolherImagem();
|
|
506
|
+
const imagens = await escolherImagens({ multiplo: true });
|
|
507
|
+
const pdf = await escolherArquivo({ tipos: ["application/pdf"] });
|
|
508
|
+
const arquivos = await escolherArquivos({ multiplo: true });
|
|
509
|
+
|
|
510
|
+
await salvarArquivo({
|
|
511
|
+
nome: "relatorio.txt",
|
|
512
|
+
mimeType: "text/plain",
|
|
513
|
+
conteudo: "Conteudo salvo pelo app"
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
await compartilhar({ texto: "Veja isso", url: "https://exemplo.com" });
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
O retorno de arquivos tem este formato:
|
|
520
|
+
|
|
521
|
+
```json
|
|
522
|
+
{
|
|
523
|
+
"uri": "content://...",
|
|
524
|
+
"name": "foto.png",
|
|
525
|
+
"nome": "foto.png",
|
|
526
|
+
"size": 12345,
|
|
527
|
+
"tamanho": 12345,
|
|
528
|
+
"mimeType": "image/png"
|
|
529
|
+
}
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
Microfone:
|
|
533
|
+
|
|
534
|
+
```js
|
|
535
|
+
await solicitarPermissaoMicrofone();
|
|
536
|
+
|
|
537
|
+
await ouvirMic();
|
|
538
|
+
|
|
539
|
+
// ... depois, quando quiser parar
|
|
540
|
+
const audio = await pararMic();
|
|
541
|
+
const audioUrl = `data:${audio.mimeType};base64,${audio.base64}`;
|
|
542
|
+
|
|
543
|
+
const player = new Audio(audioUrl);
|
|
544
|
+
player.play();
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
`ouvirMic()` comeca a gravar e tambem pede permissao se ela ainda nao foi concedida. Para uma experiencia mais clara, prefira chamar `solicitarPermissaoMicrofone()` antes e explicar ao usuario por que o app precisa do microfone.
|
|
548
|
+
|
|
549
|
+
`pararMic()` encerra a gravacao e retorna:
|
|
550
|
+
|
|
551
|
+
```json
|
|
552
|
+
{
|
|
553
|
+
"base64": "AAAA...",
|
|
554
|
+
"mimeType": "audio/mp4",
|
|
555
|
+
"extension": "m4a",
|
|
556
|
+
"size": 12345,
|
|
557
|
+
"durationMs": 3200
|
|
558
|
+
}
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
Para tratar o retorno, use `mimeType` e `base64` juntos em um Data URL quando quiser tocar, baixar ou enviar o audio. Se `pararMic()` for chamado rapido demais, o Android pode nao conseguir finalizar o arquivo; aguarde alguns instantes apos `ouvirMic()`.
|
|
562
|
+
|
|
563
|
+
Lanterna, tela, clipboard e intents:
|
|
564
|
+
|
|
565
|
+
```js
|
|
566
|
+
await solicitarPermissaoCamera();
|
|
567
|
+
const lanternaStatus = await lanterna(true);
|
|
568
|
+
await alternarLanterna();
|
|
569
|
+
|
|
570
|
+
await manterTelaLigada(true);
|
|
571
|
+
await brilhoTela(0.8);
|
|
572
|
+
|
|
573
|
+
await copiarTexto("codigo123");
|
|
574
|
+
const texto = await lerTextoCopiado();
|
|
575
|
+
|
|
576
|
+
await abrirNoApp("/sobre.html");
|
|
577
|
+
await abrirNoApp("#/pedido/123", { substituir: true });
|
|
578
|
+
await abrirForaDoApp("https://exemplo.com");
|
|
579
|
+
await abrirWhatsapp("559999999999", "Oi");
|
|
580
|
+
await discar("11999999999");
|
|
581
|
+
await abrirMapa("Avenida Paulista, Sao Paulo");
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
Use `abrirNoApp()`/`openInApp()` quando a navegacao deve acontecer dentro do proprio APK/WebView. Use `abrirForaDoApp()`/`openOutsideApp()` ou `abrirUrlExterno()`/`openExternalUrl()` quando quer mandar o usuario para navegador, WhatsApp, Maps ou outro app Android.
|
|
585
|
+
|
|
586
|
+
Informacoes e desempenho:
|
|
587
|
+
|
|
588
|
+
```js
|
|
589
|
+
const aparelho = await infoDispositivo();
|
|
590
|
+
const rede = await infoRede();
|
|
591
|
+
const bateria = await infoBateria();
|
|
592
|
+
const memoria = await infoMemoria();
|
|
593
|
+
const armazenamento = await infoArmazenamento();
|
|
594
|
+
const desempenho = await infoDesempenho();
|
|
595
|
+
const abertos = await appsAbertos();
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
`infoDesempenho()` agrupa memoria, armazenamento, bateria, rede e `timestamp`. Valores de memoria e armazenamento retornam bytes.
|
|
599
|
+
|
|
600
|
+
`appsAbertos()` retorna os processos/apps que o Android permite o APK enxergar:
|
|
601
|
+
|
|
602
|
+
```json
|
|
603
|
+
{
|
|
604
|
+
"apps": [
|
|
605
|
+
{
|
|
606
|
+
"name": "MeuApp",
|
|
607
|
+
"packageName": "com.seuapp.meuapp",
|
|
608
|
+
"ramBytes": 12345678,
|
|
609
|
+
"ramMb": 11.77,
|
|
610
|
+
"importanceName": "foreground"
|
|
611
|
+
}
|
|
612
|
+
],
|
|
613
|
+
"porNome": {
|
|
614
|
+
"MeuApp": {
|
|
615
|
+
"ramBytes": 12345678,
|
|
616
|
+
"ramMb": 11.77
|
|
617
|
+
}
|
|
618
|
+
},
|
|
619
|
+
"limited": true
|
|
620
|
+
}
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
Por privacidade, Android moderno pode limitar essa lista ao proprio app e alguns processos visiveis ao APK. Entao essa funcao nao deve ser tratada como gerenciador completo de tarefas do sistema.
|
|
624
|
+
|
|
625
|
+
Eventos nativos:
|
|
626
|
+
|
|
627
|
+
```js
|
|
628
|
+
const parar = aoEvento("app:background", (evento) => {
|
|
629
|
+
console.log("App saiu da frente", evento.timestamp);
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
aoEvento("app:voltou", console.log);
|
|
633
|
+
aoEvento("botao:voltar", console.log);
|
|
634
|
+
aoEvento("link:aberto", (evento) => console.log(evento.url));
|
|
635
|
+
aoEvento("rede:mudou", console.log);
|
|
636
|
+
aoEvento("bateria:mudou", console.log);
|
|
637
|
+
aoEvento("notificacao:clicada", console.log);
|
|
638
|
+
|
|
639
|
+
parar();
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
Deep links:
|
|
643
|
+
|
|
644
|
+
```js
|
|
645
|
+
const linkInicial = await obterLinkInicial();
|
|
646
|
+
|
|
647
|
+
aoAbrirLink((evento) => {
|
|
648
|
+
console.log(evento.url, evento.path, evento.query);
|
|
649
|
+
});
|
|
650
|
+
```
|
|
651
|
+
|
|
337
652
|
Clique em notificacao:
|
|
338
653
|
|
|
339
654
|
```js
|
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
"packageId": "com.seuapp.meuapp",
|
|
5
5
|
"version": "1.0.0",
|
|
6
6
|
"mode": "fullscreen",
|
|
7
|
+
"orientation": "default",
|
|
8
|
+
"minSdkVersion": 24,
|
|
9
|
+
"themeColor": "#126fff",
|
|
10
|
+
"themeMode": "fixed",
|
|
11
|
+
"oneSignalAppId": "",
|
|
7
12
|
"icon": "",
|
|
8
13
|
"splash": "",
|
|
9
14
|
"permissions": [
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "html2apk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Node CLI and library to turn an HTML/CSS/JS folder into an Android APK through Cordova.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -71,6 +71,7 @@
|
|
|
71
71
|
"node": ">=18"
|
|
72
72
|
},
|
|
73
73
|
"dependencies": {
|
|
74
|
-
"cordova": "^13.0.0"
|
|
74
|
+
"cordova": "^13.0.0",
|
|
75
|
+
"onesignal-cordova-plugin": "^5.3.10"
|
|
75
76
|
}
|
|
76
77
|
}
|
package/src/cli/index.js
CHANGED
|
@@ -10,7 +10,7 @@ function printHelp() {
|
|
|
10
10
|
|
|
11
11
|
Usage:
|
|
12
12
|
html2apk init
|
|
13
|
-
html2apk build [--release] [--debug] [--mode fullscreen|standalone] [--android-platform android@15.0.0]
|
|
13
|
+
html2apk build [--release] [--debug] [--mode fullscreen|standalone|floating] [--theme fixed|auto] [--orientation vertical|horizontal] [--min-sdk 24] [--android-platform android@15.0.0]
|
|
14
14
|
html2apk doctor
|
|
15
15
|
|
|
16
16
|
The current working directory is always treated as the user app root.`);
|
|
@@ -28,6 +28,18 @@ function parseBuildArgs(args) {
|
|
|
28
28
|
} else if (arg === "--mode") {
|
|
29
29
|
options.mode = args[index + 1];
|
|
30
30
|
index += 1;
|
|
31
|
+
} else if (arg === "--orientation") {
|
|
32
|
+
options.orientation = args[index + 1];
|
|
33
|
+
index += 1;
|
|
34
|
+
} else if (arg === "--theme-color") {
|
|
35
|
+
options.themeColor = args[index + 1];
|
|
36
|
+
index += 1;
|
|
37
|
+
} else if (arg === "--theme" || arg === "--theme-mode") {
|
|
38
|
+
options.themeMode = args[index + 1];
|
|
39
|
+
index += 1;
|
|
40
|
+
} else if (arg === "--min-sdk" || arg === "--min-sdk-version" || arg === "--minSdkVersion") {
|
|
41
|
+
options.minSdkVersion = args[index + 1];
|
|
42
|
+
index += 1;
|
|
31
43
|
} else if (arg === "--entry-file") {
|
|
32
44
|
options.entryFile = args[index + 1];
|
|
33
45
|
index += 1;
|
|
@@ -64,6 +76,15 @@ function createPlaceholderConfig(projectName = "MeuApp") {
|
|
|
64
76
|
packageId: `com.seuapp.${packageSegment(appName)}`,
|
|
65
77
|
version: "1.0.0",
|
|
66
78
|
mode: "fullscreen",
|
|
79
|
+
orientation: "default",
|
|
80
|
+
minSdkVersion: 24,
|
|
81
|
+
themeColor: "#126fff",
|
|
82
|
+
themeMode: "fixed",
|
|
83
|
+
oneSignalAppId: "",
|
|
84
|
+
deepLinks: {
|
|
85
|
+
schemes: [],
|
|
86
|
+
appLinks: []
|
|
87
|
+
},
|
|
67
88
|
icon: "",
|
|
68
89
|
splash: "",
|
|
69
90
|
permissions: [
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require("fs/promises");
|
|
4
4
|
const path = require("path");
|
|
5
|
+
const {
|
|
6
|
+
DEFAULT_ANDROID_MIN_SDK_VERSION,
|
|
7
|
+
MAX_ANDROID_MIN_SDK_VERSION
|
|
8
|
+
} = require("../core/defaults");
|
|
5
9
|
|
|
6
10
|
function xmlEscape(value) {
|
|
7
11
|
return String(value)
|
|
@@ -17,12 +21,16 @@ function androidPermissionName(permission) {
|
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
function renderPermissions(permissions) {
|
|
20
|
-
|
|
24
|
+
const uniquePermissions = Array.from(new Set((permissions || [])
|
|
25
|
+
.map((permission) => String(permission).trim())
|
|
26
|
+
.filter(Boolean)
|
|
27
|
+
.map(androidPermissionName)));
|
|
28
|
+
if (!uniquePermissions.length) {
|
|
21
29
|
return "";
|
|
22
30
|
}
|
|
23
31
|
|
|
24
|
-
const items =
|
|
25
|
-
.map((permission) => ` <uses-permission android:name="${xmlEscape(
|
|
32
|
+
const items = uniquePermissions
|
|
33
|
+
.map((permission) => ` <uses-permission android:name="${xmlEscape(permission)}" />`)
|
|
26
34
|
.join("\n");
|
|
27
35
|
|
|
28
36
|
return ` <config-file target="AndroidManifest.xml" parent="/manifest">
|
|
@@ -45,25 +53,104 @@ function renderPreference(name, value) {
|
|
|
45
53
|
return ` <preference name="${xmlEscape(name)}" value="${xmlEscape(value)}" />`;
|
|
46
54
|
}
|
|
47
55
|
|
|
56
|
+
function normalizeMinSdkVersion(value) {
|
|
57
|
+
const parsed = Number.parseInt(value, 10);
|
|
58
|
+
return Number.isInteger(parsed)
|
|
59
|
+
&& parsed >= DEFAULT_ANDROID_MIN_SDK_VERSION
|
|
60
|
+
&& parsed <= MAX_ANDROID_MIN_SDK_VERSION
|
|
61
|
+
? parsed
|
|
62
|
+
: DEFAULT_ANDROID_MIN_SDK_VERSION;
|
|
63
|
+
}
|
|
64
|
+
|
|
48
65
|
function renderAndroidSplashPreferences(options) {
|
|
49
66
|
const splashIcon = options.androidSplashScreenAnimatedIcon || options.splash || options.icon;
|
|
50
67
|
if (!splashIcon) {
|
|
51
68
|
return "";
|
|
52
69
|
}
|
|
53
70
|
|
|
71
|
+
const themeColor = options.themeColor || options.splashBackgroundColor || options.backgroundColor || "#FFFFFF";
|
|
72
|
+
|
|
54
73
|
return [
|
|
55
74
|
renderPreference("AndroidWindowSplashScreenAnimatedIcon", splashIcon),
|
|
56
|
-
renderPreference("AndroidWindowSplashScreenBackground",
|
|
75
|
+
renderPreference("AndroidWindowSplashScreenBackground", themeColor),
|
|
57
76
|
renderPreference("AndroidWindowSplashScreenAnimationDuration", options.splashAnimationDuration || "200")
|
|
58
77
|
].filter(Boolean).join("\n");
|
|
59
78
|
}
|
|
60
79
|
|
|
80
|
+
function renderDeepLinkData(pathItem) {
|
|
81
|
+
if (!pathItem || pathItem === "*" || pathItem === "/*") {
|
|
82
|
+
return "";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (pathItem.includes("*")) {
|
|
86
|
+
return ` android:pathPattern="${xmlEscape(pathItem.replace(/\*/g, ".*"))}"`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return ` android:pathPrefix="${xmlEscape(pathItem)}"`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function renderDeepLinkIntentFilters(deepLinks = {}) {
|
|
93
|
+
const filters = [];
|
|
94
|
+
const schemes = Array.isArray(deepLinks.schemes) ? deepLinks.schemes : [];
|
|
95
|
+
const appLinks = Array.isArray(deepLinks.appLinks) ? deepLinks.appLinks : [];
|
|
96
|
+
|
|
97
|
+
for (const scheme of schemes) {
|
|
98
|
+
filters.push(` <config-file target="AndroidManifest.xml" parent="/manifest/application/activity">
|
|
99
|
+
<intent-filter>
|
|
100
|
+
<action android:name="android.intent.action.VIEW" />
|
|
101
|
+
<category android:name="android.intent.category.DEFAULT" />
|
|
102
|
+
<category android:name="android.intent.category.BROWSABLE" />
|
|
103
|
+
<data android:scheme="${xmlEscape(scheme)}" />
|
|
104
|
+
</intent-filter>
|
|
105
|
+
</config-file>`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
for (const link of appLinks) {
|
|
109
|
+
if (!link || !link.host) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const scheme = link.scheme || "https";
|
|
114
|
+
const paths = Array.isArray(link.paths) && link.paths.length ? link.paths : [""];
|
|
115
|
+
const dataItems = paths
|
|
116
|
+
.map((pathItem) => ` <data android:scheme="${xmlEscape(scheme)}" android:host="${xmlEscape(link.host)}"${renderDeepLinkData(pathItem)} />`)
|
|
117
|
+
.join("\n");
|
|
118
|
+
const verify = link.autoVerify ? " android:autoVerify=\"true\"" : "";
|
|
119
|
+
|
|
120
|
+
filters.push(` <config-file target="AndroidManifest.xml" parent="/manifest/application/activity">
|
|
121
|
+
<intent-filter${verify}>
|
|
122
|
+
<action android:name="android.intent.action.VIEW" />
|
|
123
|
+
<category android:name="android.intent.category.DEFAULT" />
|
|
124
|
+
<category android:name="android.intent.category.BROWSABLE" />
|
|
125
|
+
${dataItems}
|
|
126
|
+
</intent-filter>
|
|
127
|
+
</config-file>`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return filters.join("\n");
|
|
131
|
+
}
|
|
132
|
+
|
|
61
133
|
function renderConfigXml(options) {
|
|
62
134
|
const fullscreen = options.mode === "fullscreen" ? "true" : "false";
|
|
63
|
-
const
|
|
135
|
+
const permissionsList = options.mode === "floating"
|
|
136
|
+
? [...(options.permissions || []), "SYSTEM_ALERT_WINDOW"]
|
|
137
|
+
: (options.permissions || []);
|
|
138
|
+
const permissions = renderPermissions(permissionsList);
|
|
64
139
|
const icon = renderIcon(options.icon);
|
|
65
140
|
const splashPreferences = renderAndroidSplashPreferences(options);
|
|
66
|
-
const
|
|
141
|
+
const deepLinkIntentFilters = renderDeepLinkIntentFilters(options.deepLinks);
|
|
142
|
+
const platformItems = [permissions, deepLinkIntentFilters, icon, splashPreferences].filter(Boolean).join("\n");
|
|
143
|
+
const backgroundPreference = renderPreference("BackgroundColor", options.themeColor || options.backgroundColor);
|
|
144
|
+
const themeModePreference = renderPreference("Html2ApkThemeMode", options.themeMode || options.theme || "fixed");
|
|
145
|
+
const oneSignalPreference = renderPreference("Html2ApkOneSignalAppId", options.oneSignalAppId);
|
|
146
|
+
const modePreference = renderPreference("Html2ApkMode", options.mode || "standalone");
|
|
147
|
+
const minSdkPreference = renderPreference(
|
|
148
|
+
"android-minSdkVersion",
|
|
149
|
+
normalizeMinSdkVersion(options.minSdkVersion || options.androidMinSdkVersion)
|
|
150
|
+
);
|
|
151
|
+
const orientationPreference = ["portrait", "landscape"].includes(options.orientation)
|
|
152
|
+
? renderPreference("Orientation", options.orientation)
|
|
153
|
+
: "";
|
|
67
154
|
|
|
68
155
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
69
156
|
<widget id="${xmlEscape(options.packageId)}"
|
|
@@ -91,6 +178,11 @@ function renderConfigXml(options) {
|
|
|
91
178
|
<preference name="AndroidLaunchMode" value="singleTop" />
|
|
92
179
|
<preference name="DisallowOverscroll" value="true" />
|
|
93
180
|
<preference name="GradlePluginKotlinEnabled" value="true" />
|
|
181
|
+
${minSdkPreference}
|
|
182
|
+
${modePreference}
|
|
183
|
+
${themeModePreference}
|
|
184
|
+
${oneSignalPreference}
|
|
185
|
+
${orientationPreference ? `${orientationPreference}\n` : ""}${backgroundPreference ? `${backgroundPreference}\n` : ""}
|
|
94
186
|
|
|
95
187
|
<platform name="android">
|
|
96
188
|
${platformItems || " <!-- Extra Android options are generated here. -->"}
|