html2apk 0.2.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 +309 -3
- package/examples/minimal/app.json +2 -0
- package/examples/minimal/dist/MeuApp-1.0.0-debug.apk +0 -0
- package/package.json +3 -2
- package/src/cli/index.js +10 -1
- package/src/cordova/config-xml.js +59 -1
- package/src/core/build-apk.js +150 -1
- package/src/core/config.js +47 -1
- package/src/core/defaults.js +6 -0
- package/src/desktop/main.js +20 -1
- package/src/desktop/preload.js +1 -0
- package/src/desktop/renderer/index.html +17 -0
- package/src/desktop/renderer/renderer.js +359 -19
- package/src/desktop/renderer/styles.css +38 -0
- package/src/templates/cordova-plugin-html2apk-bridge/plugin.xml +4 -0
- package/src/templates/cordova-plugin-html2apk-bridge/src/android/Html2ApkBridge.java +1507 -55
- 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 +485 -1
- package/src/templates/html2apk-auto-theme.js +144 -0
- package/src/templates/html2apk-onesignal.js +155 -0
package/README.md
CHANGED
|
@@ -186,6 +186,7 @@ html2apk build --entry-file index.html
|
|
|
186
186
|
html2apk build --web-root .
|
|
187
187
|
html2apk build --app-name MeuApp
|
|
188
188
|
html2apk build --package-id com.seuapp.meuapp
|
|
189
|
+
html2apk build --theme auto
|
|
189
190
|
html2apk build --android-platform android@15.0.0
|
|
190
191
|
```
|
|
191
192
|
|
|
@@ -209,9 +210,21 @@ Exemplo completo:
|
|
|
209
210
|
"orientation": "default",
|
|
210
211
|
"minSdkVersion": 24,
|
|
211
212
|
"themeColor": "#126fff",
|
|
213
|
+
"themeMode": "fixed",
|
|
214
|
+
"oneSignalAppId": "",
|
|
212
215
|
"icon": "",
|
|
213
216
|
"splash": "",
|
|
214
|
-
"
|
|
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"],
|
|
215
228
|
"plugins": ["cordova-plugin-camera"],
|
|
216
229
|
"release": false,
|
|
217
230
|
"androidPlatform": "android@15.0.0",
|
|
@@ -238,7 +251,10 @@ Campos principais:
|
|
|
238
251
|
| `mode` | `fullscreen` para tela cheia, `standalone` para modo normal ou `floating` para icone flutuante. |
|
|
239
252
|
| `orientation` | `default`, `vertical`, `horizontal`, `portrait` ou `landscape`. |
|
|
240
253
|
| `minSdkVersion` | Versao minima do Android em API level. Padrao: `24`. |
|
|
241
|
-
| `themeColor` | Cor base do tema/splash Android, em hexadecimal. |
|
|
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`. |
|
|
242
258
|
| `entryFile` | Arquivo HTML inicial. Normalmente `index.html`. |
|
|
243
259
|
| `webRoot` | Pasta onde estao os arquivos web. Normalmente `"."`. |
|
|
244
260
|
| `permissions` | Permissoes Android adicionadas ao app. |
|
|
@@ -254,6 +270,33 @@ Prioridade de configuracao:
|
|
|
254
270
|
2. `app.json` ou `config.json`.
|
|
255
271
|
3. Valores padrao do html2apk.
|
|
256
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
|
+
|
|
257
300
|
## Exemplo Minimo
|
|
258
301
|
|
|
259
302
|
Este repositorio ja inclui um exemplo em:
|
|
@@ -310,7 +353,59 @@ Retorno:
|
|
|
310
353
|
|
|
311
354
|
## Bridge Nativa
|
|
312
355
|
|
|
313
|
-
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.
|
|
314
409
|
|
|
315
410
|
No seu JavaScript do app:
|
|
316
411
|
|
|
@@ -323,6 +418,10 @@ vibrar(250);
|
|
|
323
418
|
notificar({
|
|
324
419
|
titulo: "Pedido aprovado",
|
|
325
420
|
texto: "Toque para abrir os detalhes",
|
|
421
|
+
acoes: [
|
|
422
|
+
{ id: "abrir", titulo: "Abrir" },
|
|
423
|
+
{ id: "cancelar", titulo: "Cancelar" }
|
|
424
|
+
],
|
|
326
425
|
aoClicar: {
|
|
327
426
|
acao: "abrir-rota",
|
|
328
427
|
rota: "/pedido/123",
|
|
@@ -340,9 +439,216 @@ agendarNotificacao({
|
|
|
340
439
|
}
|
|
341
440
|
});
|
|
342
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
|
+
|
|
343
463
|
fullscreen(true);
|
|
344
464
|
```
|
|
345
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
|
+
|
|
346
652
|
Clique em notificacao:
|
|
347
653
|
|
|
348
654
|
```js
|
|
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|floating] [--orientation vertical|horizontal] [--min-sdk 24] [--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.`);
|
|
@@ -34,6 +34,9 @@ function parseBuildArgs(args) {
|
|
|
34
34
|
} else if (arg === "--theme-color") {
|
|
35
35
|
options.themeColor = args[index + 1];
|
|
36
36
|
index += 1;
|
|
37
|
+
} else if (arg === "--theme" || arg === "--theme-mode") {
|
|
38
|
+
options.themeMode = args[index + 1];
|
|
39
|
+
index += 1;
|
|
37
40
|
} else if (arg === "--min-sdk" || arg === "--min-sdk-version" || arg === "--minSdkVersion") {
|
|
38
41
|
options.minSdkVersion = args[index + 1];
|
|
39
42
|
index += 1;
|
|
@@ -76,6 +79,12 @@ function createPlaceholderConfig(projectName = "MeuApp") {
|
|
|
76
79
|
orientation: "default",
|
|
77
80
|
minSdkVersion: 24,
|
|
78
81
|
themeColor: "#126fff",
|
|
82
|
+
themeMode: "fixed",
|
|
83
|
+
oneSignalAppId: "",
|
|
84
|
+
deepLinks: {
|
|
85
|
+
schemes: [],
|
|
86
|
+
appLinks: []
|
|
87
|
+
},
|
|
79
88
|
icon: "",
|
|
80
89
|
splash: "",
|
|
81
90
|
permissions: [
|
|
@@ -77,6 +77,59 @@ function renderAndroidSplashPreferences(options) {
|
|
|
77
77
|
].filter(Boolean).join("\n");
|
|
78
78
|
}
|
|
79
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
|
+
|
|
80
133
|
function renderConfigXml(options) {
|
|
81
134
|
const fullscreen = options.mode === "fullscreen" ? "true" : "false";
|
|
82
135
|
const permissionsList = options.mode === "floating"
|
|
@@ -85,8 +138,11 @@ function renderConfigXml(options) {
|
|
|
85
138
|
const permissions = renderPermissions(permissionsList);
|
|
86
139
|
const icon = renderIcon(options.icon);
|
|
87
140
|
const splashPreferences = renderAndroidSplashPreferences(options);
|
|
88
|
-
const
|
|
141
|
+
const deepLinkIntentFilters = renderDeepLinkIntentFilters(options.deepLinks);
|
|
142
|
+
const platformItems = [permissions, deepLinkIntentFilters, icon, splashPreferences].filter(Boolean).join("\n");
|
|
89
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);
|
|
90
146
|
const modePreference = renderPreference("Html2ApkMode", options.mode || "standalone");
|
|
91
147
|
const minSdkPreference = renderPreference(
|
|
92
148
|
"android-minSdkVersion",
|
|
@@ -124,6 +180,8 @@ function renderConfigXml(options) {
|
|
|
124
180
|
<preference name="GradlePluginKotlinEnabled" value="true" />
|
|
125
181
|
${minSdkPreference}
|
|
126
182
|
${modePreference}
|
|
183
|
+
${themeModePreference}
|
|
184
|
+
${oneSignalPreference}
|
|
127
185
|
${orientationPreference ? `${orientationPreference}\n` : ""}${backgroundPreference ? `${backgroundPreference}\n` : ""}
|
|
128
186
|
|
|
129
187
|
<platform name="android">
|
package/src/core/build-apk.js
CHANGED
|
@@ -8,11 +8,15 @@ const { validateEntryFile, validateRequiredOptions } = require("./validation");
|
|
|
8
8
|
const { createCordovaProject, addAndroidPlatform, buildAndroid, addCordovaPlugin } = require("../cordova/project");
|
|
9
9
|
const { writeConfigXml } = require("../cordova/config-xml");
|
|
10
10
|
const { findApk } = require("../cordova/apk-finder");
|
|
11
|
-
const { copyWebAssets, ensureDir, removePath, copyFile } = require("../utils/fs-extra");
|
|
11
|
+
const { copyWebAssets, ensureDir, removePath, copyFile, copyDirectory } = require("../utils/fs-extra");
|
|
12
12
|
const { createCommandRunner } = require("../utils/command-runner");
|
|
13
13
|
const { installBridgePlugin } = require("../bridge/install-bridge");
|
|
14
14
|
const { getRuntimeEnvironment } = require("../runtime-manager");
|
|
15
15
|
|
|
16
|
+
const AUTO_THEME_SCRIPT_NAME = "html2apk-auto-theme.js";
|
|
17
|
+
const ONESIGNAL_SCRIPT_NAME = "html2apk-onesignal.js";
|
|
18
|
+
const ONESIGNAL_PLUGIN_PACKAGE = "onesignal-cordova-plugin";
|
|
19
|
+
|
|
16
20
|
function isRemoteAsset(assetPath) {
|
|
17
21
|
return /^https?:\/\//i.test(String(assetPath || ""));
|
|
18
22
|
}
|
|
@@ -34,6 +38,141 @@ function toCordovaPath(value) {
|
|
|
34
38
|
return String(value).replace(/\\/g, "/");
|
|
35
39
|
}
|
|
36
40
|
|
|
41
|
+
function isAutoTheme(options) {
|
|
42
|
+
return String(options.themeMode || options.theme || "").toLowerCase() === "auto";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function oneSignalAppId(options) {
|
|
46
|
+
return String(options.oneSignalAppId || options.onesignalAppId || options.oneSignal?.appId || options.onesignal?.appId || "").trim();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function hasOneSignal(options) {
|
|
50
|
+
return oneSignalAppId(options).length > 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function scriptTag(scriptPath) {
|
|
54
|
+
return `<script src="${scriptPath}"></script>`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function injectScriptIntoHtml(htmlPath, scriptPath) {
|
|
58
|
+
let html = await fs.readFile(htmlPath, "utf8");
|
|
59
|
+
if (html.includes(scriptPath)) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const tag = scriptTag(scriptPath);
|
|
64
|
+
if (/<\/body>/i.test(html)) {
|
|
65
|
+
html = html.replace(/<\/body>/i, ` ${tag}\n</body>`);
|
|
66
|
+
} else {
|
|
67
|
+
html = `${html}\n${tag}\n`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
await fs.writeFile(htmlPath, html, "utf8");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function installAutoThemeScript(buildDir, options) {
|
|
74
|
+
if (!isAutoTheme(options)) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const wwwDir = path.join(buildDir, "www");
|
|
79
|
+
const source = path.resolve(__dirname, "..", "templates", AUTO_THEME_SCRIPT_NAME);
|
|
80
|
+
const target = path.join(wwwDir, AUTO_THEME_SCRIPT_NAME);
|
|
81
|
+
const entryHtmlPath = path.resolve(wwwDir, options.entryFile || "index.html");
|
|
82
|
+
const scriptPath = toCordovaPath(path.relative(path.dirname(entryHtmlPath), target));
|
|
83
|
+
|
|
84
|
+
if (!isInside(wwwDir, entryHtmlPath)) {
|
|
85
|
+
throw new Error(`Entry file must stay inside the Cordova www folder: ${options.entryFile}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
await copyFile(source, target);
|
|
89
|
+
await injectScriptIntoHtml(entryHtmlPath, scriptPath || AUTO_THEME_SCRIPT_NAME);
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function jsString(value) {
|
|
94
|
+
return JSON.stringify(String(value || ""));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function installOneSignalScript(buildDir, options) {
|
|
98
|
+
const appId = oneSignalAppId(options);
|
|
99
|
+
if (!appId) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const wwwDir = path.join(buildDir, "www");
|
|
104
|
+
const source = path.resolve(__dirname, "..", "templates", ONESIGNAL_SCRIPT_NAME);
|
|
105
|
+
const target = path.join(wwwDir, ONESIGNAL_SCRIPT_NAME);
|
|
106
|
+
const entryHtmlPath = path.resolve(wwwDir, options.entryFile || "index.html");
|
|
107
|
+
const scriptPath = toCordovaPath(path.relative(path.dirname(entryHtmlPath), target));
|
|
108
|
+
const template = await fs.readFile(source, "utf8");
|
|
109
|
+
|
|
110
|
+
if (!isInside(wwwDir, entryHtmlPath)) {
|
|
111
|
+
throw new Error(`Entry file must stay inside the Cordova www folder: ${options.entryFile}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
await fs.writeFile(
|
|
115
|
+
target,
|
|
116
|
+
template.replace("__HTML2APK_ONESIGNAL_APP_ID__", jsString(appId)),
|
|
117
|
+
"utf8"
|
|
118
|
+
);
|
|
119
|
+
await injectScriptIntoHtml(entryHtmlPath, scriptPath || ONESIGNAL_SCRIPT_NAME);
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function removePluginPlatform(pluginXml, platformName) {
|
|
124
|
+
const pattern = new RegExp(`\\n?\\s*<platform name="${platformName}">[\\s\\S]*?\\n\\s*</platform>`, "i");
|
|
125
|
+
return pluginXml.replace(pattern, "");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function prepareBundledPlugin(buildDir, packageName) {
|
|
129
|
+
const localPath = path.resolve(__dirname, "..", "..", "node_modules", packageName);
|
|
130
|
+
try {
|
|
131
|
+
await fs.access(path.join(localPath, "plugin.xml"));
|
|
132
|
+
} catch {
|
|
133
|
+
return packageName;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const destination = path.join(buildDir, `${packageName}-android`);
|
|
137
|
+
await removePath(destination);
|
|
138
|
+
await copyDirectory(localPath, destination, () => false);
|
|
139
|
+
|
|
140
|
+
const packageJsonPath = path.join(destination, "package.json");
|
|
141
|
+
const sourcePackage = JSON.parse(await fs.readFile(packageJsonPath, "utf8"));
|
|
142
|
+
const pluginPackage = {
|
|
143
|
+
name: sourcePackage.name,
|
|
144
|
+
version: sourcePackage.version,
|
|
145
|
+
description: sourcePackage.description,
|
|
146
|
+
license: sourcePackage.license,
|
|
147
|
+
main: sourcePackage.main || "dist/index.cjs",
|
|
148
|
+
types: sourcePackage.types,
|
|
149
|
+
cordova: {
|
|
150
|
+
id: sourcePackage.cordova?.id || packageName,
|
|
151
|
+
platforms: ["android"]
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
Object.keys(pluginPackage).forEach((key) => {
|
|
155
|
+
if (pluginPackage[key] === undefined) {
|
|
156
|
+
delete pluginPackage[key];
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
await fs.writeFile(packageJsonPath, `${JSON.stringify(pluginPackage, null, 2)}\n`, "utf8");
|
|
160
|
+
|
|
161
|
+
const pluginXmlPath = path.join(destination, "plugin.xml");
|
|
162
|
+
let pluginXml = await fs.readFile(pluginXmlPath, "utf8");
|
|
163
|
+
pluginXml = removePluginPlatform(pluginXml, "ios");
|
|
164
|
+
await fs.writeFile(pluginXmlPath, pluginXml, "utf8");
|
|
165
|
+
|
|
166
|
+
return destination;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function hasPlugin(plugins, packageName) {
|
|
170
|
+
return (plugins || []).some((plugin) => {
|
|
171
|
+
const text = String(plugin || "").replace(/\\/g, "/").toLowerCase();
|
|
172
|
+
return text === packageName || text.endsWith(`/node_modules/${packageName}`) || text.endsWith(`/${packageName}`);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
37
176
|
async function copyCordovaAsset(projectRoot, buildDir, assetPath, assetName) {
|
|
38
177
|
if (!assetPath || isRemoteAsset(assetPath)) {
|
|
39
178
|
return assetPath;
|
|
@@ -136,10 +275,20 @@ async function buildApk(overrides = {}) {
|
|
|
136
275
|
cordovaOptions.androidSplashScreenAnimatedIcon = toBuildAssetPath(buildDir, cordovaOptions.splash);
|
|
137
276
|
await writeConfigXml(path.join(buildDir, "config.xml"), cordovaOptions);
|
|
138
277
|
await copyWebAssets(webRoot, path.join(buildDir, "www"), options, projectRoot);
|
|
278
|
+
if (await installAutoThemeScript(buildDir, options)) {
|
|
279
|
+
log("Theme mode: auto (system bars follow the visible screen color).");
|
|
280
|
+
}
|
|
281
|
+
if (await installOneSignalScript(buildDir, options)) {
|
|
282
|
+
log("OneSignal: enabled for remote push notifications.");
|
|
283
|
+
}
|
|
139
284
|
|
|
140
285
|
const bridgePluginPath = await installBridgePlugin(buildDir);
|
|
141
286
|
await addCordovaPlugin(buildDir, bridgePluginPath, runner);
|
|
142
287
|
|
|
288
|
+
if (hasOneSignal(options) && !hasPlugin(options.plugins, ONESIGNAL_PLUGIN_PACKAGE)) {
|
|
289
|
+
await addCordovaPlugin(buildDir, await prepareBundledPlugin(buildDir, ONESIGNAL_PLUGIN_PACKAGE), runner);
|
|
290
|
+
}
|
|
291
|
+
|
|
143
292
|
for (const plugin of options.plugins) {
|
|
144
293
|
await addCordovaPlugin(buildDir, plugin, runner);
|
|
145
294
|
}
|