html2apk 0.3.0 → 0.5.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 +114 -31
- package/examples/minimal/dist/MeuApp-1.0.0-debug.apk +0 -0
- package/package.json +1 -1
- package/src/cordova/project.js +19 -1
- package/src/core/build-apk.js +268 -30
- package/src/desktop/main.js +132 -1
- package/src/desktop/preload.js +13 -0
- package/src/desktop/renderer/index.html +3 -2
- package/src/desktop/renderer/renderer.js +997 -39
- package/src/desktop/renderer/styles.css +60 -2
- package/src/templates/cordova-plugin-html2apk-bridge/plugin.xml +2 -0
- package/src/templates/cordova-plugin-html2apk-bridge/src/android/Html2ApkBridge.java +458 -34
- package/src/templates/cordova-plugin-html2apk-bridge/src/android/NotificationClickReceiver.java +28 -0
- package/src/templates/cordova-plugin-html2apk-bridge/src/android/NotificationReceiver.java +1 -1
- package/src/templates/cordova-plugin-html2apk-bridge/www/html2apk-bridge.js +306 -10
- package/src/templates/html2apk-early-bridge.js +860 -0
- package/src/templates/html2apk-onesignal.js +1 -1
- package/src/utils/command-runner.js +3 -0
|
@@ -50,7 +50,7 @@ const i18n = {
|
|
|
50
50
|
oneSignalAppId: "OneSignal App ID",
|
|
51
51
|
oneSignalAppIdHint: "Opcional. Use o App ID do OneSignal, nao a REST API Key.",
|
|
52
52
|
androidPermissions: "Permissoes Android",
|
|
53
|
-
chooseIcon: "
|
|
53
|
+
chooseIcon: "Trocar icone PNG",
|
|
54
54
|
reviewBuild: "Revisar build",
|
|
55
55
|
debugBuild: "Debug tecnico",
|
|
56
56
|
debugBuildText: "Mantem a pasta Cordova temporaria para inspecao.",
|
|
@@ -65,6 +65,7 @@ const i18n = {
|
|
|
65
65
|
buildEyebrow: "Revisao",
|
|
66
66
|
buildTitle: "Confirme as informacoes e gere o APK",
|
|
67
67
|
startBuild: "Gerar APK",
|
|
68
|
+
startUsbDebug: "Testar no USB",
|
|
68
69
|
stepFolder: "Pasta recebida",
|
|
69
70
|
stepSettings: "Configuracoes completas",
|
|
70
71
|
stepDoctor: "Ambiente verificado",
|
|
@@ -76,9 +77,16 @@ const i18n = {
|
|
|
76
77
|
logsTitle: "Logs do processo",
|
|
77
78
|
codesEyebrow: "Bridge nativa",
|
|
78
79
|
codesTitle: "Codigos interpretados",
|
|
79
|
-
codesIntro: "
|
|
80
|
+
codesIntro: "Use estes blocos no JavaScript do seu projeto. O html2apk espera o Android ficar pronto, pede permissoes quando precisar e abre a configuracao certa quando o Android bloquear o pop-up.",
|
|
81
|
+
javaLabel: "Motor nativo",
|
|
82
|
+
doesLabel: "O que faz",
|
|
83
|
+
whenUseLabel: "Quando usar",
|
|
80
84
|
returnsLabel: "Retorno",
|
|
81
85
|
handlingLabel: "Como tratar",
|
|
86
|
+
exampleLabel: "Exemplo copiavel",
|
|
87
|
+
copyCode: "Copiar",
|
|
88
|
+
copiedCode: "Copiado",
|
|
89
|
+
copyFailed: "Nao foi possivel copiar",
|
|
82
90
|
clearLogs: "Limpar logs",
|
|
83
91
|
helpEyebrow: "Sem misterio",
|
|
84
92
|
helpTitle: "Doctor, build e dependencias",
|
|
@@ -102,9 +110,15 @@ const i18n = {
|
|
|
102
110
|
buildRunning: "Gerando APK",
|
|
103
111
|
buildOk: "APK gerado com sucesso",
|
|
104
112
|
buildFail: "Build falhou",
|
|
113
|
+
usbDebugRunning: "Gerando e instalando no celular USB",
|
|
114
|
+
usbDebugOk: "APK instalado no celular USB",
|
|
115
|
+
usbDebugFail: "Teste USB falhou",
|
|
105
116
|
droppedFolder: "Pasta recebida",
|
|
106
117
|
noFolderDrop: "Solte uma pasta do projeto",
|
|
107
118
|
projectLoaded: "Projeto carregado",
|
|
119
|
+
projectAutoUpdated: "Projeto atualizado automaticamente",
|
|
120
|
+
projectConfigReloaded: "Configuracoes recarregadas do projeto",
|
|
121
|
+
projectWatcherFail: "Nao foi possivel acompanhar alteracoes da pasta",
|
|
108
122
|
logsCleared: "Logs limpos",
|
|
109
123
|
settingsOk: "Configuracoes completas",
|
|
110
124
|
settingsMissing: "Complete as configuracoes obrigatorias",
|
|
@@ -114,7 +128,7 @@ const i18n = {
|
|
|
114
128
|
invalidPackageId: "Informe um Package ID valido, exemplo: com.seuapp.meuapp.",
|
|
115
129
|
invalidVersion: "Informe a versao no formato 1.0.0.",
|
|
116
130
|
missingMode: "Escolha o modo do app.",
|
|
117
|
-
|
|
131
|
+
defaultIcon: "Icone padrao do html2apk",
|
|
118
132
|
invalidIconType: "Use um icone PNG para evitar falhas no Android.",
|
|
119
133
|
invalidThemeColor: "Use uma cor hexadecimal valida, exemplo: #126fff.",
|
|
120
134
|
invalidOneSignalAppId: "Use um OneSignal App ID valido ou deixe vazio.",
|
|
@@ -141,6 +155,8 @@ const i18n = {
|
|
|
141
155
|
successEyebrow: "Concluido",
|
|
142
156
|
successTitle: "APK gerado com sucesso",
|
|
143
157
|
successText: "Seu arquivo Android esta pronto na pasta dist.",
|
|
158
|
+
usbSuccessTitle: "APK instalado no celular",
|
|
159
|
+
usbSuccessText: "O build debug foi enviado por USB. O app deve abrir no aparelho conectado.",
|
|
144
160
|
newBuild: "Novo build"
|
|
145
161
|
},
|
|
146
162
|
en: {
|
|
@@ -190,7 +206,7 @@ const i18n = {
|
|
|
190
206
|
oneSignalAppId: "OneSignal App ID",
|
|
191
207
|
oneSignalAppIdHint: "Optional. Use the OneSignal App ID, not the REST API Key.",
|
|
192
208
|
androidPermissions: "Android permissions",
|
|
193
|
-
chooseIcon: "
|
|
209
|
+
chooseIcon: "Change PNG icon",
|
|
194
210
|
reviewBuild: "Review build",
|
|
195
211
|
debugBuild: "Technical debug",
|
|
196
212
|
debugBuildText: "Keeps the temporary Cordova folder for inspection.",
|
|
@@ -205,6 +221,7 @@ const i18n = {
|
|
|
205
221
|
buildEyebrow: "Review",
|
|
206
222
|
buildTitle: "Confirm the information and build the APK",
|
|
207
223
|
startBuild: "Build APK",
|
|
224
|
+
startUsbDebug: "Test on USB",
|
|
208
225
|
stepFolder: "Folder received",
|
|
209
226
|
stepSettings: "Settings complete",
|
|
210
227
|
stepDoctor: "Environment checked",
|
|
@@ -216,9 +233,16 @@ const i18n = {
|
|
|
216
233
|
logsTitle: "Process logs",
|
|
217
234
|
codesEyebrow: "Native bridge",
|
|
218
235
|
codesTitle: "Interpreted code",
|
|
219
|
-
codesIntro: "
|
|
236
|
+
codesIntro: "Use these blocks in your project JavaScript. html2apk waits for Android to be ready, asks permissions when needed and opens the right settings screen when Android blocks the prompt.",
|
|
237
|
+
javaLabel: "Native engine",
|
|
238
|
+
doesLabel: "What it does",
|
|
239
|
+
whenUseLabel: "When to use",
|
|
220
240
|
returnsLabel: "Returns",
|
|
221
241
|
handlingLabel: "How to handle",
|
|
242
|
+
exampleLabel: "Copyable example",
|
|
243
|
+
copyCode: "Copy",
|
|
244
|
+
copiedCode: "Copied",
|
|
245
|
+
copyFailed: "Could not copy",
|
|
222
246
|
clearLogs: "Clear logs",
|
|
223
247
|
helpEyebrow: "Plain and simple",
|
|
224
248
|
helpTitle: "Doctor, build and dependencies",
|
|
@@ -242,9 +266,15 @@ const i18n = {
|
|
|
242
266
|
buildRunning: "Building APK",
|
|
243
267
|
buildOk: "APK generated successfully",
|
|
244
268
|
buildFail: "Build failed",
|
|
269
|
+
usbDebugRunning: "Building and installing on USB phone",
|
|
270
|
+
usbDebugOk: "APK installed on USB phone",
|
|
271
|
+
usbDebugFail: "USB test failed",
|
|
245
272
|
droppedFolder: "Folder received",
|
|
246
273
|
noFolderDrop: "Drop a project folder",
|
|
247
274
|
projectLoaded: "Project loaded",
|
|
275
|
+
projectAutoUpdated: "Project updated automatically",
|
|
276
|
+
projectConfigReloaded: "Settings reloaded from project",
|
|
277
|
+
projectWatcherFail: "Could not watch folder changes",
|
|
248
278
|
logsCleared: "Logs cleared",
|
|
249
279
|
settingsOk: "Settings complete",
|
|
250
280
|
settingsMissing: "Complete the required settings",
|
|
@@ -254,7 +284,7 @@ const i18n = {
|
|
|
254
284
|
invalidPackageId: "Enter a valid Package ID, example: com.yourapp.name.",
|
|
255
285
|
invalidVersion: "Enter the version as 1.0.0.",
|
|
256
286
|
missingMode: "Choose the app mode.",
|
|
257
|
-
|
|
287
|
+
defaultIcon: "Default html2apk icon",
|
|
258
288
|
invalidIconType: "Use a PNG icon to avoid Android build failures.",
|
|
259
289
|
invalidThemeColor: "Use a valid hex color, example: #126fff.",
|
|
260
290
|
invalidOneSignalAppId: "Use a valid OneSignal App ID or leave it empty.",
|
|
@@ -281,6 +311,8 @@ const i18n = {
|
|
|
281
311
|
successEyebrow: "Complete",
|
|
282
312
|
successTitle: "APK generated successfully",
|
|
283
313
|
successText: "Your Android file is ready in the dist folder.",
|
|
314
|
+
usbSuccessTitle: "APK installed on phone",
|
|
315
|
+
usbSuccessText: "The debug build was sent over USB. The app should open on the connected device.",
|
|
284
316
|
newBuild: "New build"
|
|
285
317
|
}
|
|
286
318
|
};
|
|
@@ -359,11 +391,11 @@ const nativeCodeEntries = [
|
|
|
359
391
|
handling: { pt: "Limite duracoes longas e deixe a permissao `VIBRATE` no app; se falhar, siga sem bloquear o fluxo principal.", en: "Keep long durations under control and include the `VIBRATE` permission; if it fails, continue without blocking the main flow." }
|
|
360
392
|
},
|
|
361
393
|
{
|
|
362
|
-
syntax: { pt: "notificar({ titulo, texto, acoes })", en: "notify({ title, text, actions })" },
|
|
394
|
+
syntax: { pt: "notificar({ titulo, texto, aoClicar?, acoes?, open? })", en: "notify({ title, text, onClick?, actions?, open? })" },
|
|
363
395
|
java: "notify",
|
|
364
|
-
description: { pt: "Cria notificacao Android imediata. `acoes`
|
|
365
|
-
returns: { pt: "Promise<void>; cliques chegam em `aoEvento('notificacao:clicada')` ou `aoClicarNotificacao()
|
|
366
|
-
handling: { pt: "
|
|
396
|
+
description: { pt: "Cria notificacao Android imediata. So `titulo` e `texto` ja bastam; `aoClicar`, `acoes` e `open` sao opcionais.", en: "Creates an immediate Android notification. `title` and `text` are enough; `onClick`, `actions` and `open` are optional." },
|
|
397
|
+
returns: { pt: "Promise<void>; cliques chegam em `aoEvento('notificacao:clicada')` ou `aoClicarNotificacao()` e tambem podem executar `aoClicar` automaticamente.", en: "Promise<void>; clicks arrive through `onEvent('notification:clicked')` or `onNotificationClick()` and can also run `onClick` automatically." },
|
|
398
|
+
handling: { pt: "Use `aoClicar: () => funcao()` enquanto o app esta vivo. Para agendada ou app fechado, prefira `aoClicar: { funcao, argumentos }`. `open:false` evita abrir a tela; JavaScript so roda assim se o app ainda estiver vivo, mas acoes externas como `abrirForaDoApp` funcionam por fallback nativo.", en: "Use `onClick: () => functionName()` while the app process is alive. For scheduled notifications or a closed app, prefer `onClick: { functionName, args }`. `open:false` avoids opening the screen; JavaScript only runs this way if the app is still alive, but external actions like `openOutsideApp` work through a native fallback." }
|
|
367
399
|
},
|
|
368
400
|
{
|
|
369
401
|
syntax: { pt: "agendarNotificacao({ titulo, texto, quando }) / agendarNotificacoes([...])", en: "scheduleNotification({ title, text, when }) / scheduleNotifications([...])" },
|
|
@@ -377,13 +409,13 @@ const nativeCodeEntries = [
|
|
|
377
409
|
java: "AlarmManager + NotificationStore",
|
|
378
410
|
description: { pt: "Cria um loop de notificacoes que continua funcionando com o app fechado e alterna os itens da lista.", en: "Creates a notification loop that keeps working with the app closed and rotates through the list items." },
|
|
379
411
|
returns: { pt: "{ id, when, repeating, loop }. Guarde o `id` para cancelar depois.", en: "{ id, when, repeating, loop }. Store the `id` to cancel later." },
|
|
380
|
-
handling: { pt: "
|
|
412
|
+
handling: { pt: "Cancele com `cancelarNotificacao(id)`. Se o clique precisar chamar funcao automaticamente mesmo apos o Android reabrir o app, use `funcao` + `argumentos` no `aoClicar`.", en: "Cancel with `cancelNotification(id)`. If the click must call a function automatically after Android reopens the app, use `functionName` + `args` in `onClick`." }
|
|
381
413
|
},
|
|
382
414
|
{
|
|
383
415
|
syntax: { pt: "solicitarPermissaoNotificacoes()", en: "requestNotificationPermission()" },
|
|
384
416
|
java: "requestNotificationPermission",
|
|
385
417
|
description: { pt: "Pede permissao para mostrar notificacoes quando o Android exigir.", en: "Requests permission to show notifications when Android requires it." },
|
|
386
|
-
returns: { pt: "{ permission, required, granted }.", en: "{ permission, required, granted }." },
|
|
418
|
+
returns: { pt: "{ permission, required, granted, requiresSettings, settingsOpened }.", en: "{ permission, required, granted, requiresSettings, settingsOpened }." },
|
|
387
419
|
handling: { pt: "Se `granted` for falso, desative botoes de notificacao ou explique ao usuario como habilitar depois.", en: "If `granted` is false, disable notification buttons or explain how the user can enable it later." }
|
|
388
420
|
},
|
|
389
421
|
{
|
|
@@ -398,7 +430,7 @@ const nativeCodeEntries = [
|
|
|
398
430
|
java: "permissionStatus",
|
|
399
431
|
description: { pt: "Consulta se permissoes Android estao liberadas.", en: "Checks whether Android permissions are granted." },
|
|
400
432
|
returns: { pt: "Objeto com permissao Android como chave e boolean como valor.", en: "Object with Android permission names as keys and booleans as values." },
|
|
401
|
-
handling: { pt: "Use
|
|
433
|
+
handling: { pt: "Use apenas para consultar. Para pedir permissao, chame a funcao real (`lanterna`, `ouvirMic`, `notificar`) ou a API manual correspondente.", en: "Use only to check. To request permission, call the real function (`flashlight`, `listenMic`, `notify`) or the matching manual API." }
|
|
402
434
|
},
|
|
403
435
|
{
|
|
404
436
|
syntax: { pt: "aoEvento('app:background', fn)", en: "onEvent('app:background', fn)" },
|
|
@@ -424,23 +456,23 @@ const nativeCodeEntries = [
|
|
|
424
456
|
{
|
|
425
457
|
syntax: { pt: "solicitarPermissaoCamera()", en: "requestCameraPermission()" },
|
|
426
458
|
java: "requestCameraPermission",
|
|
427
|
-
description: { pt: "Pede permissao de camera antes
|
|
428
|
-
returns: { pt: "{ permission, required, granted }.", en: "{ permission, required, granted }." },
|
|
429
|
-
handling: { pt: "
|
|
459
|
+
description: { pt: "Pede permissao de camera manualmente, se voce quiser preparar a experiencia antes da lanterna.", en: "Requests camera permission manually, if you want to prepare the experience before flashlight." },
|
|
460
|
+
returns: { pt: "{ permission, required, granted, requiresSettings, settingsOpened }.", en: "{ permission, required, granted, requiresSettings, settingsOpened }." },
|
|
461
|
+
handling: { pt: "`lanterna()` ja pede permissao sozinha. Use esta funcao apenas se quiser explicar antes e controlar a UI pelo `granted`.", en: "`flashlight()` already requests permission by itself. Use this only if you want to explain first and control the UI from `granted`." }
|
|
430
462
|
},
|
|
431
463
|
{
|
|
432
464
|
syntax: { pt: "await ouvirMic(); const audio = await pararMic()", en: "await listenMic(); const audio = await stopMic()" },
|
|
433
465
|
java: "MediaRecorder",
|
|
434
466
|
description: { pt: "Comeca a gravar pelo microfone e finaliza retornando o audio em base64.", en: "Starts recording from the microphone and finishes by returning the audio as base64." },
|
|
435
|
-
returns: { pt: "`ouvirMic`: { recording, startedAt, granted }. `pararMic`: { base64, mimeType, extension, durationMs, size }.", en: "`listenMic`: { recording, startedAt, granted }. `stopMic`: { base64, mimeType, extension, durationMs, size }." },
|
|
436
|
-
handling: { pt: "
|
|
467
|
+
returns: { pt: "`ouvirMic`: { recording, startedAt, granted, settingsOpened }. `pararMic`: { base64, mimeType, extension, durationMs, size }.", en: "`listenMic`: { recording, startedAt, granted, settingsOpened }. `stopMic`: { base64, mimeType, extension, durationMs, size }." },
|
|
468
|
+
handling: { pt: "`ouvirMic()` pede microfone sozinho. Se `settingsOpened` vier true, o Android abriu as configuracoes; quando o usuario voltar, chame `ouvirMic()` de novo.", en: "`listenMic()` requests the microphone by itself. If `settingsOpened` is true, Android opened settings; when the user returns, call `listenMic()` again." }
|
|
437
469
|
},
|
|
438
470
|
{
|
|
439
471
|
syntax: { pt: "lanterna(true) / statusLanterna()", en: "flashlight(true) / flashlightStatus()" },
|
|
440
472
|
java: "flashlight",
|
|
441
473
|
description: { pt: "Liga, desliga e consulta a lanterna do aparelho.", en: "Turns the device flashlight on/off and reads its status." },
|
|
442
474
|
returns: { pt: "{ available, enabled, permissionGranted }.", en: "{ available, enabled, permissionGranted }." },
|
|
443
|
-
handling: { pt: "
|
|
475
|
+
handling: { pt: "Ao chamar, o html2apk pede CAMERA automaticamente. Se `settingsOpened` vier true, o Android abriu as configuracoes porque o pop-up estava bloqueado.", en: "When called, html2apk automatically requests CAMERA. If `settingsOpened` is true, Android opened settings because the prompt was blocked." }
|
|
444
476
|
},
|
|
445
477
|
{
|
|
446
478
|
syntax: { pt: "alternarLanterna()", en: "toggleFlashlight()" },
|
|
@@ -608,7 +640,709 @@ const nativeCodeEntries = [
|
|
|
608
640
|
java: "FloatingIconService",
|
|
609
641
|
description: { pt: "Controla o icone flutuante quando o app foi gerado em modo floating.", en: "Controls the floating icon when the app was generated in floating mode." },
|
|
610
642
|
returns: { pt: "Promise<void>.", en: "Promise<void>." },
|
|
611
|
-
handling: { pt: "
|
|
643
|
+
handling: { pt: "`iniciarIconeFlutuante()` abre a tela de permissao automaticamente se faltar sobreposicao. Quando o usuario voltar, chame novamente para iniciar.", en: "`startFloatingIcon()` opens the permission screen automatically if draw-over-apps is missing. When the user comes back, call it again to start." }
|
|
644
|
+
}
|
|
645
|
+
];
|
|
646
|
+
|
|
647
|
+
const nativeCodeRecipes = [
|
|
648
|
+
{
|
|
649
|
+
when: { pt: "Para avisos curtos dentro do app, como sucesso, erro simples ou confirmacao.", en: "For short in-app feedback such as success, simple errors or confirmations." },
|
|
650
|
+
example: {
|
|
651
|
+
pt: `try {
|
|
652
|
+
await toast("Salvo com sucesso");
|
|
653
|
+
} catch (erro) {
|
|
654
|
+
console.error("Toast falhou", erro);
|
|
655
|
+
}`,
|
|
656
|
+
en: `try {
|
|
657
|
+
await toast("Saved successfully");
|
|
658
|
+
} catch (error) {
|
|
659
|
+
console.error("Toast failed", error);
|
|
660
|
+
}`
|
|
661
|
+
}
|
|
662
|
+
},
|
|
663
|
+
{
|
|
664
|
+
when: { pt: "Para dar feedback fisico em botoes, alertas ou acoes importantes.", en: "For physical feedback on buttons, alerts or important actions." },
|
|
665
|
+
example: {
|
|
666
|
+
pt: `await vibrar(250);`,
|
|
667
|
+
en: `await vibrate(250);`
|
|
668
|
+
}
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
when: { pt: "Para mostrar uma notificacao simples agora. `aoClicar`, `acoes` e `open` sao opcionais.", en: "To show a simple notification now. `onClick`, `actions` and `open` are optional." },
|
|
672
|
+
example: {
|
|
673
|
+
pt: `await notificar({
|
|
674
|
+
titulo: "Pedido aprovado",
|
|
675
|
+
texto: "Toque para abrir o app"
|
|
676
|
+
});`,
|
|
677
|
+
en: `await notify({
|
|
678
|
+
title: "Order approved",
|
|
679
|
+
text: "Tap to open the app"
|
|
680
|
+
});`
|
|
681
|
+
}
|
|
682
|
+
},
|
|
683
|
+
{
|
|
684
|
+
when: { pt: "Para executar algo quando a notificacao for clicada.", en: "To run something when the notification is clicked." },
|
|
685
|
+
example: {
|
|
686
|
+
pt: `await notificar({
|
|
687
|
+
titulo: "Pedido aprovado",
|
|
688
|
+
texto: "Toque para abrir os detalhes",
|
|
689
|
+
aoClicar: () => abrirForaDoApp("https://exemplo.com/pedidos/123")
|
|
690
|
+
});`,
|
|
691
|
+
en: `await notify({
|
|
692
|
+
title: "Order approved",
|
|
693
|
+
text: "Tap to open details",
|
|
694
|
+
onClick: () => openOutsideApp("https://example.com/orders/123")
|
|
695
|
+
});`
|
|
696
|
+
}
|
|
697
|
+
},
|
|
698
|
+
{
|
|
699
|
+
when: { pt: "Para colocar botoes na notificacao, cada um chamando uma funcao.", en: "To add buttons to the notification, each one calling a function." },
|
|
700
|
+
example: {
|
|
701
|
+
pt: `window.marcarPedidoLido = (id) => {
|
|
702
|
+
localStorage.setItem("pedido:" + id, "lido");
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
await notificar({
|
|
706
|
+
titulo: "Pedido aprovado",
|
|
707
|
+
texto: "Escolha uma acao",
|
|
708
|
+
acoes: [
|
|
709
|
+
{
|
|
710
|
+
id: "abrir",
|
|
711
|
+
titulo: "Abrir",
|
|
712
|
+
open: true,
|
|
713
|
+
aoClicar: { funcao: "abrirNoApp", argumentos: ["#/pedido/123"] }
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
id: "lido",
|
|
717
|
+
titulo: "Marcar lido",
|
|
718
|
+
open: false,
|
|
719
|
+
aoClicar: { funcao: "marcarPedidoLido", argumentos: [123], open: false }
|
|
720
|
+
}
|
|
721
|
+
]
|
|
722
|
+
});`,
|
|
723
|
+
en: `window.markOrderRead = (id) => {
|
|
724
|
+
localStorage.setItem("order:" + id, "read");
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
await notify({
|
|
728
|
+
title: "Order approved",
|
|
729
|
+
text: "Choose an action",
|
|
730
|
+
actions: [
|
|
731
|
+
{
|
|
732
|
+
id: "open",
|
|
733
|
+
title: "Open",
|
|
734
|
+
open: true,
|
|
735
|
+
onClick: { functionName: "openInApp", args: ["#/order/123"] }
|
|
736
|
+
},
|
|
737
|
+
{
|
|
738
|
+
id: "read",
|
|
739
|
+
title: "Mark read",
|
|
740
|
+
open: false,
|
|
741
|
+
onClick: { functionName: "markOrderRead", args: [123], open: false }
|
|
742
|
+
}
|
|
743
|
+
]
|
|
744
|
+
});`
|
|
745
|
+
}
|
|
746
|
+
},
|
|
747
|
+
{
|
|
748
|
+
when: { pt: "Para clique de notificacao agendada ou com app fechado.", en: "For scheduled notification clicks or when the app is closed." },
|
|
749
|
+
example: {
|
|
750
|
+
pt: `await agendarNotificacao({
|
|
751
|
+
titulo: "Pedido aprovado",
|
|
752
|
+
texto: "Toque para abrir os detalhes",
|
|
753
|
+
quando: Date.now() + 60 * 1000,
|
|
754
|
+
aoClicar: {
|
|
755
|
+
funcao: "abrirForaDoApp",
|
|
756
|
+
argumentos: ["https://exemplo.com/pedidos/123"]
|
|
757
|
+
}
|
|
758
|
+
});`,
|
|
759
|
+
en: `await scheduleNotification({
|
|
760
|
+
title: "Order approved",
|
|
761
|
+
text: "Tap to open details",
|
|
762
|
+
when: Date.now() + 60 * 1000,
|
|
763
|
+
onClick: {
|
|
764
|
+
functionName: "openOutsideApp",
|
|
765
|
+
args: ["https://example.com/orders/123"]
|
|
766
|
+
}
|
|
767
|
+
});`
|
|
768
|
+
}
|
|
769
|
+
},
|
|
770
|
+
{
|
|
771
|
+
when: { pt: "Para lembrar o usuario depois, mesmo se o app nao estiver aberto.", en: "To remind the user later, even if the app is not open." },
|
|
772
|
+
example: {
|
|
773
|
+
pt: `const aviso = await agendarNotificacao({
|
|
774
|
+
titulo: "Lembrete",
|
|
775
|
+
texto: "Volte ao app daqui 1 minuto",
|
|
776
|
+
quando: Date.now() + 60 * 1000
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
console.log("ID para cancelar depois:", aviso.id);`,
|
|
780
|
+
en: `const reminder = await scheduleNotification({
|
|
781
|
+
title: "Reminder",
|
|
782
|
+
text: "Come back in 1 minute",
|
|
783
|
+
when: Date.now() + 60 * 1000
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
console.log("ID to cancel later:", reminder.id);`
|
|
787
|
+
}
|
|
788
|
+
},
|
|
789
|
+
{
|
|
790
|
+
when: { pt: "Para uma sequencia recorrente de notificacoes, como a cada 12 horas.", en: "For a repeating sequence of notifications, such as every 12 hours." },
|
|
791
|
+
example: {
|
|
792
|
+
pt: `const loop = await agendarLoopNotificacoes({
|
|
793
|
+
aCada: "12h",
|
|
794
|
+
notificacoes: [
|
|
795
|
+
{ titulo: "Agua", texto: "Beba agua agora" },
|
|
796
|
+
{ titulo: "Alongar", texto: "Faca uma pausa rapida" }
|
|
797
|
+
]
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
// Depois, se quiser parar:
|
|
801
|
+
// await cancelarNotificacao(loop.id);`,
|
|
802
|
+
en: `const loop = await scheduleNotificationLoop({
|
|
803
|
+
every: "12h",
|
|
804
|
+
notifications: [
|
|
805
|
+
{ title: "Water", text: "Drink water now" },
|
|
806
|
+
{ title: "Stretch", text: "Take a quick break" }
|
|
807
|
+
]
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
// Later, to stop it:
|
|
811
|
+
// await cancelNotification(loop.id);`
|
|
812
|
+
}
|
|
813
|
+
},
|
|
814
|
+
{
|
|
815
|
+
when: { pt: "Opcional: use se quiser pedir a permissao antes de chamar notificacoes.", en: "Optional: use it if you want to ask permission before calling notifications." },
|
|
816
|
+
example: {
|
|
817
|
+
pt: `const permissao = await solicitarPermissaoNotificacoes();
|
|
818
|
+
|
|
819
|
+
if (!permissao.granted) {
|
|
820
|
+
if (permissao.settingsOpened) {
|
|
821
|
+
console.log("O Android abriu as configuracoes do app");
|
|
822
|
+
}
|
|
823
|
+
}`,
|
|
824
|
+
en: `const permission = await requestNotificationPermission();
|
|
825
|
+
|
|
826
|
+
if (!permission.granted) {
|
|
827
|
+
if (permission.settingsOpened) {
|
|
828
|
+
console.log("Android opened the app settings");
|
|
829
|
+
}
|
|
830
|
+
}`
|
|
831
|
+
}
|
|
832
|
+
},
|
|
833
|
+
{
|
|
834
|
+
when: { pt: "Para push remoto enviado pelo painel/API do OneSignal. Requer OneSignal App ID no build.", en: "For remote push sent by the OneSignal dashboard/API. Requires OneSignal App ID in the build." },
|
|
835
|
+
example: {
|
|
836
|
+
pt: `const permitido = await solicitarPermissaoPush();
|
|
837
|
+
|
|
838
|
+
if (permitido) {
|
|
839
|
+
await identificarUsuarioPush("usuario-123");
|
|
840
|
+
await adicionarTagPush("plano", "premium");
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
const parar = aoClicarPush((evento) => {
|
|
844
|
+
abrirNoApp("#/notificacoes");
|
|
845
|
+
});`,
|
|
846
|
+
en: `const allowed = await requestPushPermission();
|
|
847
|
+
|
|
848
|
+
if (allowed) {
|
|
849
|
+
await loginPushUser("user-123");
|
|
850
|
+
await addPushTag("plan", "premium");
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const stop = onPushClick((event) => {
|
|
854
|
+
openInApp("#/notifications");
|
|
855
|
+
});`
|
|
856
|
+
}
|
|
857
|
+
},
|
|
858
|
+
{
|
|
859
|
+
when: { pt: "Para consultar permissoes sem abrir uma solicitacao na tela.", en: "To check permissions without opening a permission prompt." },
|
|
860
|
+
example: {
|
|
861
|
+
pt: `const permissoes = await statusPermissoes([
|
|
862
|
+
"CAMERA",
|
|
863
|
+
"RECORD_AUDIO",
|
|
864
|
+
"POST_NOTIFICATIONS"
|
|
865
|
+
]);
|
|
866
|
+
|
|
867
|
+
console.log(permissoes);`,
|
|
868
|
+
en: `const permissions = await permissionStatus([
|
|
869
|
+
"CAMERA",
|
|
870
|
+
"RECORD_AUDIO",
|
|
871
|
+
"POST_NOTIFICATIONS"
|
|
872
|
+
]);
|
|
873
|
+
|
|
874
|
+
console.log(permissions);`
|
|
875
|
+
}
|
|
876
|
+
},
|
|
877
|
+
{
|
|
878
|
+
when: { pt: "Para reagir a eventos do Android, como app indo para segundo plano ou clique em notificacao.", en: "To react to Android events, such as backgrounding or notification clicks." },
|
|
879
|
+
example: {
|
|
880
|
+
pt: `const parar = aoEvento("app:background", (evento) => {
|
|
881
|
+
console.log("Saiu da frente", evento.timestamp);
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
// Quando nao precisar mais:
|
|
885
|
+
// parar();`,
|
|
886
|
+
en: `const stop = onEvent("app:background", (event) => {
|
|
887
|
+
console.log("Moved to background", event.timestamp);
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
// When you no longer need it:
|
|
891
|
+
// stop();`
|
|
892
|
+
}
|
|
893
|
+
},
|
|
894
|
+
{
|
|
895
|
+
when: { pt: "Para pausar/resumir tarefas quando o usuario minimiza ou volta para o app.", en: "To pause/resume work when the user minimizes or returns to the app." },
|
|
896
|
+
example: {
|
|
897
|
+
pt: `const pararMinimizar = aoMinimizar(() => {
|
|
898
|
+
console.log("Pause videos, timers ou leituras pesadas");
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
const pararVoltar = aoVoltarParaApp(() => {
|
|
902
|
+
console.log("Atualize dados se precisar");
|
|
903
|
+
});`,
|
|
904
|
+
en: `const stopMinimize = onMinimize(() => {
|
|
905
|
+
console.log("Pause videos, timers or heavy reads");
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
const stopResume = onAppResume(() => {
|
|
909
|
+
console.log("Refresh data if needed");
|
|
910
|
+
});`
|
|
911
|
+
}
|
|
912
|
+
},
|
|
913
|
+
{
|
|
914
|
+
when: { pt: "Para abrir uma tela especifica quando o APK foi chamado por link.", en: "To open a specific screen when the APK was launched from a link." },
|
|
915
|
+
example: {
|
|
916
|
+
pt: `const linkInicial = await obterLinkInicial();
|
|
917
|
+
if (linkInicial) {
|
|
918
|
+
abrirNoApp("#" + linkInicial.path);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
aoAbrirLink((link) => {
|
|
922
|
+
console.log("Link recebido", link.url);
|
|
923
|
+
});`,
|
|
924
|
+
en: `const initialLink = await getInitialLink();
|
|
925
|
+
if (initialLink) {
|
|
926
|
+
openInApp("#" + initialLink.path);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
onOpenLink((link) => {
|
|
930
|
+
console.log("Link received", link.url);
|
|
931
|
+
});`
|
|
932
|
+
}
|
|
933
|
+
},
|
|
934
|
+
{
|
|
935
|
+
when: { pt: "Opcional: use antes da lanterna se quiser explicar a permissao ao usuario.", en: "Optional: use it before flashlight if you want to explain the permission." },
|
|
936
|
+
example: {
|
|
937
|
+
pt: `const camera = await solicitarPermissaoCamera();
|
|
938
|
+
|
|
939
|
+
if (camera.granted) {
|
|
940
|
+
await lanterna(true);
|
|
941
|
+
} else if (camera.settingsOpened) {
|
|
942
|
+
console.log("Libere Camera nas configuracoes e volte ao app");
|
|
943
|
+
}`,
|
|
944
|
+
en: `const camera = await requestCameraPermission();
|
|
945
|
+
|
|
946
|
+
if (camera.granted) {
|
|
947
|
+
await flashlight(true);
|
|
948
|
+
} else if (camera.settingsOpened) {
|
|
949
|
+
console.log("Allow Camera in settings and return to the app");
|
|
950
|
+
}`
|
|
951
|
+
}
|
|
952
|
+
},
|
|
953
|
+
{
|
|
954
|
+
when: { pt: "Para gravar audio curto pelo microfone e receber o arquivo em base64.", en: "To record short audio from the microphone and receive the file as base64." },
|
|
955
|
+
example: {
|
|
956
|
+
pt: `const inicio = await ouvirMic();
|
|
957
|
+
if (inicio.settingsOpened) {
|
|
958
|
+
console.log("Libere Microfone nas configuracoes e tente de novo");
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
setTimeout(async () => {
|
|
963
|
+
const audio = await pararMic();
|
|
964
|
+
const url = "data:" + audio.mimeType + ";base64," + audio.base64;
|
|
965
|
+
new Audio(url).play();
|
|
966
|
+
}, 3000);`,
|
|
967
|
+
en: `const start = await listenMic();
|
|
968
|
+
if (start.settingsOpened) {
|
|
969
|
+
console.log("Allow Microphone in settings and try again");
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
setTimeout(async () => {
|
|
974
|
+
const audio = await stopMic();
|
|
975
|
+
const url = "data:" + audio.mimeType + ";base64," + audio.base64;
|
|
976
|
+
new Audio(url).play();
|
|
977
|
+
}, 3000);`
|
|
978
|
+
}
|
|
979
|
+
},
|
|
980
|
+
{
|
|
981
|
+
when: { pt: "Para ligar ou desligar a lanterna. A permissao de camera e pedida automaticamente.", en: "To turn the flashlight on or off. Camera permission is requested automatically." },
|
|
982
|
+
example: {
|
|
983
|
+
pt: `const status = await lanterna(true);
|
|
984
|
+
|
|
985
|
+
if (status.settingsOpened) {
|
|
986
|
+
console.log("Libere Camera nas configuracoes e chame lanterna(true) de novo");
|
|
987
|
+
} else if (!status.available) {
|
|
988
|
+
await toast("Este aparelho nao tem lanterna");
|
|
989
|
+
}`,
|
|
990
|
+
en: `const status = await flashlight(true);
|
|
991
|
+
|
|
992
|
+
if (status.settingsOpened) {
|
|
993
|
+
console.log("Allow Camera in settings and call flashlight(true) again");
|
|
994
|
+
} else if (!status.available) {
|
|
995
|
+
await toast("This device has no flashlight");
|
|
996
|
+
}`
|
|
997
|
+
}
|
|
998
|
+
},
|
|
999
|
+
{
|
|
1000
|
+
when: { pt: "Para um botao que liga/desliga a lanterna sem guardar estado manualmente.", en: "For a button that toggles flashlight without tracking state manually." },
|
|
1001
|
+
example: {
|
|
1002
|
+
pt: `const status = await alternarLanterna();
|
|
1003
|
+
console.log("Lanterna ligada?", status.enabled);`,
|
|
1004
|
+
en: `const status = await toggleFlashlight();
|
|
1005
|
+
console.log("Flashlight on?", status.enabled);`
|
|
1006
|
+
}
|
|
1007
|
+
},
|
|
1008
|
+
{
|
|
1009
|
+
when: { pt: "Para deixar o usuario escolher uma foto da galeria ou arquivos de imagem.", en: "To let the user choose a photo from gallery or image files." },
|
|
1010
|
+
example: {
|
|
1011
|
+
pt: `const imagem = await escolherImagem();
|
|
1012
|
+
|
|
1013
|
+
if (imagem) {
|
|
1014
|
+
document.querySelector("img.preview").src = imagem.uri;
|
|
1015
|
+
}`,
|
|
1016
|
+
en: `const image = await pickImage();
|
|
1017
|
+
|
|
1018
|
+
if (image) {
|
|
1019
|
+
document.querySelector("img.preview").src = image.uri;
|
|
1020
|
+
}`
|
|
1021
|
+
}
|
|
1022
|
+
},
|
|
1023
|
+
{
|
|
1024
|
+
when: { pt: "Para galeria com selecao multipla, como anexar varias fotos.", en: "For multiple gallery selection, such as attaching many photos." },
|
|
1025
|
+
example: {
|
|
1026
|
+
pt: `const imagens = await escolherImagens({ multiplo: true });
|
|
1027
|
+
|
|
1028
|
+
for (const imagem of imagens) {
|
|
1029
|
+
console.log(imagem.nome, imagem.tamanho);
|
|
1030
|
+
}`,
|
|
1031
|
+
en: `const images = await pickImages({ multiple: true });
|
|
1032
|
+
|
|
1033
|
+
for (const image of images) {
|
|
1034
|
+
console.log(image.name, image.size);
|
|
1035
|
+
}`
|
|
1036
|
+
}
|
|
1037
|
+
},
|
|
1038
|
+
{
|
|
1039
|
+
when: { pt: "Para escolher PDF, ZIP, TXT ou qualquer tipo de documento.", en: "To choose PDF, ZIP, TXT or any document type." },
|
|
1040
|
+
example: {
|
|
1041
|
+
pt: `const pdf = await escolherArquivo({
|
|
1042
|
+
tipos: ["application/pdf"]
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
if (pdf) {
|
|
1046
|
+
console.log("PDF escolhido:", pdf.nome);
|
|
1047
|
+
}`,
|
|
1048
|
+
en: `const pdf = await pickFile({
|
|
1049
|
+
types: ["application/pdf"]
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
if (pdf) {
|
|
1053
|
+
console.log("Chosen PDF:", pdf.name);
|
|
1054
|
+
}`
|
|
1055
|
+
}
|
|
1056
|
+
},
|
|
1057
|
+
{
|
|
1058
|
+
when: { pt: "Para anexar varios arquivos de uma vez.", en: "To attach multiple files at once." },
|
|
1059
|
+
example: {
|
|
1060
|
+
pt: `const arquivos = await escolherArquivos({ multiplo: true });
|
|
1061
|
+
|
|
1062
|
+
const total = arquivos.reduce((soma, arquivo) => {
|
|
1063
|
+
return soma + (arquivo.tamanho || 0);
|
|
1064
|
+
}, 0);
|
|
1065
|
+
|
|
1066
|
+
console.log("Total em bytes:", total);`,
|
|
1067
|
+
en: `const files = await pickFiles({ multiple: true });
|
|
1068
|
+
|
|
1069
|
+
const total = files.reduce((sum, file) => {
|
|
1070
|
+
return sum + (file.size || 0);
|
|
1071
|
+
}, 0);
|
|
1072
|
+
|
|
1073
|
+
console.log("Total bytes:", total);`
|
|
1074
|
+
}
|
|
1075
|
+
},
|
|
1076
|
+
{
|
|
1077
|
+
when: { pt: "Para pedir um video do usuario sem mostrar documentos que nao sejam video.", en: "To ask the user for a video without showing non-video documents." },
|
|
1078
|
+
example: {
|
|
1079
|
+
pt: `const video = await escolherVideo();
|
|
1080
|
+
|
|
1081
|
+
if (video && video.tamanho > 50 * 1024 * 1024) {
|
|
1082
|
+
await toast("Video muito grande");
|
|
1083
|
+
}`,
|
|
1084
|
+
en: `const video = await pickVideo();
|
|
1085
|
+
|
|
1086
|
+
if (video && video.size > 50 * 1024 * 1024) {
|
|
1087
|
+
await toast("Video is too large");
|
|
1088
|
+
}`
|
|
1089
|
+
}
|
|
1090
|
+
},
|
|
1091
|
+
{
|
|
1092
|
+
when: { pt: "Para o usuario escolher uma pasta quando o Android permitir acesso por URI.", en: "To let the user choose a folder when Android allows URI access." },
|
|
1093
|
+
example: {
|
|
1094
|
+
pt: `const pasta = await escolherPasta();
|
|
1095
|
+
|
|
1096
|
+
if (pasta.uri) {
|
|
1097
|
+
console.log("Pasta escolhida:", pasta.uri);
|
|
1098
|
+
}`,
|
|
1099
|
+
en: `const folder = await pickFolder();
|
|
1100
|
+
|
|
1101
|
+
if (folder.uri) {
|
|
1102
|
+
console.log("Chosen folder:", folder.uri);
|
|
1103
|
+
}`
|
|
1104
|
+
}
|
|
1105
|
+
},
|
|
1106
|
+
{
|
|
1107
|
+
when: { pt: "Para salvar texto ou base64 em um arquivo escolhido pelo usuario.", en: "To save text or base64 to a file chosen by the user." },
|
|
1108
|
+
example: {
|
|
1109
|
+
pt: `const salvo = await salvarArquivo({
|
|
1110
|
+
nome: "relatorio.txt",
|
|
1111
|
+
mimeType: "text/plain",
|
|
1112
|
+
conteudo: "Conteudo do relatorio"
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
if (salvo.saved) {
|
|
1116
|
+
await toast("Arquivo salvo");
|
|
1117
|
+
}`,
|
|
1118
|
+
en: `const saved = await saveFile({
|
|
1119
|
+
name: "report.txt",
|
|
1120
|
+
mimeType: "text/plain",
|
|
1121
|
+
content: "Report content"
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
if (saved.saved) {
|
|
1125
|
+
await toast("File saved");
|
|
1126
|
+
}`
|
|
1127
|
+
}
|
|
1128
|
+
},
|
|
1129
|
+
{
|
|
1130
|
+
when: { pt: "Para abrir o compartilhamento nativo do Android com texto e/ou link.", en: "To open Android native sharing with text and/or link." },
|
|
1131
|
+
example: {
|
|
1132
|
+
pt: `await compartilhar({
|
|
1133
|
+
texto: "Veja esse app",
|
|
1134
|
+
url: "https://exemplo.com"
|
|
1135
|
+
});`,
|
|
1136
|
+
en: `await share({
|
|
1137
|
+
text: "Check this app",
|
|
1138
|
+
url: "https://example.com"
|
|
1139
|
+
});`
|
|
1140
|
+
}
|
|
1141
|
+
},
|
|
1142
|
+
{
|
|
1143
|
+
when: { pt: "Para copiar dados para a area de transferencia ou ler o que esta copiado.", en: "To copy data to the clipboard or read what is copied." },
|
|
1144
|
+
example: {
|
|
1145
|
+
pt: `await copiarTexto("ABC-123");
|
|
1146
|
+
await toast("Codigo copiado");
|
|
1147
|
+
|
|
1148
|
+
const copiado = await lerTextoCopiado();
|
|
1149
|
+
console.log(copiado);`,
|
|
1150
|
+
en: `await copyText("ABC-123");
|
|
1151
|
+
await toast("Code copied");
|
|
1152
|
+
|
|
1153
|
+
const copied = await readText();
|
|
1154
|
+
console.log(copied);`
|
|
1155
|
+
}
|
|
1156
|
+
},
|
|
1157
|
+
{
|
|
1158
|
+
when: { pt: "Para impedir que a tela apague em leitura, video, mapa ou monitoramento.", en: "To keep screen awake during reading, video, maps or monitoring." },
|
|
1159
|
+
example: {
|
|
1160
|
+
pt: `await manterTelaLigada(true);
|
|
1161
|
+
|
|
1162
|
+
// Ao sair da tela:
|
|
1163
|
+
await manterTelaLigada(false);`,
|
|
1164
|
+
en: `await keepScreenOn(true);
|
|
1165
|
+
|
|
1166
|
+
// When leaving the screen:
|
|
1167
|
+
await keepScreenOn(false);`
|
|
1168
|
+
}
|
|
1169
|
+
},
|
|
1170
|
+
{
|
|
1171
|
+
when: { pt: "Para controlar o brilho so dentro do APK, sem mudar o brilho do sistema todo.", en: "To control brightness only inside the APK, without changing the whole system brightness." },
|
|
1172
|
+
example: {
|
|
1173
|
+
pt: `await brilhoTela(0.8);
|
|
1174
|
+
|
|
1175
|
+
// Restaurar comportamento padrao:
|
|
1176
|
+
await brilhoTela(-1);`,
|
|
1177
|
+
en: `await setScreenBrightness(0.8);
|
|
1178
|
+
|
|
1179
|
+
// Restore default behavior:
|
|
1180
|
+
await setScreenBrightness(-1);`
|
|
1181
|
+
}
|
|
1182
|
+
},
|
|
1183
|
+
{
|
|
1184
|
+
when: { pt: "Para combinar status bar/navigation bar com uma tela especifica do app.", en: "To match status/navigation bars with a specific app screen." },
|
|
1185
|
+
example: {
|
|
1186
|
+
pt: `await definirCorTema({
|
|
1187
|
+
statusBarColor: "#126fff",
|
|
1188
|
+
navigationBarColor: "#101827"
|
|
1189
|
+
});`,
|
|
1190
|
+
en: `await setThemeColor({
|
|
1191
|
+
statusBarColor: "#126fff",
|
|
1192
|
+
navigationBarColor: "#101827"
|
|
1193
|
+
});`
|
|
1194
|
+
}
|
|
1195
|
+
},
|
|
1196
|
+
{
|
|
1197
|
+
when: { pt: "Para navegar dentro do proprio WebView do APK.", en: "To navigate inside the APK WebView itself." },
|
|
1198
|
+
example: {
|
|
1199
|
+
pt: `await abrirNoApp("#/perfil");
|
|
1200
|
+
|
|
1201
|
+
// Sem deixar voltar para a tela anterior:
|
|
1202
|
+
await abrirNoApp("#/login", { substituir: true });`,
|
|
1203
|
+
en: `await openInApp("#/profile");
|
|
1204
|
+
|
|
1205
|
+
// Without keeping the previous screen:
|
|
1206
|
+
await openInApp("#/login", { replace: true });`
|
|
1207
|
+
}
|
|
1208
|
+
},
|
|
1209
|
+
{
|
|
1210
|
+
when: { pt: "Para abrir navegador, outro app ou link externo fora do APK.", en: "To open browser, another app or external link outside the APK." },
|
|
1211
|
+
example: {
|
|
1212
|
+
pt: `await abrirForaDoApp("https://exemplo.com");`,
|
|
1213
|
+
en: `await openOutsideApp("https://example.com");`
|
|
1214
|
+
}
|
|
1215
|
+
},
|
|
1216
|
+
{
|
|
1217
|
+
when: { pt: "Para abrir uma conversa do WhatsApp com numero e mensagem opcional.", en: "To open a WhatsApp conversation with phone number and optional message." },
|
|
1218
|
+
example: {
|
|
1219
|
+
pt: `await abrirWhatsapp(
|
|
1220
|
+
"559999999999",
|
|
1221
|
+
"Oi, vim pelo app"
|
|
1222
|
+
);`,
|
|
1223
|
+
en: `await openWhatsapp(
|
|
1224
|
+
"559999999999",
|
|
1225
|
+
"Hi, I came from the app"
|
|
1226
|
+
);`
|
|
1227
|
+
}
|
|
1228
|
+
},
|
|
1229
|
+
{
|
|
1230
|
+
when: { pt: "Para abrir o discador ou o app de mapas sem fazer a acao sozinho.", en: "To open dialer or maps without performing the action automatically." },
|
|
1231
|
+
example: {
|
|
1232
|
+
pt: `await discar("11999999999");
|
|
1233
|
+
await abrirMapa("Avenida Paulista, Sao Paulo");`,
|
|
1234
|
+
en: `await dial("11999999999");
|
|
1235
|
+
await openMap("Avenida Paulista, Sao Paulo");`
|
|
1236
|
+
}
|
|
1237
|
+
},
|
|
1238
|
+
{
|
|
1239
|
+
when: { pt: "Para diagnostico, suporte ou pequenas adaptacoes por versao do Android.", en: "For diagnostics, support or small adaptations by Android version." },
|
|
1240
|
+
example: {
|
|
1241
|
+
pt: `const aparelho = await infoDispositivo();
|
|
1242
|
+
|
|
1243
|
+
console.log(aparelho.modelo, aparelho.androidVersion);`,
|
|
1244
|
+
en: `const device = await deviceInfo();
|
|
1245
|
+
|
|
1246
|
+
console.log(device.model, device.androidVersion);`
|
|
1247
|
+
}
|
|
1248
|
+
},
|
|
1249
|
+
{
|
|
1250
|
+
when: { pt: "Para adaptar a UI quando ficar offline ou quando a bateria estiver baixa.", en: "To adapt the UI when offline or when battery is low." },
|
|
1251
|
+
example: {
|
|
1252
|
+
pt: `const rede = await infoRede();
|
|
1253
|
+
const bateria = await infoBateria();
|
|
1254
|
+
|
|
1255
|
+
if (!rede.online) {
|
|
1256
|
+
await toast("Sem internet");
|
|
1257
|
+
}`,
|
|
1258
|
+
en: `const network = await networkInfo();
|
|
1259
|
+
const battery = await batteryInfo();
|
|
1260
|
+
|
|
1261
|
+
if (!network.online) {
|
|
1262
|
+
await toast("No internet");
|
|
1263
|
+
}`
|
|
1264
|
+
}
|
|
1265
|
+
},
|
|
1266
|
+
{
|
|
1267
|
+
when: { pt: "Para saber se o app deve reduzir imagens, cache ou tarefas pesadas.", en: "To know whether the app should reduce images, cache or heavy tasks." },
|
|
1268
|
+
example: {
|
|
1269
|
+
pt: `const memoria = await infoMemoria();
|
|
1270
|
+
const mbLivre = memoria.availableBytes / 1024 / 1024;
|
|
1271
|
+
|
|
1272
|
+
if (memoria.lowMemory) {
|
|
1273
|
+
console.log("Reduza cache. MB livre:", mbLivre);
|
|
1274
|
+
}`,
|
|
1275
|
+
en: `const memory = await memoryInfo();
|
|
1276
|
+
const freeMb = memory.availableBytes / 1024 / 1024;
|
|
1277
|
+
|
|
1278
|
+
if (memory.lowMemory) {
|
|
1279
|
+
console.log("Reduce cache. Free MB:", freeMb);
|
|
1280
|
+
}`
|
|
1281
|
+
}
|
|
1282
|
+
},
|
|
1283
|
+
{
|
|
1284
|
+
when: { pt: "Para conferir espaco antes de baixar ou salvar arquivos grandes.", en: "To check space before downloading or saving large files." },
|
|
1285
|
+
example: {
|
|
1286
|
+
pt: `const armazenamento = await infoArmazenamento();
|
|
1287
|
+
const livre = armazenamento.internal.availableBytes;
|
|
1288
|
+
|
|
1289
|
+
console.log("Livre em MB:", Math.round(livre / 1024 / 1024));`,
|
|
1290
|
+
en: `const storage = await storageInfo();
|
|
1291
|
+
const free = storage.internal.availableBytes;
|
|
1292
|
+
|
|
1293
|
+
console.log("Free MB:", Math.round(free / 1024 / 1024));`
|
|
1294
|
+
}
|
|
1295
|
+
},
|
|
1296
|
+
{
|
|
1297
|
+
when: { pt: "Para uma tela de diagnostico com memoria, armazenamento, bateria e rede juntos.", en: "For a diagnostics screen with memory, storage, battery and network together." },
|
|
1298
|
+
example: {
|
|
1299
|
+
pt: `const desempenho = await infoDesempenho();
|
|
1300
|
+
|
|
1301
|
+
console.log(desempenho.memory);
|
|
1302
|
+
console.log(desempenho.storage);
|
|
1303
|
+
console.log(desempenho.battery);`,
|
|
1304
|
+
en: `const performance = await performanceInfo();
|
|
1305
|
+
|
|
1306
|
+
console.log(performance.memory);
|
|
1307
|
+
console.log(performance.storage);
|
|
1308
|
+
console.log(performance.battery);`
|
|
1309
|
+
}
|
|
1310
|
+
},
|
|
1311
|
+
{
|
|
1312
|
+
when: { pt: "Para diagnostico aproximado de processos que o Android deixa o APK enxergar.", en: "For approximate diagnostics of processes Android lets the APK see." },
|
|
1313
|
+
example: {
|
|
1314
|
+
pt: `const resultado = await appsAbertos();
|
|
1315
|
+
|
|
1316
|
+
for (const app of resultado.apps) {
|
|
1317
|
+
console.log(app.nome, app.ramMb + " MB");
|
|
1318
|
+
}`,
|
|
1319
|
+
en: `const result = await openAppsMemory();
|
|
1320
|
+
|
|
1321
|
+
for (const app of result.apps) {
|
|
1322
|
+
console.log(app.name, app.ramMb + " MB");
|
|
1323
|
+
}`
|
|
1324
|
+
}
|
|
1325
|
+
},
|
|
1326
|
+
{
|
|
1327
|
+
when: { pt: "Para apps em modo floating que precisam mostrar/esconder o icone flutuante.", en: "For floating-mode apps that need to show/hide the floating icon." },
|
|
1328
|
+
example: {
|
|
1329
|
+
pt: `const status = await iniciarIconeFlutuante();
|
|
1330
|
+
|
|
1331
|
+
if (status.requiresSettings) {
|
|
1332
|
+
console.log("O Android abriu a tela de sobreposicao");
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
// Para desligar:
|
|
1336
|
+
// await pararIconeFlutuante();`,
|
|
1337
|
+
en: `const status = await startFloatingIcon();
|
|
1338
|
+
|
|
1339
|
+
if (status.requiresSettings) {
|
|
1340
|
+
console.log("Android opened the draw-over-apps screen");
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
// To turn it off:
|
|
1344
|
+
// await stopFloatingIcon();`
|
|
1345
|
+
}
|
|
612
1346
|
}
|
|
613
1347
|
];
|
|
614
1348
|
|
|
@@ -624,6 +1358,7 @@ const state = {
|
|
|
624
1358
|
buildRunning: false,
|
|
625
1359
|
lastApkPath: null,
|
|
626
1360
|
lastDistPath: null,
|
|
1361
|
+
defaultIconPath: "",
|
|
627
1362
|
animationTimer: null,
|
|
628
1363
|
progress: 0,
|
|
629
1364
|
logsVisible: localStorage.getItem("html2apk.logsVisible") === "true"
|
|
@@ -659,6 +1394,7 @@ function collectElements() {
|
|
|
659
1394
|
"entryPath",
|
|
660
1395
|
"doctorButton",
|
|
661
1396
|
"buildButton",
|
|
1397
|
+
"usbDebugButton",
|
|
662
1398
|
"appNameInput",
|
|
663
1399
|
"packageIdInput",
|
|
664
1400
|
"versionInput",
|
|
@@ -691,6 +1427,8 @@ function collectElements() {
|
|
|
691
1427
|
"apkPath",
|
|
692
1428
|
"openDistButton",
|
|
693
1429
|
"showApkButton",
|
|
1430
|
+
"successTitle",
|
|
1431
|
+
"successText",
|
|
694
1432
|
"successApkPath",
|
|
695
1433
|
"successOpenDistButton",
|
|
696
1434
|
"successShowApkButton",
|
|
@@ -874,6 +1612,7 @@ function updateActionButtons() {
|
|
|
874
1612
|
|
|
875
1613
|
function setBuildButtons(enabled) {
|
|
876
1614
|
elements.buildButton.disabled = !enabled;
|
|
1615
|
+
elements.usbDebugButton.disabled = !enabled;
|
|
877
1616
|
}
|
|
878
1617
|
|
|
879
1618
|
function packageSegment(value) {
|
|
@@ -900,12 +1639,16 @@ function toFileUrl(filePath) {
|
|
|
900
1639
|
return `file:///${String(filePath).replace(/\\/g, "/").replace(/^\/+/, "")}`;
|
|
901
1640
|
}
|
|
902
1641
|
|
|
1642
|
+
function defaultIconPath() {
|
|
1643
|
+
return state.defaultIconPath || "";
|
|
1644
|
+
}
|
|
1645
|
+
|
|
903
1646
|
function isAbsolutePath(filePath) {
|
|
904
1647
|
return /^[a-zA-Z]:[\\/]/.test(String(filePath || "")) || String(filePath || "").startsWith("/");
|
|
905
1648
|
}
|
|
906
1649
|
|
|
907
1650
|
function iconPreviewPath(iconPath) {
|
|
908
|
-
if (!iconPath
|
|
1651
|
+
if (!iconPath) {
|
|
909
1652
|
return "../../../html2apk.png";
|
|
910
1653
|
}
|
|
911
1654
|
if (isAbsolutePath(iconPath)) {
|
|
@@ -914,6 +1657,18 @@ function iconPreviewPath(iconPath) {
|
|
|
914
1657
|
return toFileUrl(`${state.project.projectRoot}\\${iconPath}`);
|
|
915
1658
|
}
|
|
916
1659
|
|
|
1660
|
+
function displayIconValue(iconPath) {
|
|
1661
|
+
const value = String(iconPath || "").trim();
|
|
1662
|
+
if (!value || (state.defaultIconPath && value === state.defaultIconPath)) {
|
|
1663
|
+
return text("defaultIcon");
|
|
1664
|
+
}
|
|
1665
|
+
return value;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
function isConfigFilePath(filePath) {
|
|
1669
|
+
return /(^|[\\/])(app|config)\.json$/i.test(String(filePath || ""));
|
|
1670
|
+
}
|
|
1671
|
+
|
|
917
1672
|
function escapeHtml(value) {
|
|
918
1673
|
return String(value || "")
|
|
919
1674
|
.replace(/&/g, "&")
|
|
@@ -922,6 +1677,61 @@ function escapeHtml(value) {
|
|
|
922
1677
|
.replace(/"/g, """);
|
|
923
1678
|
}
|
|
924
1679
|
|
|
1680
|
+
function recipeForCode(index) {
|
|
1681
|
+
const language = currentLanguage();
|
|
1682
|
+
const recipe = nativeCodeRecipes[index] || {};
|
|
1683
|
+
return {
|
|
1684
|
+
when: recipe.when ? recipe.when[language] || recipe.when.pt : "",
|
|
1685
|
+
example: recipe.example ? recipe.example[language] || recipe.example.pt : ""
|
|
1686
|
+
};
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
async function copyToClipboard(value) {
|
|
1690
|
+
const textValue = String(value || "");
|
|
1691
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
1692
|
+
await navigator.clipboard.writeText(textValue);
|
|
1693
|
+
return;
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
const textarea = document.createElement("textarea");
|
|
1697
|
+
textarea.value = textValue;
|
|
1698
|
+
textarea.setAttribute("readonly", "");
|
|
1699
|
+
textarea.style.position = "fixed";
|
|
1700
|
+
textarea.style.left = "-9999px";
|
|
1701
|
+
document.body.appendChild(textarea);
|
|
1702
|
+
textarea.select();
|
|
1703
|
+
document.execCommand("copy");
|
|
1704
|
+
textarea.remove();
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
async function handleNativeCodeCopy(event) {
|
|
1708
|
+
const button = event.target.closest("[data-copy-code]");
|
|
1709
|
+
if (!button) {
|
|
1710
|
+
return;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
const index = Number.parseInt(button.dataset.copyCode, 10);
|
|
1714
|
+
const recipe = recipeForCode(index);
|
|
1715
|
+
if (!recipe.example) {
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
const originalText = button.textContent;
|
|
1720
|
+
try {
|
|
1721
|
+
await copyToClipboard(recipe.example);
|
|
1722
|
+
button.textContent = text("copiedCode");
|
|
1723
|
+
button.classList.add("copied");
|
|
1724
|
+
} catch (error) {
|
|
1725
|
+
button.textContent = text("copyFailed");
|
|
1726
|
+
button.classList.add("copy-error");
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
setTimeout(() => {
|
|
1730
|
+
button.textContent = originalText || text("copyCode");
|
|
1731
|
+
button.classList.remove("copied", "copy-error");
|
|
1732
|
+
}, 1400);
|
|
1733
|
+
}
|
|
1734
|
+
|
|
925
1735
|
function selectedOptionText(select) {
|
|
926
1736
|
if (!select || !select.selectedOptions || !select.selectedOptions.length) {
|
|
927
1737
|
return select ? select.value : "";
|
|
@@ -1023,21 +1833,37 @@ function renderNativeCodeGrid() {
|
|
|
1023
1833
|
}
|
|
1024
1834
|
|
|
1025
1835
|
const language = currentLanguage();
|
|
1836
|
+
const javaLabel = text("javaLabel");
|
|
1837
|
+
const doesLabel = text("doesLabel");
|
|
1838
|
+
const whenUseLabel = text("whenUseLabel");
|
|
1026
1839
|
const returnsLabel = text("returnsLabel");
|
|
1027
1840
|
const handlingLabel = text("handlingLabel");
|
|
1028
|
-
|
|
1841
|
+
const exampleLabel = text("exampleLabel");
|
|
1842
|
+
const copyCodeLabel = text("copyCode");
|
|
1843
|
+
elements.nativeCodeGrid.innerHTML = nativeCodeEntries.map((entry, index) => {
|
|
1029
1844
|
const syntax = entry.syntax ? entry.syntax[language] || entry.syntax.pt : entry.js;
|
|
1030
1845
|
const description = entry.description[language] || entry.description.pt;
|
|
1031
1846
|
const returns = entry.returns[language] || entry.returns.pt;
|
|
1032
1847
|
const handling = entry.handling[language] || entry.handling.pt;
|
|
1848
|
+
const recipe = recipeForCode(index);
|
|
1033
1849
|
|
|
1034
1850
|
return `
|
|
1035
1851
|
<article class="code-card">
|
|
1036
|
-
<code
|
|
1037
|
-
|
|
1038
|
-
|
|
1852
|
+
<div class="code-card-top">
|
|
1853
|
+
<code>${escapeHtml(syntax)}</code>
|
|
1854
|
+
<span>${escapeHtml(javaLabel)}: ${escapeHtml(entry.java)}</span>
|
|
1855
|
+
</div>
|
|
1856
|
+
<p><strong>${escapeHtml(doesLabel)}:</strong> ${escapeHtml(description)}</p>
|
|
1857
|
+
<small><strong>${escapeHtml(whenUseLabel)}:</strong> ${escapeHtml(recipe.when)}</small>
|
|
1039
1858
|
<small><strong>${escapeHtml(returnsLabel)}:</strong> ${escapeHtml(returns)}</small>
|
|
1040
1859
|
<small class="handling"><strong>${escapeHtml(handlingLabel)}:</strong> ${escapeHtml(handling)}</small>
|
|
1860
|
+
<div class="copy-example">
|
|
1861
|
+
<div class="copy-example-header">
|
|
1862
|
+
<strong>${escapeHtml(exampleLabel)}</strong>
|
|
1863
|
+
<button type="button" class="copy-code-button" data-copy-code="${index}">${escapeHtml(copyCodeLabel)}</button>
|
|
1864
|
+
</div>
|
|
1865
|
+
<pre><code>${escapeHtml(recipe.example)}</code></pre>
|
|
1866
|
+
</div>
|
|
1041
1867
|
</article>
|
|
1042
1868
|
`;
|
|
1043
1869
|
}).join("");
|
|
@@ -1058,8 +1884,9 @@ function populateSettings(config = {}, project = state.project) {
|
|
|
1058
1884
|
elements.themeColorTextInput.value = themeColor;
|
|
1059
1885
|
elements.oneSignalAppIdInput.value = oneSignalAppIdFromConfig(config);
|
|
1060
1886
|
renderPermissionOptions(Array.isArray(config.permissions) && config.permissions.length ? config.permissions : DEFAULT_PERMISSIONS);
|
|
1061
|
-
|
|
1062
|
-
elements.
|
|
1887
|
+
const iconPath = String(config.icon || "").trim() || defaultIconPath();
|
|
1888
|
+
elements.iconPathInput.value = iconPath;
|
|
1889
|
+
elements.iconPreview.src = iconPreviewPath(iconPath);
|
|
1063
1890
|
elements.debugInput.checked = Boolean(config.debug);
|
|
1064
1891
|
elements.releaseInput.checked = Boolean(config.release);
|
|
1065
1892
|
}
|
|
@@ -1093,9 +1920,7 @@ function validateSettings() {
|
|
|
1093
1920
|
if (!isValidOptionalOneSignalAppId(elements.oneSignalAppIdInput.value)) {
|
|
1094
1921
|
errors.push(text("invalidOneSignalAppId"));
|
|
1095
1922
|
}
|
|
1096
|
-
if (
|
|
1097
|
-
errors.push(text("missingIcon"));
|
|
1098
|
-
} else if (!/\.png$/i.test(elements.iconPathInput.value.trim())) {
|
|
1923
|
+
if (elements.iconPathInput.value.trim() && !/\.png$/i.test(elements.iconPathInput.value.trim())) {
|
|
1099
1924
|
errors.push(text("invalidIconType"));
|
|
1100
1925
|
}
|
|
1101
1926
|
|
|
@@ -1142,7 +1967,7 @@ function renderReview() {
|
|
|
1142
1967
|
[text("appThemeColor"), elements.themeColorTextInput.value.trim()],
|
|
1143
1968
|
[text("oneSignalAppId"), elements.oneSignalAppIdInput.value.trim() || "-"],
|
|
1144
1969
|
[text("androidPermissions"), selectedPermissions().join(", ")],
|
|
1145
|
-
[text("appIcon"), elements.iconPathInput.value.trim()]
|
|
1970
|
+
[text("appIcon"), displayIconValue(elements.iconPathInput.value.trim())]
|
|
1146
1971
|
];
|
|
1147
1972
|
|
|
1148
1973
|
elements.reviewGrid.innerHTML = items.map(([label, value]) => `
|
|
@@ -1305,22 +2130,67 @@ async function ensureEnvironmentBeforeSettings() {
|
|
|
1305
2130
|
return ready;
|
|
1306
2131
|
}
|
|
1307
2132
|
|
|
1308
|
-
|
|
2133
|
+
function renderProjectSnapshot(project) {
|
|
1309
2134
|
state.project = project;
|
|
1310
|
-
state.doctorOk = false;
|
|
1311
|
-
state.environmentOk = false;
|
|
1312
|
-
state.environmentChecking = false;
|
|
1313
|
-
state.environmentFailed = false;
|
|
1314
|
-
state.settingsValid = false;
|
|
1315
|
-
state.lastApkPath = null;
|
|
1316
2135
|
state.lastDistPath = project.distPath;
|
|
1317
|
-
|
|
1318
2136
|
elements.projectSummary.classList.remove("hidden");
|
|
1319
2137
|
elements.projectName.textContent = project.name;
|
|
1320
2138
|
elements.projectPath.textContent = project.projectRoot;
|
|
1321
2139
|
elements.configStatus.textContent = project.hasAppJson || project.hasConfigJson ? text("detected") : text("notDetected");
|
|
1322
2140
|
elements.entryStatus.textContent = project.hasEntryFile ? text("detected") : text("missing");
|
|
1323
2141
|
elements.entryPath.textContent = project.entryPath;
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
async function watchCurrentProject() {
|
|
2145
|
+
if (!state.project || !api.watchProject) {
|
|
2146
|
+
return;
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
try {
|
|
2150
|
+
const result = await api.watchProject(state.project.projectRoot);
|
|
2151
|
+
if (result && !result.ok) {
|
|
2152
|
+
appendLog(`${text("projectWatcherFail")}: ${result.message}`, "error");
|
|
2153
|
+
}
|
|
2154
|
+
} catch (error) {
|
|
2155
|
+
appendLog(`${text("projectWatcherFail")}: ${error.message}`, "error");
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
function applyProjectChange(payload) {
|
|
2160
|
+
if (!payload || !payload.project || !state.project) {
|
|
2161
|
+
return;
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
if (payload.project.projectRoot !== state.project.projectRoot) {
|
|
2165
|
+
return;
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
renderProjectSnapshot(payload.project);
|
|
2169
|
+
|
|
2170
|
+
const reloadSettings = isConfigFilePath(payload.changedPath);
|
|
2171
|
+
if (reloadSettings && !state.buildRunning) {
|
|
2172
|
+
populateSettings(payload.project.config || {}, payload.project);
|
|
2173
|
+
appendLog(text("projectConfigReloaded"), "system");
|
|
2174
|
+
} else {
|
|
2175
|
+
appendLog(`${text("projectAutoUpdated")}: ${payload.changedPath || payload.project.projectRoot}`, "system");
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
setStep("folder", payload.project.hasEntryFile ? "done" : "active", payload.project.hasEntryFile ? text("folderReady") : text("missing"));
|
|
2179
|
+
validateSettings();
|
|
2180
|
+
if (document.querySelector(".nav-item.active")?.dataset.view === "build") {
|
|
2181
|
+
renderReview();
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
async function summarizeProject(project) {
|
|
2186
|
+
renderProjectSnapshot(project);
|
|
2187
|
+
state.doctorOk = false;
|
|
2188
|
+
state.environmentOk = false;
|
|
2189
|
+
state.environmentChecking = false;
|
|
2190
|
+
state.environmentFailed = false;
|
|
2191
|
+
state.settingsValid = false;
|
|
2192
|
+
state.lastApkPath = null;
|
|
2193
|
+
|
|
1324
2194
|
elements.nextSettingsButton.disabled = false;
|
|
1325
2195
|
elements.doctorButton.disabled = true;
|
|
1326
2196
|
setBuildButtons(false);
|
|
@@ -1334,6 +2204,7 @@ async function summarizeProject(project) {
|
|
|
1334
2204
|
appendLog(`${text("droppedFolder")}: ${project.projectRoot}`, "system");
|
|
1335
2205
|
validateSettings();
|
|
1336
2206
|
setProgress(project.hasEntryFile ? 25 : 15, project.hasEntryFile ? text("progressFolder") : text("missing"), project.hasEntryFile ? "" : "error");
|
|
2207
|
+
await watchCurrentProject();
|
|
1337
2208
|
await ensureEnvironmentBeforeSettings();
|
|
1338
2209
|
}
|
|
1339
2210
|
|
|
@@ -1451,6 +2322,8 @@ async function runBuildFlow() {
|
|
|
1451
2322
|
state.lastApkPath = result.apkPath;
|
|
1452
2323
|
state.lastDistPath = state.project.distPath;
|
|
1453
2324
|
elements.apkPath.textContent = result.apkPath;
|
|
2325
|
+
elements.successTitle.textContent = text("successTitle");
|
|
2326
|
+
elements.successText.textContent = text("successText");
|
|
1454
2327
|
elements.successApkPath.textContent = result.apkPath;
|
|
1455
2328
|
elements.resultPanel.classList.remove("hidden");
|
|
1456
2329
|
setStep("build", "done", text("buildOk"));
|
|
@@ -1470,6 +2343,74 @@ async function runBuildFlow() {
|
|
|
1470
2343
|
}
|
|
1471
2344
|
}
|
|
1472
2345
|
|
|
2346
|
+
async function runUsbDebugFlow() {
|
|
2347
|
+
if (!state.project || state.buildRunning) {
|
|
2348
|
+
setStatus("error", state.project ? text("usbDebugRunning") : text("chooseProjectFirst"));
|
|
2349
|
+
return;
|
|
2350
|
+
}
|
|
2351
|
+
if (!goToReview()) {
|
|
2352
|
+
return;
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
showLogBar();
|
|
2356
|
+
state.buildRunning = true;
|
|
2357
|
+
updateActionButtons();
|
|
2358
|
+
elements.resultPanel.classList.add("hidden");
|
|
2359
|
+
setView("build");
|
|
2360
|
+
setStep("folder", "done", text("folderReady"));
|
|
2361
|
+
|
|
2362
|
+
const doctorOk = await runDoctorOnly();
|
|
2363
|
+
if (!doctorOk) {
|
|
2364
|
+
state.buildRunning = false;
|
|
2365
|
+
updateActionButtons();
|
|
2366
|
+
return;
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
setStatus("busy", text("usbDebugRunning"));
|
|
2370
|
+
setStep("build", "active", text("usbDebugRunning"));
|
|
2371
|
+
setProgress(90, text("progressBuild"), "active");
|
|
2372
|
+
startAnimatedLogs();
|
|
2373
|
+
|
|
2374
|
+
try {
|
|
2375
|
+
const response = await api.runUsbDebugBuild(buildOptions());
|
|
2376
|
+
stopAnimatedLogs();
|
|
2377
|
+
if (!response.ok) {
|
|
2378
|
+
setStep("build", "error", text("usbDebugFail"));
|
|
2379
|
+
setStatus("error", text("usbDebugFail"));
|
|
2380
|
+
setProgress(90, text("progressError"), "error");
|
|
2381
|
+
appendLog(response.message || text("usbDebugFail"), "error");
|
|
2382
|
+
if (response.buildDir) {
|
|
2383
|
+
appendLog(`Build directory kept: ${response.buildDir}`, "system");
|
|
2384
|
+
}
|
|
2385
|
+
return;
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
const result = response.result;
|
|
2389
|
+
state.lastApkPath = result.apkPath;
|
|
2390
|
+
state.lastDistPath = state.project.distPath;
|
|
2391
|
+
elements.apkPath.textContent = result.apkPath;
|
|
2392
|
+
elements.successTitle.textContent = text("usbSuccessTitle");
|
|
2393
|
+
elements.successText.textContent = text("usbSuccessText");
|
|
2394
|
+
elements.successApkPath.textContent = result.apkPath;
|
|
2395
|
+
elements.resultPanel.classList.remove("hidden");
|
|
2396
|
+
setStep("build", "done", text("usbDebugOk"));
|
|
2397
|
+
setStatus("ready", text("usbDebugOk"));
|
|
2398
|
+
setProgress(100, text("progressDone"));
|
|
2399
|
+
appendLog(`${text("usbDebugOk")}: ${result.device && result.device.id ? result.device.id : "Android USB"}`, "success");
|
|
2400
|
+
appendLog(`${text("buildOk")}: ${result.apkPath}`, "success");
|
|
2401
|
+
setView("success");
|
|
2402
|
+
} catch (error) {
|
|
2403
|
+
stopAnimatedLogs();
|
|
2404
|
+
setStep("build", "error", text("usbDebugFail"));
|
|
2405
|
+
setStatus("error", error.message);
|
|
2406
|
+
setProgress(90, text("progressError"), "error");
|
|
2407
|
+
appendLog(error.message, "error");
|
|
2408
|
+
} finally {
|
|
2409
|
+
state.buildRunning = false;
|
|
2410
|
+
updateActionButtons();
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
|
|
1473
2414
|
function toggleTheme() {
|
|
1474
2415
|
state.theme = state.theme === "dark" ? "light" : "dark";
|
|
1475
2416
|
applyTheme();
|
|
@@ -1519,6 +2460,7 @@ function bindEvents() {
|
|
|
1519
2460
|
elements.settingsNextButton.addEventListener("click", goToReview);
|
|
1520
2461
|
elements.doctorButton.addEventListener("click", runDoctorOnly);
|
|
1521
2462
|
elements.buildButton.addEventListener("click", runBuildFlow);
|
|
2463
|
+
elements.usbDebugButton.addEventListener("click", runUsbDebugFlow);
|
|
1522
2464
|
elements.clearLogsButton.addEventListener("click", clearLogs);
|
|
1523
2465
|
elements.toggleLogsButton.addEventListener("click", toggleLogBar);
|
|
1524
2466
|
elements.bottomToggleLogsButton.addEventListener("click", toggleLogBar);
|
|
@@ -1553,6 +2495,7 @@ function bindEvents() {
|
|
|
1553
2495
|
validateSettings();
|
|
1554
2496
|
});
|
|
1555
2497
|
elements.permissionGrid.addEventListener("change", validateSettings);
|
|
2498
|
+
elements.nativeCodeGrid.addEventListener("click", handleNativeCodeCopy);
|
|
1556
2499
|
[
|
|
1557
2500
|
elements.appNameInput,
|
|
1558
2501
|
elements.packageIdInput,
|
|
@@ -1629,6 +2572,16 @@ function bindEvents() {
|
|
|
1629
2572
|
api.onInstallLog((payload) => {
|
|
1630
2573
|
appendLog(payload.line, payload.kind || "raw");
|
|
1631
2574
|
});
|
|
2575
|
+
|
|
2576
|
+
if (api.onProjectChanged) {
|
|
2577
|
+
api.onProjectChanged(applyProjectChange);
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
if (api.onProjectWatchError) {
|
|
2581
|
+
api.onProjectWatchError((payload) => {
|
|
2582
|
+
appendLog(`${text("projectWatcherFail")}: ${payload.message}`, "error");
|
|
2583
|
+
});
|
|
2584
|
+
}
|
|
1632
2585
|
}
|
|
1633
2586
|
|
|
1634
2587
|
async function init() {
|
|
@@ -1645,6 +2598,11 @@ async function init() {
|
|
|
1645
2598
|
try {
|
|
1646
2599
|
const info = await api.appInfo();
|
|
1647
2600
|
elements.appVersion.textContent = `v${info.version}`;
|
|
2601
|
+
state.defaultIconPath = info.iconPath || "";
|
|
2602
|
+
if (elements.iconPathInput && !elements.iconPathInput.value.trim() && state.defaultIconPath) {
|
|
2603
|
+
elements.iconPathInput.value = state.defaultIconPath;
|
|
2604
|
+
elements.iconPreview.src = iconPreviewPath(state.defaultIconPath);
|
|
2605
|
+
}
|
|
1648
2606
|
} catch {
|
|
1649
2607
|
elements.appVersion.textContent = "v0.1.0";
|
|
1650
2608
|
}
|