html2apk 0.2.0 → 0.4.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 +323 -12
- 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/cordova/project.js +19 -1
- package/src/core/build-apk.js +399 -23
- package/src/core/config.js +47 -1
- package/src/core/defaults.js +6 -0
- package/src/desktop/main.js +152 -2
- package/src/desktop/preload.js +14 -0
- package/src/desktop/renderer/index.html +20 -2
- package/src/desktop/renderer/renderer.js +1254 -41
- package/src/desktop/renderer/styles.css +98 -2
- package/src/templates/cordova-plugin-html2apk-bridge/plugin.xml +4 -0
- package/src/templates/cordova-plugin-html2apk-bridge/src/android/Html2ApkBridge.java +1770 -38
- 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 +516 -3
- package/src/templates/html2apk-auto-theme.js +144 -0
- package/src/templates/html2apk-onesignal.js +155 -0
- package/src/utils/command-runner.js +3 -0
|
@@ -43,9 +43,14 @@ const i18n = {
|
|
|
43
43
|
orientationLandscape: "Horizontal",
|
|
44
44
|
minSdkVersion: "Android minimo",
|
|
45
45
|
appIcon: "Icone do app",
|
|
46
|
-
appThemeColor: "Cor do tema
|
|
46
|
+
appThemeColor: "Cor do tema/fallback",
|
|
47
|
+
themeMode: "Tema do APK",
|
|
48
|
+
themeModeFixed: "Cor fixa",
|
|
49
|
+
themeModeAuto: "Automatico pela tela",
|
|
50
|
+
oneSignalAppId: "OneSignal App ID",
|
|
51
|
+
oneSignalAppIdHint: "Opcional. Use o App ID do OneSignal, nao a REST API Key.",
|
|
47
52
|
androidPermissions: "Permissoes Android",
|
|
48
|
-
chooseIcon: "
|
|
53
|
+
chooseIcon: "Trocar icone PNG",
|
|
49
54
|
reviewBuild: "Revisar build",
|
|
50
55
|
debugBuild: "Debug tecnico",
|
|
51
56
|
debugBuildText: "Mantem a pasta Cordova temporaria para inspecao.",
|
|
@@ -60,6 +65,7 @@ const i18n = {
|
|
|
60
65
|
buildEyebrow: "Revisao",
|
|
61
66
|
buildTitle: "Confirme as informacoes e gere o APK",
|
|
62
67
|
startBuild: "Gerar APK",
|
|
68
|
+
startUsbDebug: "Testar no USB",
|
|
63
69
|
stepFolder: "Pasta recebida",
|
|
64
70
|
stepSettings: "Configuracoes completas",
|
|
65
71
|
stepDoctor: "Ambiente verificado",
|
|
@@ -71,7 +77,16 @@ const i18n = {
|
|
|
71
77
|
logsTitle: "Logs do processo",
|
|
72
78
|
codesEyebrow: "Bridge nativa",
|
|
73
79
|
codesTitle: "Codigos interpretados",
|
|
74
|
-
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",
|
|
84
|
+
returnsLabel: "Retorno",
|
|
85
|
+
handlingLabel: "Como tratar",
|
|
86
|
+
exampleLabel: "Exemplo copiavel",
|
|
87
|
+
copyCode: "Copiar",
|
|
88
|
+
copiedCode: "Copiado",
|
|
89
|
+
copyFailed: "Nao foi possivel copiar",
|
|
75
90
|
clearLogs: "Limpar logs",
|
|
76
91
|
helpEyebrow: "Sem misterio",
|
|
77
92
|
helpTitle: "Doctor, build e dependencias",
|
|
@@ -79,6 +94,9 @@ const i18n = {
|
|
|
79
94
|
helpBuild: "Cria um projeto Cordova temporario, copia seu app web e gera o arquivo APK em dist.",
|
|
80
95
|
helpDepsTitle: "Dependencias no EXE",
|
|
81
96
|
helpDeps: "O executavel empacota html2apk, a interface e dependencias Node/Cordova. Se faltarem pacotes Android, ele pede permissao e tenta instalar; JDK/Gradle podem exigir instalacao do sistema.",
|
|
97
|
+
helpCreatorTitle: "Criador",
|
|
98
|
+
helpCreatorText: "Conheca o Dev Caio Multiversando, criador desta linda aplicacao html2apk.",
|
|
99
|
+
openInstagram: "Conhecer o dev",
|
|
82
100
|
ready: "Pronto",
|
|
83
101
|
selected: "Selecionado",
|
|
84
102
|
missing: "Nao encontrado",
|
|
@@ -92,9 +110,15 @@ const i18n = {
|
|
|
92
110
|
buildRunning: "Gerando APK",
|
|
93
111
|
buildOk: "APK gerado com sucesso",
|
|
94
112
|
buildFail: "Build falhou",
|
|
113
|
+
usbDebugRunning: "Gerando e instalando no celular USB",
|
|
114
|
+
usbDebugOk: "APK instalado no celular USB",
|
|
115
|
+
usbDebugFail: "Teste USB falhou",
|
|
95
116
|
droppedFolder: "Pasta recebida",
|
|
96
117
|
noFolderDrop: "Solte uma pasta do projeto",
|
|
97
118
|
projectLoaded: "Projeto carregado",
|
|
119
|
+
projectAutoUpdated: "Projeto atualizado automaticamente",
|
|
120
|
+
projectConfigReloaded: "Configuracoes recarregadas do projeto",
|
|
121
|
+
projectWatcherFail: "Nao foi possivel acompanhar alteracoes da pasta",
|
|
98
122
|
logsCleared: "Logs limpos",
|
|
99
123
|
settingsOk: "Configuracoes completas",
|
|
100
124
|
settingsMissing: "Complete as configuracoes obrigatorias",
|
|
@@ -104,9 +128,10 @@ const i18n = {
|
|
|
104
128
|
invalidPackageId: "Informe um Package ID valido, exemplo: com.seuapp.meuapp.",
|
|
105
129
|
invalidVersion: "Informe a versao no formato 1.0.0.",
|
|
106
130
|
missingMode: "Escolha o modo do app.",
|
|
107
|
-
|
|
131
|
+
defaultIcon: "Icone padrao do html2apk",
|
|
108
132
|
invalidIconType: "Use um icone PNG para evitar falhas no Android.",
|
|
109
133
|
invalidThemeColor: "Use uma cor hexadecimal valida, exemplo: #126fff.",
|
|
134
|
+
invalidOneSignalAppId: "Use um OneSignal App ID valido ou deixe vazio.",
|
|
110
135
|
invalidMinSdkVersion: "Escolha uma versao minima do Android entre API 24 e API 36.",
|
|
111
136
|
iconSelected: "Icone selecionado",
|
|
112
137
|
progressLabel: "Progresso",
|
|
@@ -130,6 +155,8 @@ const i18n = {
|
|
|
130
155
|
successEyebrow: "Concluido",
|
|
131
156
|
successTitle: "APK gerado com sucesso",
|
|
132
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.",
|
|
133
160
|
newBuild: "Novo build"
|
|
134
161
|
},
|
|
135
162
|
en: {
|
|
@@ -172,9 +199,14 @@ const i18n = {
|
|
|
172
199
|
orientationLandscape: "Landscape",
|
|
173
200
|
minSdkVersion: "Minimum Android",
|
|
174
201
|
appIcon: "App icon",
|
|
175
|
-
appThemeColor: "
|
|
202
|
+
appThemeColor: "Theme/fallback color",
|
|
203
|
+
themeMode: "APK theme",
|
|
204
|
+
themeModeFixed: "Fixed color",
|
|
205
|
+
themeModeAuto: "Auto from screen",
|
|
206
|
+
oneSignalAppId: "OneSignal App ID",
|
|
207
|
+
oneSignalAppIdHint: "Optional. Use the OneSignal App ID, not the REST API Key.",
|
|
176
208
|
androidPermissions: "Android permissions",
|
|
177
|
-
chooseIcon: "
|
|
209
|
+
chooseIcon: "Change PNG icon",
|
|
178
210
|
reviewBuild: "Review build",
|
|
179
211
|
debugBuild: "Technical debug",
|
|
180
212
|
debugBuildText: "Keeps the temporary Cordova folder for inspection.",
|
|
@@ -189,6 +221,7 @@ const i18n = {
|
|
|
189
221
|
buildEyebrow: "Review",
|
|
190
222
|
buildTitle: "Confirm the information and build the APK",
|
|
191
223
|
startBuild: "Build APK",
|
|
224
|
+
startUsbDebug: "Test on USB",
|
|
192
225
|
stepFolder: "Folder received",
|
|
193
226
|
stepSettings: "Settings complete",
|
|
194
227
|
stepDoctor: "Environment checked",
|
|
@@ -200,7 +233,16 @@ const i18n = {
|
|
|
200
233
|
logsTitle: "Process logs",
|
|
201
234
|
codesEyebrow: "Native bridge",
|
|
202
235
|
codesTitle: "Interpreted code",
|
|
203
|
-
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",
|
|
240
|
+
returnsLabel: "Returns",
|
|
241
|
+
handlingLabel: "How to handle",
|
|
242
|
+
exampleLabel: "Copyable example",
|
|
243
|
+
copyCode: "Copy",
|
|
244
|
+
copiedCode: "Copied",
|
|
245
|
+
copyFailed: "Could not copy",
|
|
204
246
|
clearLogs: "Clear logs",
|
|
205
247
|
helpEyebrow: "Plain and simple",
|
|
206
248
|
helpTitle: "Doctor, build and dependencies",
|
|
@@ -208,6 +250,9 @@ const i18n = {
|
|
|
208
250
|
helpBuild: "Creates a temporary Cordova project, copies your web app and generates the APK in dist.",
|
|
209
251
|
helpDepsTitle: "Dependencies in the EXE",
|
|
210
252
|
helpDeps: "The executable bundles html2apk, the interface and Node/Cordova dependencies. If Android packages are missing, it asks permission and tries to install them; JDK/Gradle may still require system installation.",
|
|
253
|
+
helpCreatorTitle: "Creator",
|
|
254
|
+
helpCreatorText: "Meet Dev Caio Multiversando, creator of this lovely html2apk app.",
|
|
255
|
+
openInstagram: "Meet the dev",
|
|
211
256
|
ready: "Ready",
|
|
212
257
|
selected: "Selected",
|
|
213
258
|
missing: "Missing",
|
|
@@ -221,9 +266,15 @@ const i18n = {
|
|
|
221
266
|
buildRunning: "Building APK",
|
|
222
267
|
buildOk: "APK generated successfully",
|
|
223
268
|
buildFail: "Build failed",
|
|
269
|
+
usbDebugRunning: "Building and installing on USB phone",
|
|
270
|
+
usbDebugOk: "APK installed on USB phone",
|
|
271
|
+
usbDebugFail: "USB test failed",
|
|
224
272
|
droppedFolder: "Folder received",
|
|
225
273
|
noFolderDrop: "Drop a project folder",
|
|
226
274
|
projectLoaded: "Project loaded",
|
|
275
|
+
projectAutoUpdated: "Project updated automatically",
|
|
276
|
+
projectConfigReloaded: "Settings reloaded from project",
|
|
277
|
+
projectWatcherFail: "Could not watch folder changes",
|
|
227
278
|
logsCleared: "Logs cleared",
|
|
228
279
|
settingsOk: "Settings complete",
|
|
229
280
|
settingsMissing: "Complete the required settings",
|
|
@@ -233,9 +284,10 @@ const i18n = {
|
|
|
233
284
|
invalidPackageId: "Enter a valid Package ID, example: com.yourapp.name.",
|
|
234
285
|
invalidVersion: "Enter the version as 1.0.0.",
|
|
235
286
|
missingMode: "Choose the app mode.",
|
|
236
|
-
|
|
287
|
+
defaultIcon: "Default html2apk icon",
|
|
237
288
|
invalidIconType: "Use a PNG icon to avoid Android build failures.",
|
|
238
289
|
invalidThemeColor: "Use a valid hex color, example: #126fff.",
|
|
290
|
+
invalidOneSignalAppId: "Use a valid OneSignal App ID or leave it empty.",
|
|
239
291
|
invalidMinSdkVersion: "Choose a minimum Android version between API 24 and API 36.",
|
|
240
292
|
iconSelected: "Icon selected",
|
|
241
293
|
progressLabel: "Progress",
|
|
@@ -259,6 +311,8 @@ const i18n = {
|
|
|
259
311
|
successEyebrow: "Complete",
|
|
260
312
|
successTitle: "APK generated successfully",
|
|
261
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.",
|
|
262
316
|
newBuild: "New build"
|
|
263
317
|
}
|
|
264
318
|
};
|
|
@@ -276,6 +330,7 @@ const animatedBuildLines = [
|
|
|
276
330
|
const DEFAULT_PERMISSIONS = ["INTERNET", "POST_NOTIFICATIONS", "VIBRATE"];
|
|
277
331
|
const DEFAULT_MIN_SDK_VERSION = 24;
|
|
278
332
|
const MIN_SDK_OPTIONS = [24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36];
|
|
333
|
+
const DEVELOPER_INSTAGRAM_URL = "https://www.instagram.com/caiomutiversando?igsh=MWFpcmYzZDB3YTNzZQ==";
|
|
279
334
|
|
|
280
335
|
const permissionOptions = [
|
|
281
336
|
{
|
|
@@ -321,19 +376,889 @@ const permissionOptions = [
|
|
|
321
376
|
];
|
|
322
377
|
|
|
323
378
|
const nativeCodeEntries = [
|
|
324
|
-
{
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
{
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
379
|
+
{
|
|
380
|
+
syntax: { pt: "toast('Mensagem')", en: "toast('Message')" },
|
|
381
|
+
java: "toast",
|
|
382
|
+
description: { pt: "Mostra uma mensagem rapida nativa.", en: "Shows a native short message." },
|
|
383
|
+
returns: { pt: "Promise<void>.", en: "Promise<void>." },
|
|
384
|
+
handling: { pt: "Use com `await` quando a proxima acao depender do feedback; coloque em `try/catch` se quiser mostrar erro ao usuario.", en: "Use `await` when the next action depends on the feedback; wrap in `try/catch` if you want to show an error to the user." }
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
syntax: { pt: "vibrar(250)", en: "vibrate(250)" },
|
|
388
|
+
java: "vibrate",
|
|
389
|
+
description: { pt: "Aciona a vibracao do aparelho por milissegundos.", en: "Vibrates the device for milliseconds." },
|
|
390
|
+
returns: { pt: "Promise<void>.", en: "Promise<void>." },
|
|
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." }
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
syntax: { pt: "notificar({ titulo, texto, acoes })", en: "notify({ title, text, actions })" },
|
|
395
|
+
java: "notify",
|
|
396
|
+
description: { pt: "Cria notificacao Android imediata. `acoes` ou `actions` viram botoes clicaveis.", en: "Creates an immediate Android notification. `actions` or `acoes` become clickable buttons." },
|
|
397
|
+
returns: { pt: "Promise<void>; cliques chegam em `aoEvento('notificacao:clicada')` ou `aoClicarNotificacao()`.", en: "Promise<void>; clicks arrive through `onEvent('notification:clicked')` or `onNotificationClick()`." },
|
|
398
|
+
handling: { pt: "Ao chamar, o html2apk pede permissao automaticamente no Android 13+. Se o Android nao puder mostrar o pop-up, ele abre as configuracoes e retorna `settingsOpened`.", en: "When called, html2apk automatically requests permission on Android 13+. If Android cannot show the prompt, it opens settings and returns `settingsOpened`." }
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
syntax: { pt: "agendarNotificacao({ titulo, texto, quando }) / agendarNotificacoes([...])", en: "scheduleNotification({ title, text, when }) / scheduleNotifications([...])" },
|
|
402
|
+
java: "scheduleNotification",
|
|
403
|
+
description: { pt: "Agenda uma notificacao ou uma lista de notificacoes mesmo com o app fechado.", en: "Schedules one notification or a list of notifications even when the app is closed." },
|
|
404
|
+
returns: { pt: "Uma notificacao retorna { id, when }; varias retornam Promise<Array> com esses objetos.", en: "One notification returns { id, when }; multiple notifications return Promise<Array> with those objects." },
|
|
405
|
+
handling: { pt: "`quando`/`when` deve ser timestamp em ms. Para varias, passe array; cada item ganha `id` automatico se voce nao informar. Se precisar de horario exato, confira `podeAgendarNotificacaoExata()`.", en: "`when`/`quando` must be a millisecond timestamp. For multiple, pass an array; each item gets an automatic `id` if you do not provide one. For exact timing, check `canScheduleExactAlarms()`." }
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
syntax: { pt: "agendarLoopNotificacoes({ aCada: '12h', notificacoes: [...] })", en: "scheduleNotificationLoop({ every: '12h', notifications: [...] })" },
|
|
409
|
+
java: "AlarmManager + NotificationStore",
|
|
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." },
|
|
411
|
+
returns: { pt: "{ id, when, repeating, loop }. Guarde o `id` para cancelar depois.", en: "{ id, when, repeating, loop }. Store the `id` to cancel later." },
|
|
412
|
+
handling: { pt: "Use `aoClicarNotificacao()` ou `aoEvento('notificacao:clicada')` para chamar funcoes conforme `aoClicar`. Cancele com `cancelarNotificacao(id)`.", en: "Use `onNotificationClick()` or `onEvent('notification:clicked')` to call functions from `onClick`. Cancel with `cancelNotification(id)`." }
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
syntax: { pt: "solicitarPermissaoNotificacoes()", en: "requestNotificationPermission()" },
|
|
416
|
+
java: "requestNotificationPermission",
|
|
417
|
+
description: { pt: "Pede permissao para mostrar notificacoes quando o Android exigir.", en: "Requests permission to show notifications when Android requires it." },
|
|
418
|
+
returns: { pt: "{ permission, required, granted, requiresSettings, settingsOpened }.", en: "{ permission, required, granted, requiresSettings, settingsOpened }." },
|
|
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." }
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
syntax: { pt: "solicitarPermissaoPush() / aoClicarPush(fn)", en: "requestPushPermission() / onPushClick(fn)" },
|
|
423
|
+
java: "onesignal-cordova-plugin",
|
|
424
|
+
description: { pt: "Ativa push remoto via OneSignal quando o OneSignal App ID foi preenchido nas configuracoes.", en: "Enables remote push through OneSignal when the OneSignal App ID was filled in settings." },
|
|
425
|
+
returns: { pt: "`solicitarPermissaoPush` retorna boolean. `aoClicarPush` retorna funcao para parar de escutar.", en: "`requestPushPermission` returns boolean. `onPushClick` returns an unsubscribe function." },
|
|
426
|
+
handling: { pt: "Use esse caminho para notificacoes enviadas pelo painel/API do OneSignal. Nao coloque REST API Key dentro do APK; use `identificarUsuarioPush(id)` e `adicionarTagPush(chave, valor)` para segmentar usuarios.", en: "Use this path for notifications sent from the OneSignal dashboard/API. Do not put a REST API Key inside the APK; use `loginPushUser(id)` and `addPushTag(key, value)` to target users." }
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
syntax: { pt: "statusPermissoes(['CAMERA'])", en: "permissionStatus(['CAMERA'])" },
|
|
430
|
+
java: "permissionStatus",
|
|
431
|
+
description: { pt: "Consulta se permissoes Android estao liberadas.", en: "Checks whether Android permissions are granted." },
|
|
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." },
|
|
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." }
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
syntax: { pt: "aoEvento('app:background', fn)", en: "onEvent('app:background', fn)" },
|
|
437
|
+
java: "dispatchEvent",
|
|
438
|
+
description: { pt: "Escuta eventos nativos: app:pronto, app:background, app:voltou, botao:voltar, link:aberto, rede:mudou, bateria:mudou, notificacao:recebida e notificacao:clicada.", en: "Listens to native events: app:ready, app:background, app:resumed, back:button, link:opened, network:changed, battery:changed, notification:received and notification:clicked." },
|
|
439
|
+
returns: { pt: "Funcao para cancelar a escuta.", en: "Unsubscribe function." },
|
|
440
|
+
handling: { pt: "Guarde o retorno em `parar` e chame quando a tela/componente for desmontado para evitar escutas duplicadas.", en: "Store the return value as `stop` and call it when the screen/component unmounts to avoid duplicate listeners." }
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
syntax: { pt: "aoMinimizar(fn) / aoVoltarParaApp(fn)", en: "onMinimize(fn) / onAppResume(fn)" },
|
|
444
|
+
java: "lifecycle",
|
|
445
|
+
description: { pt: "Atalhos para saber quando o app saiu da frente ou voltou.", en: "Shortcuts for knowing when the app left the foreground or resumed." },
|
|
446
|
+
returns: { pt: "Funcao para cancelar a escuta.", en: "Unsubscribe function." },
|
|
447
|
+
handling: { pt: "Pause timers, audio ou leitura pesada ao minimizar; ao voltar, confira se dados precisam ser recarregados.", en: "Pause timers, audio or heavy reads on minimize; on resume, check whether data should be refreshed." }
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
syntax: { pt: "obterLinkInicial() / aoAbrirLink(fn)", en: "getInitialLink() / onOpenLink(fn)" },
|
|
451
|
+
java: "getInitialLink",
|
|
452
|
+
description: { pt: "Lida com deep links/app links que abriram o APK.", en: "Handles deep links/app links that opened the APK." },
|
|
453
|
+
returns: { pt: "{ url, scheme, host, path, query } ou null; o listener recebe o mesmo objeto.", en: "{ url, scheme, host, path, query } or null; the listener receives the same object." },
|
|
454
|
+
handling: { pt: "No inicio do app, leia `obterLinkInicial()` e tambem registre `aoAbrirLink()` para links recebidos com o app ja aberto.", en: "At app startup, read `getInitialLink()` and also register `onOpenLink()` for links received while the app is already open." }
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
syntax: { pt: "solicitarPermissaoCamera()", en: "requestCameraPermission()" },
|
|
458
|
+
java: "requestCameraPermission",
|
|
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`." }
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
syntax: { pt: "await ouvirMic(); const audio = await pararMic()", en: "await listenMic(); const audio = await stopMic()" },
|
|
465
|
+
java: "MediaRecorder",
|
|
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." },
|
|
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." }
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
syntax: { pt: "lanterna(true) / statusLanterna()", en: "flashlight(true) / flashlightStatus()" },
|
|
472
|
+
java: "flashlight",
|
|
473
|
+
description: { pt: "Liga, desliga e consulta a lanterna do aparelho.", en: "Turns the device flashlight on/off and reads its status." },
|
|
474
|
+
returns: { pt: "{ available, enabled, permissionGranted }.", en: "{ available, enabled, permissionGranted }." },
|
|
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." }
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
syntax: { pt: "alternarLanterna()", en: "toggleFlashlight()" },
|
|
479
|
+
java: "toggleFlashlight",
|
|
480
|
+
description: { pt: "Inverte o estado atual da lanterna.", en: "Toggles the current flashlight state." },
|
|
481
|
+
returns: { pt: "{ available, enabled, permissionGranted }.", en: "{ available, enabled, permissionGranted }." },
|
|
482
|
+
handling: { pt: "Atualize o botao usando `enabled` retornado, sem assumir que o toggle sempre conseguiu mudar o estado.", en: "Update the button from the returned `enabled` value instead of assuming the toggle always succeeded." }
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
syntax: { pt: "escolherImagem()", en: "pickImage()" },
|
|
486
|
+
java: "pickFile",
|
|
487
|
+
description: { pt: "Abre o seletor nativo para o usuario escolher uma imagem.", en: "Opens the native picker for one image." },
|
|
488
|
+
returns: { pt: "{ uri, name, nome, size, tamanho, mimeType } ou null.", en: "{ uri, name, nome, size, tamanho, mimeType } or null." },
|
|
489
|
+
handling: { pt: "Se vier `null`, o usuario cancelou. Use `uri` em `<img>`/upload; nao espere caminho absoluto de arquivo.", en: "If it returns `null`, the user canceled. Use `uri` for `<img>`/upload; do not expect an absolute file path." }
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
syntax: { pt: "escolherImagens({ multiplo: true })", en: "pickImages({ multiple: true })" },
|
|
493
|
+
java: "pickFile",
|
|
494
|
+
description: { pt: "Abre galeria/seletor para varias imagens.", en: "Opens gallery/picker for multiple images." },
|
|
495
|
+
returns: { pt: "Array de arquivos; vazio se o usuario cancelar.", en: "Array of files; empty when the user cancels." },
|
|
496
|
+
handling: { pt: "Sempre trate como array. Limite quantidade/tamanho antes de enviar ou processar.", en: "Always handle it as an array. Limit quantity/size before uploading or processing." }
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
syntax: { pt: "escolherArquivo({ tipos: ['application/pdf'] })", en: "pickFile({ types: ['application/pdf'] })" },
|
|
500
|
+
java: "pickFile",
|
|
501
|
+
description: { pt: "Abre o seletor nativo para PDF, ZIP, TXT ou qualquer MIME.", en: "Opens the native picker for PDF, ZIP, TXT or any MIME." },
|
|
502
|
+
returns: { pt: "{ uri, name, nome, size, tamanho, mimeType } ou null.", en: "{ uri, name, nome, size, tamanho, mimeType } or null." },
|
|
503
|
+
handling: { pt: "Valide `mimeType` e `size` antes de aceitar. Cancelamento retorna `null` na funcao de arquivo unico.", en: "Validate `mimeType` and `size` before accepting. Cancel returns `null` for the single-file function." }
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
syntax: { pt: "escolherArquivos({ multiplo: true })", en: "pickFiles({ multiple: true })" },
|
|
507
|
+
java: "pickFile",
|
|
508
|
+
description: { pt: "Seleciona varios arquivos de qualquer tipo ou dos MIME informados.", en: "Selects multiple files of any type or the MIME types you provide." },
|
|
509
|
+
returns: { pt: "Array de arquivos.", en: "Array of files." },
|
|
510
|
+
handling: { pt: "Itere sobre o array e trate item por item; nao bloqueie a UI se muitos arquivos forem escolhidos.", en: "Iterate over the array and handle each item; avoid blocking the UI if many files are selected." }
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
syntax: { pt: "escolherVideo()", en: "pickVideo()" },
|
|
514
|
+
java: "pickFile",
|
|
515
|
+
description: { pt: "Abre o seletor nativo filtrando videos.", en: "Opens the native picker filtered to videos." },
|
|
516
|
+
returns: { pt: "{ uri, name, size, mimeType } ou null.", en: "{ uri, name, size, mimeType } or null." },
|
|
517
|
+
handling: { pt: "Videos podem ser grandes: confira `size` e mostre progresso se for fazer upload.", en: "Videos can be large: check `size` and show progress if you upload them." }
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
syntax: { pt: "escolherPasta()", en: "pickFolder()" },
|
|
521
|
+
java: "pickFolder",
|
|
522
|
+
description: { pt: "Abre o seletor nativo de pasta quando o Android permitir.", en: "Opens the native folder picker when Android allows it." },
|
|
523
|
+
returns: { pt: "{ uri } ou objeto vazio se cancelar.", en: "{ uri } or an empty object when canceled." },
|
|
524
|
+
handling: { pt: "Confira se `uri` existe antes de salvar. Android moderno entrega URI de documento, nao caminho real.", en: "Check that `uri` exists before saving. Modern Android returns a document URI, not a real path." }
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
syntax: { pt: "salvarArquivo({ nome, conteudo })", en: "saveFile({ name, content })" },
|
|
528
|
+
java: "saveFile",
|
|
529
|
+
description: { pt: "Abre o modal nativo para o usuario escolher onde salvar.", en: "Opens the native modal so the user chooses where to save." },
|
|
530
|
+
returns: { pt: "{ uri, name, size, mimeType, saved } ou objeto vazio se cancelar.", en: "{ uri, name, size, mimeType, saved } or an empty object when canceled." },
|
|
531
|
+
handling: { pt: "Depois do retorno, confira `saved === true`. Para binario, envie `base64`; para texto, use `conteudo`/`content`.", en: "After the return, check `saved === true`. For binary data, send `base64`; for text, use `content`/`conteudo`." }
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
syntax: { pt: "compartilhar({ texto, url })", en: "share({ text, url })" },
|
|
535
|
+
java: "share",
|
|
536
|
+
description: { pt: "Abre a folha nativa de compartilhamento.", en: "Opens the native share sheet." },
|
|
537
|
+
returns: { pt: "Promise<void>.", en: "Promise<void>." },
|
|
538
|
+
handling: { pt: "Monte mensagens curtas e trate erro quando nao houver app capaz de compartilhar.", en: "Build short messages and handle errors when no app can share the content." }
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
syntax: { pt: "copiarTexto('texto') / lerTextoCopiado()", en: "copyText('text') / readText()" },
|
|
542
|
+
java: "copyText/readText",
|
|
543
|
+
description: { pt: "Escreve e le a area de transferencia.", en: "Writes and reads the clipboard." },
|
|
544
|
+
returns: { pt: "`copiarTexto` retorna void; `lerTextoCopiado` retorna string.", en: "`copyText` returns void; `readText` returns string." },
|
|
545
|
+
handling: { pt: "Apos copiar, confirme com um toast. Ao ler, aceite string vazia porque a area de transferencia pode estar vazia.", en: "After copying, confirm with a toast. When reading, accept an empty string because the clipboard may be empty." }
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
syntax: { pt: "manterTelaLigada(true)", en: "keepScreenOn(true)" },
|
|
549
|
+
java: "keepScreenAwake",
|
|
550
|
+
description: { pt: "Impede a tela de apagar enquanto o app esta aberto.", en: "Keeps the screen awake while the app is open." },
|
|
551
|
+
returns: { pt: "Promise<void>.", en: "Promise<void>." },
|
|
552
|
+
handling: { pt: "Ative so em telas de leitura, video ou monitoramento; desligue com `false` ao sair da tela.", en: "Enable only on reading, video or monitoring screens; turn it off with `false` when leaving the screen." }
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
syntax: { pt: "brilhoTela(0.8)", en: "setScreenBrightness(0.8)" },
|
|
556
|
+
java: "setScreenBrightness",
|
|
557
|
+
description: { pt: "Ajusta o brilho apenas da janela do app.", en: "Adjusts only this app window brightness." },
|
|
558
|
+
returns: { pt: "Promise<void>.", en: "Promise<void>." },
|
|
559
|
+
handling: { pt: "Use valor entre 0 e 1. Guarde o valor anterior se quiser restaurar a experiencia do usuario.", en: "Use a value between 0 and 1. Store the previous value if you want to restore the user experience." }
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
syntax: { pt: "definirCorTema({ statusBarColor, navigationBarColor })", en: "setThemeColor({ statusBarColor, navigationBarColor })" },
|
|
563
|
+
java: "setSystemBarsColor",
|
|
564
|
+
description: { pt: "Muda as cores da barra de status e navegacao Android em tempo real.", en: "Changes Android status and navigation bar colors at runtime." },
|
|
565
|
+
returns: { pt: "{ statusBarColor, navigationBarColor, darkIcons, applied }.", en: "{ statusBarColor, navigationBarColor, darkIcons, applied }." },
|
|
566
|
+
handling: { pt: "Use quando quiser controlar manualmente. Com `themeMode: 'auto'`, o html2apk chama isso sozinho conforme a cor visivel na tela.", en: "Use when you want manual control. With `themeMode: 'auto'`, html2apk calls this automatically from the visible screen color." }
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
syntax: { pt: "abrirNoApp('/pagina.html')", en: "openInApp('/page.html')" },
|
|
570
|
+
java: "window.location",
|
|
571
|
+
description: { pt: "Navega dentro do proprio APK/WebView, bom para rotas locais, hashes ou paginas do app.", en: "Navigates inside the APK/WebView, useful for local routes, hashes or app pages." },
|
|
572
|
+
returns: { pt: "{ url, target: 'app', opened, replace }.", en: "{ url, target: 'app', opened, replace }." },
|
|
573
|
+
handling: { pt: "Use para telas internas. Passe `{ substituir: true }` se nao quiser manter a pagina anterior no historico.", en: "Use for internal screens. Pass `{ replace: true }` if you do not want to keep the previous page in history." }
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
syntax: { pt: "abrirForaDoApp('https://...')", en: "openOutsideApp('https://...')" },
|
|
577
|
+
java: "openUrl",
|
|
578
|
+
description: { pt: "Abre URL fora do APK, em navegador ou outro app Android. `abrirUrlExterno()` continua como alias.", en: "Opens a URL outside the APK, in a browser or another Android app. `openExternalUrl()` remains an alias." },
|
|
579
|
+
returns: { pt: "Promise<void>.", en: "Promise<void>." },
|
|
580
|
+
handling: { pt: "Valide a URL antes de chamar e trate erro caso nenhum app consiga abrir o endereco.", en: "Validate the URL before calling and handle errors if no app can open the address." }
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
syntax: { pt: "abrirWhatsapp('559999999999')", en: "openWhatsapp('559999999999')" },
|
|
584
|
+
java: "openWhatsapp",
|
|
585
|
+
description: { pt: "Abre conversa do WhatsApp pelo numero.", en: "Opens a WhatsApp conversation by phone number." },
|
|
586
|
+
returns: { pt: "Promise<void>.", en: "Promise<void>." },
|
|
587
|
+
handling: { pt: "Envie numero com DDI e apenas digitos. Tenha fallback para navegador se WhatsApp nao estiver disponivel.", en: "Send the phone number with country code and digits only. Keep a browser fallback if WhatsApp is unavailable." }
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
syntax: { pt: "discar('11999999999') / abrirMapa('Sao Paulo')", en: "dial('11999999999') / openMap('Sao Paulo')" },
|
|
591
|
+
java: "dial/openMap",
|
|
592
|
+
description: { pt: "Abre discador ou mapa nativo sem executar acao sensivel sozinho.", en: "Opens dialer or native map without performing sensitive action alone." },
|
|
593
|
+
returns: { pt: "Promise<void>.", en: "Promise<void>." },
|
|
594
|
+
handling: { pt: "O usuario ainda confirma chamada/rota. Mostre erro se nao houver app de telefone ou mapa.", en: "The user still confirms the call/route. Show an error if no phone or map app is available." }
|
|
595
|
+
},
|
|
596
|
+
{
|
|
597
|
+
syntax: { pt: "infoDispositivo()", en: "deviceInfo()" },
|
|
598
|
+
java: "deviceInfo",
|
|
599
|
+
description: { pt: "Retorna fabricante, modelo, Android, SDK e package name.", en: "Returns manufacturer, model, Android, SDK and package name." },
|
|
600
|
+
returns: { pt: "{ manufacturer, fabricante, model, modelo, androidVersion, sdkInt, packageName }.", en: "{ manufacturer, fabricante, model, modelo, androidVersion, sdkInt, packageName }." },
|
|
601
|
+
handling: { pt: "Use para diagnostico e ajustes de compatibilidade; nao dependa de modelo especifico para liberar recurso essencial.", en: "Use for diagnostics and compatibility tweaks; do not depend on a specific model for essential features." }
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
syntax: { pt: "infoRede() / infoBateria()", en: "networkInfo() / batteryInfo()" },
|
|
605
|
+
java: "networkInfo/batteryInfo",
|
|
606
|
+
description: { pt: "Consulta conexao atual e bateria.", en: "Reads current connection and battery." },
|
|
607
|
+
returns: { pt: "rede: { online, tipo/type }; bateria: { level, charging }.", en: "network: { online, tipo/type }; battery: { level, charging }." },
|
|
608
|
+
handling: { pt: "Combine com `aoEvento('rede:mudou')` e `aoEvento('bateria:mudou')` para atualizar a tela sem ficar consultando em loop.", en: "Combine with `onEvent('network:changed')` and `onEvent('battery:changed')` to update the UI without polling." }
|
|
609
|
+
},
|
|
610
|
+
{
|
|
611
|
+
syntax: { pt: "infoMemoria()", en: "memoryInfo()" },
|
|
612
|
+
java: "memoryInfo",
|
|
613
|
+
description: { pt: "Monitora RAM disponivel, total, baixa memoria e heap do app.", en: "Monitors available RAM, total RAM, low-memory state and app heap." },
|
|
614
|
+
returns: { pt: "{ availableBytes, totalBytes, lowMemory, appUsedBytes, appMaxBytes }.", en: "{ availableBytes, totalBytes, lowMemory, appUsedBytes, appMaxBytes }." },
|
|
615
|
+
handling: { pt: "Converta bytes para MB/GB na UI. Se `lowMemory` vier true, reduza cache, imagens grandes ou tarefas em segundo plano.", en: "Convert bytes to MB/GB in the UI. If `lowMemory` is true, reduce cache, large images or background tasks." }
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
syntax: { pt: "infoArmazenamento()", en: "storageInfo()" },
|
|
619
|
+
java: "storageInfo",
|
|
620
|
+
description: { pt: "Retorna armazenamento interno/cache disponivel e usado.", en: "Returns available and used internal/cache storage." },
|
|
621
|
+
returns: { pt: "{ internal, cache, appExternal } com bytes.", en: "{ internal, cache, appExternal } in bytes." },
|
|
622
|
+
handling: { pt: "Antes de salvar arquivos grandes, compare o tamanho com `availableBytes` e mostre aviso se faltar espaco.", en: "Before saving large files, compare size with `availableBytes` and warn when there is not enough space." }
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
syntax: { pt: "infoDesempenho()", en: "performanceInfo()" },
|
|
626
|
+
java: "performanceInfo",
|
|
627
|
+
description: { pt: "Agrupa memoria, armazenamento, bateria, rede e timestamp.", en: "Groups memory, storage, battery, network and timestamp." },
|
|
628
|
+
returns: { pt: "{ timestamp, memory, storage, battery, network }.", en: "{ timestamp, memory, storage, battery, network }." },
|
|
629
|
+
handling: { pt: "Ideal para uma tela de diagnostico. Atualize com intervalo moderado, por exemplo a cada 5 ou 10 segundos.", en: "Ideal for a diagnostics screen. Refresh at a moderate interval, for example every 5 or 10 seconds." }
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
syntax: { pt: "appsAbertos()", en: "openAppsMemory()" },
|
|
633
|
+
java: "openAppsMemory",
|
|
634
|
+
description: { pt: "Lista processos/apps que o Android permite ver e estima RAM por app. Android moderno pode retornar apenas o proprio app por privacidade.", en: "Lists processes/apps Android allows this app to see and estimates RAM per app. Modern Android may only return this app for privacy." },
|
|
635
|
+
returns: { pt: "{ apps: [{ name, packageName, ramBytes, ramMb }], porNome, limited, observacao }.", en: "{ apps: [{ name, packageName, ramBytes, ramMb }], byName, limited, note }." },
|
|
636
|
+
handling: { pt: "Use como diagnostico aproximado. Se `limited` vier true ou `apps` vier pequeno, explique que o Android bloqueou visao completa.", en: "Use it as approximate diagnostics. If `limited` is true or `apps` is small, explain that Android blocked full visibility." }
|
|
637
|
+
},
|
|
638
|
+
{
|
|
639
|
+
syntax: { pt: "iniciarIconeFlutuante() / pararIconeFlutuante()", en: "startFloatingIcon() / stopFloatingIcon()" },
|
|
640
|
+
java: "FloatingIconService",
|
|
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." },
|
|
642
|
+
returns: { pt: "Promise<void>.", en: "Promise<void>." },
|
|
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 agora. A permissao aparece automaticamente se o Android exigir.", en: "To show a notification now. Permission appears automatically if Android requires it." },
|
|
672
|
+
example: {
|
|
673
|
+
pt: `await notificar({
|
|
674
|
+
titulo: "Pedido aprovado",
|
|
675
|
+
texto: "Toque para abrir os detalhes",
|
|
676
|
+
aoClicar: { acao: "abrir-pedido", id: 123 }
|
|
677
|
+
});`,
|
|
678
|
+
en: `await notify({
|
|
679
|
+
title: "Order approved",
|
|
680
|
+
text: "Tap to open details",
|
|
681
|
+
onClick: { action: "open-order", id: 123 }
|
|
682
|
+
});`
|
|
683
|
+
}
|
|
684
|
+
},
|
|
685
|
+
{
|
|
686
|
+
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." },
|
|
687
|
+
example: {
|
|
688
|
+
pt: `const aviso = await agendarNotificacao({
|
|
689
|
+
titulo: "Lembrete",
|
|
690
|
+
texto: "Volte ao app daqui 1 minuto",
|
|
691
|
+
quando: Date.now() + 60 * 1000
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
console.log("ID para cancelar depois:", aviso.id);`,
|
|
695
|
+
en: `const reminder = await scheduleNotification({
|
|
696
|
+
title: "Reminder",
|
|
697
|
+
text: "Come back in 1 minute",
|
|
698
|
+
when: Date.now() + 60 * 1000
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
console.log("ID to cancel later:", reminder.id);`
|
|
702
|
+
}
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
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." },
|
|
706
|
+
example: {
|
|
707
|
+
pt: `const loop = await agendarLoopNotificacoes({
|
|
708
|
+
aCada: "12h",
|
|
709
|
+
notificacoes: [
|
|
710
|
+
{ titulo: "Agua", texto: "Beba agua agora" },
|
|
711
|
+
{ titulo: "Alongar", texto: "Faca uma pausa rapida" }
|
|
712
|
+
]
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
// Depois, se quiser parar:
|
|
716
|
+
// await cancelarNotificacao(loop.id);`,
|
|
717
|
+
en: `const loop = await scheduleNotificationLoop({
|
|
718
|
+
every: "12h",
|
|
719
|
+
notifications: [
|
|
720
|
+
{ title: "Water", text: "Drink water now" },
|
|
721
|
+
{ title: "Stretch", text: "Take a quick break" }
|
|
722
|
+
]
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
// Later, to stop it:
|
|
726
|
+
// await cancelNotification(loop.id);`
|
|
727
|
+
}
|
|
728
|
+
},
|
|
729
|
+
{
|
|
730
|
+
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." },
|
|
731
|
+
example: {
|
|
732
|
+
pt: `const permissao = await solicitarPermissaoNotificacoes();
|
|
733
|
+
|
|
734
|
+
if (!permissao.granted) {
|
|
735
|
+
if (permissao.settingsOpened) {
|
|
736
|
+
console.log("O Android abriu as configuracoes do app");
|
|
737
|
+
}
|
|
738
|
+
}`,
|
|
739
|
+
en: `const permission = await requestNotificationPermission();
|
|
740
|
+
|
|
741
|
+
if (!permission.granted) {
|
|
742
|
+
if (permission.settingsOpened) {
|
|
743
|
+
console.log("Android opened the app settings");
|
|
744
|
+
}
|
|
745
|
+
}`
|
|
746
|
+
}
|
|
747
|
+
},
|
|
748
|
+
{
|
|
749
|
+
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." },
|
|
750
|
+
example: {
|
|
751
|
+
pt: `const permitido = await solicitarPermissaoPush();
|
|
752
|
+
|
|
753
|
+
if (permitido) {
|
|
754
|
+
await identificarUsuarioPush("usuario-123");
|
|
755
|
+
await adicionarTagPush("plano", "premium");
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const parar = aoClicarPush((evento) => {
|
|
759
|
+
abrirNoApp("#/notificacoes");
|
|
760
|
+
});`,
|
|
761
|
+
en: `const allowed = await requestPushPermission();
|
|
762
|
+
|
|
763
|
+
if (allowed) {
|
|
764
|
+
await loginPushUser("user-123");
|
|
765
|
+
await addPushTag("plan", "premium");
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const stop = onPushClick((event) => {
|
|
769
|
+
openInApp("#/notifications");
|
|
770
|
+
});`
|
|
771
|
+
}
|
|
772
|
+
},
|
|
773
|
+
{
|
|
774
|
+
when: { pt: "Para consultar permissoes sem abrir uma solicitacao na tela.", en: "To check permissions without opening a permission prompt." },
|
|
775
|
+
example: {
|
|
776
|
+
pt: `const permissoes = await statusPermissoes([
|
|
777
|
+
"CAMERA",
|
|
778
|
+
"RECORD_AUDIO",
|
|
779
|
+
"POST_NOTIFICATIONS"
|
|
780
|
+
]);
|
|
781
|
+
|
|
782
|
+
console.log(permissoes);`,
|
|
783
|
+
en: `const permissions = await permissionStatus([
|
|
784
|
+
"CAMERA",
|
|
785
|
+
"RECORD_AUDIO",
|
|
786
|
+
"POST_NOTIFICATIONS"
|
|
787
|
+
]);
|
|
788
|
+
|
|
789
|
+
console.log(permissions);`
|
|
790
|
+
}
|
|
791
|
+
},
|
|
792
|
+
{
|
|
793
|
+
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." },
|
|
794
|
+
example: {
|
|
795
|
+
pt: `const parar = aoEvento("app:background", (evento) => {
|
|
796
|
+
console.log("Saiu da frente", evento.timestamp);
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
// Quando nao precisar mais:
|
|
800
|
+
// parar();`,
|
|
801
|
+
en: `const stop = onEvent("app:background", (event) => {
|
|
802
|
+
console.log("Moved to background", event.timestamp);
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
// When you no longer need it:
|
|
806
|
+
// stop();`
|
|
807
|
+
}
|
|
808
|
+
},
|
|
809
|
+
{
|
|
810
|
+
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." },
|
|
811
|
+
example: {
|
|
812
|
+
pt: `const pararMinimizar = aoMinimizar(() => {
|
|
813
|
+
console.log("Pause videos, timers ou leituras pesadas");
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
const pararVoltar = aoVoltarParaApp(() => {
|
|
817
|
+
console.log("Atualize dados se precisar");
|
|
818
|
+
});`,
|
|
819
|
+
en: `const stopMinimize = onMinimize(() => {
|
|
820
|
+
console.log("Pause videos, timers or heavy reads");
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
const stopResume = onAppResume(() => {
|
|
824
|
+
console.log("Refresh data if needed");
|
|
825
|
+
});`
|
|
826
|
+
}
|
|
827
|
+
},
|
|
828
|
+
{
|
|
829
|
+
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." },
|
|
830
|
+
example: {
|
|
831
|
+
pt: `const linkInicial = await obterLinkInicial();
|
|
832
|
+
if (linkInicial) {
|
|
833
|
+
abrirNoApp("#" + linkInicial.path);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
aoAbrirLink((link) => {
|
|
837
|
+
console.log("Link recebido", link.url);
|
|
838
|
+
});`,
|
|
839
|
+
en: `const initialLink = await getInitialLink();
|
|
840
|
+
if (initialLink) {
|
|
841
|
+
openInApp("#" + initialLink.path);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
onOpenLink((link) => {
|
|
845
|
+
console.log("Link received", link.url);
|
|
846
|
+
});`
|
|
847
|
+
}
|
|
848
|
+
},
|
|
849
|
+
{
|
|
850
|
+
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." },
|
|
851
|
+
example: {
|
|
852
|
+
pt: `const camera = await solicitarPermissaoCamera();
|
|
853
|
+
|
|
854
|
+
if (camera.granted) {
|
|
855
|
+
await lanterna(true);
|
|
856
|
+
} else if (camera.settingsOpened) {
|
|
857
|
+
console.log("Libere Camera nas configuracoes e volte ao app");
|
|
858
|
+
}`,
|
|
859
|
+
en: `const camera = await requestCameraPermission();
|
|
860
|
+
|
|
861
|
+
if (camera.granted) {
|
|
862
|
+
await flashlight(true);
|
|
863
|
+
} else if (camera.settingsOpened) {
|
|
864
|
+
console.log("Allow Camera in settings and return to the app");
|
|
865
|
+
}`
|
|
866
|
+
}
|
|
867
|
+
},
|
|
868
|
+
{
|
|
869
|
+
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." },
|
|
870
|
+
example: {
|
|
871
|
+
pt: `const inicio = await ouvirMic();
|
|
872
|
+
if (inicio.settingsOpened) {
|
|
873
|
+
console.log("Libere Microfone nas configuracoes e tente de novo");
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
setTimeout(async () => {
|
|
878
|
+
const audio = await pararMic();
|
|
879
|
+
const url = "data:" + audio.mimeType + ";base64," + audio.base64;
|
|
880
|
+
new Audio(url).play();
|
|
881
|
+
}, 3000);`,
|
|
882
|
+
en: `const start = await listenMic();
|
|
883
|
+
if (start.settingsOpened) {
|
|
884
|
+
console.log("Allow Microphone in settings and try again");
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
setTimeout(async () => {
|
|
889
|
+
const audio = await stopMic();
|
|
890
|
+
const url = "data:" + audio.mimeType + ";base64," + audio.base64;
|
|
891
|
+
new Audio(url).play();
|
|
892
|
+
}, 3000);`
|
|
893
|
+
}
|
|
894
|
+
},
|
|
895
|
+
{
|
|
896
|
+
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." },
|
|
897
|
+
example: {
|
|
898
|
+
pt: `const status = await lanterna(true);
|
|
899
|
+
|
|
900
|
+
if (status.settingsOpened) {
|
|
901
|
+
console.log("Libere Camera nas configuracoes e chame lanterna(true) de novo");
|
|
902
|
+
} else if (!status.available) {
|
|
903
|
+
await toast("Este aparelho nao tem lanterna");
|
|
904
|
+
}`,
|
|
905
|
+
en: `const status = await flashlight(true);
|
|
906
|
+
|
|
907
|
+
if (status.settingsOpened) {
|
|
908
|
+
console.log("Allow Camera in settings and call flashlight(true) again");
|
|
909
|
+
} else if (!status.available) {
|
|
910
|
+
await toast("This device has no flashlight");
|
|
911
|
+
}`
|
|
912
|
+
}
|
|
913
|
+
},
|
|
914
|
+
{
|
|
915
|
+
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." },
|
|
916
|
+
example: {
|
|
917
|
+
pt: `const status = await alternarLanterna();
|
|
918
|
+
console.log("Lanterna ligada?", status.enabled);`,
|
|
919
|
+
en: `const status = await toggleFlashlight();
|
|
920
|
+
console.log("Flashlight on?", status.enabled);`
|
|
921
|
+
}
|
|
922
|
+
},
|
|
923
|
+
{
|
|
924
|
+
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." },
|
|
925
|
+
example: {
|
|
926
|
+
pt: `const imagem = await escolherImagem();
|
|
927
|
+
|
|
928
|
+
if (imagem) {
|
|
929
|
+
document.querySelector("img.preview").src = imagem.uri;
|
|
930
|
+
}`,
|
|
931
|
+
en: `const image = await pickImage();
|
|
932
|
+
|
|
933
|
+
if (image) {
|
|
934
|
+
document.querySelector("img.preview").src = image.uri;
|
|
935
|
+
}`
|
|
936
|
+
}
|
|
937
|
+
},
|
|
938
|
+
{
|
|
939
|
+
when: { pt: "Para galeria com selecao multipla, como anexar varias fotos.", en: "For multiple gallery selection, such as attaching many photos." },
|
|
940
|
+
example: {
|
|
941
|
+
pt: `const imagens = await escolherImagens({ multiplo: true });
|
|
942
|
+
|
|
943
|
+
for (const imagem of imagens) {
|
|
944
|
+
console.log(imagem.nome, imagem.tamanho);
|
|
945
|
+
}`,
|
|
946
|
+
en: `const images = await pickImages({ multiple: true });
|
|
947
|
+
|
|
948
|
+
for (const image of images) {
|
|
949
|
+
console.log(image.name, image.size);
|
|
950
|
+
}`
|
|
951
|
+
}
|
|
952
|
+
},
|
|
953
|
+
{
|
|
954
|
+
when: { pt: "Para escolher PDF, ZIP, TXT ou qualquer tipo de documento.", en: "To choose PDF, ZIP, TXT or any document type." },
|
|
955
|
+
example: {
|
|
956
|
+
pt: `const pdf = await escolherArquivo({
|
|
957
|
+
tipos: ["application/pdf"]
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
if (pdf) {
|
|
961
|
+
console.log("PDF escolhido:", pdf.nome);
|
|
962
|
+
}`,
|
|
963
|
+
en: `const pdf = await pickFile({
|
|
964
|
+
types: ["application/pdf"]
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
if (pdf) {
|
|
968
|
+
console.log("Chosen PDF:", pdf.name);
|
|
969
|
+
}`
|
|
970
|
+
}
|
|
971
|
+
},
|
|
972
|
+
{
|
|
973
|
+
when: { pt: "Para anexar varios arquivos de uma vez.", en: "To attach multiple files at once." },
|
|
974
|
+
example: {
|
|
975
|
+
pt: `const arquivos = await escolherArquivos({ multiplo: true });
|
|
976
|
+
|
|
977
|
+
const total = arquivos.reduce((soma, arquivo) => {
|
|
978
|
+
return soma + (arquivo.tamanho || 0);
|
|
979
|
+
}, 0);
|
|
980
|
+
|
|
981
|
+
console.log("Total em bytes:", total);`,
|
|
982
|
+
en: `const files = await pickFiles({ multiple: true });
|
|
983
|
+
|
|
984
|
+
const total = files.reduce((sum, file) => {
|
|
985
|
+
return sum + (file.size || 0);
|
|
986
|
+
}, 0);
|
|
987
|
+
|
|
988
|
+
console.log("Total bytes:", total);`
|
|
989
|
+
}
|
|
990
|
+
},
|
|
991
|
+
{
|
|
992
|
+
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." },
|
|
993
|
+
example: {
|
|
994
|
+
pt: `const video = await escolherVideo();
|
|
995
|
+
|
|
996
|
+
if (video && video.tamanho > 50 * 1024 * 1024) {
|
|
997
|
+
await toast("Video muito grande");
|
|
998
|
+
}`,
|
|
999
|
+
en: `const video = await pickVideo();
|
|
1000
|
+
|
|
1001
|
+
if (video && video.size > 50 * 1024 * 1024) {
|
|
1002
|
+
await toast("Video is too large");
|
|
1003
|
+
}`
|
|
1004
|
+
}
|
|
1005
|
+
},
|
|
1006
|
+
{
|
|
1007
|
+
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." },
|
|
1008
|
+
example: {
|
|
1009
|
+
pt: `const pasta = await escolherPasta();
|
|
1010
|
+
|
|
1011
|
+
if (pasta.uri) {
|
|
1012
|
+
console.log("Pasta escolhida:", pasta.uri);
|
|
1013
|
+
}`,
|
|
1014
|
+
en: `const folder = await pickFolder();
|
|
1015
|
+
|
|
1016
|
+
if (folder.uri) {
|
|
1017
|
+
console.log("Chosen folder:", folder.uri);
|
|
1018
|
+
}`
|
|
1019
|
+
}
|
|
1020
|
+
},
|
|
1021
|
+
{
|
|
1022
|
+
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." },
|
|
1023
|
+
example: {
|
|
1024
|
+
pt: `const salvo = await salvarArquivo({
|
|
1025
|
+
nome: "relatorio.txt",
|
|
1026
|
+
mimeType: "text/plain",
|
|
1027
|
+
conteudo: "Conteudo do relatorio"
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
if (salvo.saved) {
|
|
1031
|
+
await toast("Arquivo salvo");
|
|
1032
|
+
}`,
|
|
1033
|
+
en: `const saved = await saveFile({
|
|
1034
|
+
name: "report.txt",
|
|
1035
|
+
mimeType: "text/plain",
|
|
1036
|
+
content: "Report content"
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
if (saved.saved) {
|
|
1040
|
+
await toast("File saved");
|
|
1041
|
+
}`
|
|
1042
|
+
}
|
|
1043
|
+
},
|
|
1044
|
+
{
|
|
1045
|
+
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." },
|
|
1046
|
+
example: {
|
|
1047
|
+
pt: `await compartilhar({
|
|
1048
|
+
texto: "Veja esse app",
|
|
1049
|
+
url: "https://exemplo.com"
|
|
1050
|
+
});`,
|
|
1051
|
+
en: `await share({
|
|
1052
|
+
text: "Check this app",
|
|
1053
|
+
url: "https://example.com"
|
|
1054
|
+
});`
|
|
1055
|
+
}
|
|
1056
|
+
},
|
|
1057
|
+
{
|
|
1058
|
+
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." },
|
|
1059
|
+
example: {
|
|
1060
|
+
pt: `await copiarTexto("ABC-123");
|
|
1061
|
+
await toast("Codigo copiado");
|
|
1062
|
+
|
|
1063
|
+
const copiado = await lerTextoCopiado();
|
|
1064
|
+
console.log(copiado);`,
|
|
1065
|
+
en: `await copyText("ABC-123");
|
|
1066
|
+
await toast("Code copied");
|
|
1067
|
+
|
|
1068
|
+
const copied = await readText();
|
|
1069
|
+
console.log(copied);`
|
|
1070
|
+
}
|
|
1071
|
+
},
|
|
1072
|
+
{
|
|
1073
|
+
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." },
|
|
1074
|
+
example: {
|
|
1075
|
+
pt: `await manterTelaLigada(true);
|
|
1076
|
+
|
|
1077
|
+
// Ao sair da tela:
|
|
1078
|
+
await manterTelaLigada(false);`,
|
|
1079
|
+
en: `await keepScreenOn(true);
|
|
1080
|
+
|
|
1081
|
+
// When leaving the screen:
|
|
1082
|
+
await keepScreenOn(false);`
|
|
1083
|
+
}
|
|
1084
|
+
},
|
|
1085
|
+
{
|
|
1086
|
+
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." },
|
|
1087
|
+
example: {
|
|
1088
|
+
pt: `await brilhoTela(0.8);
|
|
1089
|
+
|
|
1090
|
+
// Restaurar comportamento padrao:
|
|
1091
|
+
await brilhoTela(-1);`,
|
|
1092
|
+
en: `await setScreenBrightness(0.8);
|
|
1093
|
+
|
|
1094
|
+
// Restore default behavior:
|
|
1095
|
+
await setScreenBrightness(-1);`
|
|
1096
|
+
}
|
|
1097
|
+
},
|
|
1098
|
+
{
|
|
1099
|
+
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." },
|
|
1100
|
+
example: {
|
|
1101
|
+
pt: `await definirCorTema({
|
|
1102
|
+
statusBarColor: "#126fff",
|
|
1103
|
+
navigationBarColor: "#101827"
|
|
1104
|
+
});`,
|
|
1105
|
+
en: `await setThemeColor({
|
|
1106
|
+
statusBarColor: "#126fff",
|
|
1107
|
+
navigationBarColor: "#101827"
|
|
1108
|
+
});`
|
|
1109
|
+
}
|
|
1110
|
+
},
|
|
1111
|
+
{
|
|
1112
|
+
when: { pt: "Para navegar dentro do proprio WebView do APK.", en: "To navigate inside the APK WebView itself." },
|
|
1113
|
+
example: {
|
|
1114
|
+
pt: `await abrirNoApp("#/perfil");
|
|
1115
|
+
|
|
1116
|
+
// Sem deixar voltar para a tela anterior:
|
|
1117
|
+
await abrirNoApp("#/login", { substituir: true });`,
|
|
1118
|
+
en: `await openInApp("#/profile");
|
|
1119
|
+
|
|
1120
|
+
// Without keeping the previous screen:
|
|
1121
|
+
await openInApp("#/login", { replace: true });`
|
|
1122
|
+
}
|
|
1123
|
+
},
|
|
1124
|
+
{
|
|
1125
|
+
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." },
|
|
1126
|
+
example: {
|
|
1127
|
+
pt: `await abrirForaDoApp("https://exemplo.com");`,
|
|
1128
|
+
en: `await openOutsideApp("https://example.com");`
|
|
1129
|
+
}
|
|
1130
|
+
},
|
|
1131
|
+
{
|
|
1132
|
+
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." },
|
|
1133
|
+
example: {
|
|
1134
|
+
pt: `await abrirWhatsapp(
|
|
1135
|
+
"559999999999",
|
|
1136
|
+
"Oi, vim pelo app"
|
|
1137
|
+
);`,
|
|
1138
|
+
en: `await openWhatsapp(
|
|
1139
|
+
"559999999999",
|
|
1140
|
+
"Hi, I came from the app"
|
|
1141
|
+
);`
|
|
1142
|
+
}
|
|
1143
|
+
},
|
|
1144
|
+
{
|
|
1145
|
+
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." },
|
|
1146
|
+
example: {
|
|
1147
|
+
pt: `await discar("11999999999");
|
|
1148
|
+
await abrirMapa("Avenida Paulista, Sao Paulo");`,
|
|
1149
|
+
en: `await dial("11999999999");
|
|
1150
|
+
await openMap("Avenida Paulista, Sao Paulo");`
|
|
1151
|
+
}
|
|
1152
|
+
},
|
|
1153
|
+
{
|
|
1154
|
+
when: { pt: "Para diagnostico, suporte ou pequenas adaptacoes por versao do Android.", en: "For diagnostics, support or small adaptations by Android version." },
|
|
1155
|
+
example: {
|
|
1156
|
+
pt: `const aparelho = await infoDispositivo();
|
|
1157
|
+
|
|
1158
|
+
console.log(aparelho.modelo, aparelho.androidVersion);`,
|
|
1159
|
+
en: `const device = await deviceInfo();
|
|
1160
|
+
|
|
1161
|
+
console.log(device.model, device.androidVersion);`
|
|
1162
|
+
}
|
|
1163
|
+
},
|
|
1164
|
+
{
|
|
1165
|
+
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." },
|
|
1166
|
+
example: {
|
|
1167
|
+
pt: `const rede = await infoRede();
|
|
1168
|
+
const bateria = await infoBateria();
|
|
1169
|
+
|
|
1170
|
+
if (!rede.online) {
|
|
1171
|
+
await toast("Sem internet");
|
|
1172
|
+
}`,
|
|
1173
|
+
en: `const network = await networkInfo();
|
|
1174
|
+
const battery = await batteryInfo();
|
|
1175
|
+
|
|
1176
|
+
if (!network.online) {
|
|
1177
|
+
await toast("No internet");
|
|
1178
|
+
}`
|
|
1179
|
+
}
|
|
1180
|
+
},
|
|
1181
|
+
{
|
|
1182
|
+
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." },
|
|
1183
|
+
example: {
|
|
1184
|
+
pt: `const memoria = await infoMemoria();
|
|
1185
|
+
const mbLivre = memoria.availableBytes / 1024 / 1024;
|
|
1186
|
+
|
|
1187
|
+
if (memoria.lowMemory) {
|
|
1188
|
+
console.log("Reduza cache. MB livre:", mbLivre);
|
|
1189
|
+
}`,
|
|
1190
|
+
en: `const memory = await memoryInfo();
|
|
1191
|
+
const freeMb = memory.availableBytes / 1024 / 1024;
|
|
1192
|
+
|
|
1193
|
+
if (memory.lowMemory) {
|
|
1194
|
+
console.log("Reduce cache. Free MB:", freeMb);
|
|
1195
|
+
}`
|
|
1196
|
+
}
|
|
1197
|
+
},
|
|
1198
|
+
{
|
|
1199
|
+
when: { pt: "Para conferir espaco antes de baixar ou salvar arquivos grandes.", en: "To check space before downloading or saving large files." },
|
|
1200
|
+
example: {
|
|
1201
|
+
pt: `const armazenamento = await infoArmazenamento();
|
|
1202
|
+
const livre = armazenamento.internal.availableBytes;
|
|
1203
|
+
|
|
1204
|
+
console.log("Livre em MB:", Math.round(livre / 1024 / 1024));`,
|
|
1205
|
+
en: `const storage = await storageInfo();
|
|
1206
|
+
const free = storage.internal.availableBytes;
|
|
1207
|
+
|
|
1208
|
+
console.log("Free MB:", Math.round(free / 1024 / 1024));`
|
|
1209
|
+
}
|
|
1210
|
+
},
|
|
1211
|
+
{
|
|
1212
|
+
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." },
|
|
1213
|
+
example: {
|
|
1214
|
+
pt: `const desempenho = await infoDesempenho();
|
|
1215
|
+
|
|
1216
|
+
console.log(desempenho.memory);
|
|
1217
|
+
console.log(desempenho.storage);
|
|
1218
|
+
console.log(desempenho.battery);`,
|
|
1219
|
+
en: `const performance = await performanceInfo();
|
|
1220
|
+
|
|
1221
|
+
console.log(performance.memory);
|
|
1222
|
+
console.log(performance.storage);
|
|
1223
|
+
console.log(performance.battery);`
|
|
1224
|
+
}
|
|
1225
|
+
},
|
|
1226
|
+
{
|
|
1227
|
+
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." },
|
|
1228
|
+
example: {
|
|
1229
|
+
pt: `const resultado = await appsAbertos();
|
|
1230
|
+
|
|
1231
|
+
for (const app of resultado.apps) {
|
|
1232
|
+
console.log(app.nome, app.ramMb + " MB");
|
|
1233
|
+
}`,
|
|
1234
|
+
en: `const result = await openAppsMemory();
|
|
1235
|
+
|
|
1236
|
+
for (const app of result.apps) {
|
|
1237
|
+
console.log(app.name, app.ramMb + " MB");
|
|
1238
|
+
}`
|
|
1239
|
+
}
|
|
1240
|
+
},
|
|
1241
|
+
{
|
|
1242
|
+
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." },
|
|
1243
|
+
example: {
|
|
1244
|
+
pt: `const status = await iniciarIconeFlutuante();
|
|
1245
|
+
|
|
1246
|
+
if (status.requiresSettings) {
|
|
1247
|
+
console.log("O Android abriu a tela de sobreposicao");
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
// Para desligar:
|
|
1251
|
+
// await pararIconeFlutuante();`,
|
|
1252
|
+
en: `const status = await startFloatingIcon();
|
|
1253
|
+
|
|
1254
|
+
if (status.requiresSettings) {
|
|
1255
|
+
console.log("Android opened the draw-over-apps screen");
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// To turn it off:
|
|
1259
|
+
// await stopFloatingIcon();`
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
337
1262
|
];
|
|
338
1263
|
|
|
339
1264
|
const state = {
|
|
@@ -348,6 +1273,7 @@ const state = {
|
|
|
348
1273
|
buildRunning: false,
|
|
349
1274
|
lastApkPath: null,
|
|
350
1275
|
lastDistPath: null,
|
|
1276
|
+
defaultIconPath: "",
|
|
351
1277
|
animationTimer: null,
|
|
352
1278
|
progress: 0,
|
|
353
1279
|
logsVisible: localStorage.getItem("html2apk.logsVisible") === "true"
|
|
@@ -383,6 +1309,7 @@ function collectElements() {
|
|
|
383
1309
|
"entryPath",
|
|
384
1310
|
"doctorButton",
|
|
385
1311
|
"buildButton",
|
|
1312
|
+
"usbDebugButton",
|
|
386
1313
|
"appNameInput",
|
|
387
1314
|
"packageIdInput",
|
|
388
1315
|
"versionInput",
|
|
@@ -390,8 +1317,10 @@ function collectElements() {
|
|
|
390
1317
|
"orientationInput",
|
|
391
1318
|
"minSdkVersionInput",
|
|
392
1319
|
"androidPlatformInput",
|
|
1320
|
+
"themeModeInput",
|
|
393
1321
|
"themeColorInput",
|
|
394
1322
|
"themeColorTextInput",
|
|
1323
|
+
"oneSignalAppIdInput",
|
|
395
1324
|
"permissionGrid",
|
|
396
1325
|
"iconPathInput",
|
|
397
1326
|
"iconPreview",
|
|
@@ -413,6 +1342,8 @@ function collectElements() {
|
|
|
413
1342
|
"apkPath",
|
|
414
1343
|
"openDistButton",
|
|
415
1344
|
"showApkButton",
|
|
1345
|
+
"successTitle",
|
|
1346
|
+
"successText",
|
|
416
1347
|
"successApkPath",
|
|
417
1348
|
"successOpenDistButton",
|
|
418
1349
|
"successShowApkButton",
|
|
@@ -423,6 +1354,7 @@ function collectElements() {
|
|
|
423
1354
|
"toggleLogsButton",
|
|
424
1355
|
"bottomToggleLogsButton",
|
|
425
1356
|
"bottomClearLogsButton",
|
|
1357
|
+
"devInstagramButton",
|
|
426
1358
|
"minimizeButton",
|
|
427
1359
|
"maximizeButton",
|
|
428
1360
|
"closeButton",
|
|
@@ -524,6 +1456,15 @@ function toggleLogBar() {
|
|
|
524
1456
|
applyLogBarVisibility();
|
|
525
1457
|
}
|
|
526
1458
|
|
|
1459
|
+
function showLogBar() {
|
|
1460
|
+
if (state.logsVisible) {
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
state.logsVisible = true;
|
|
1465
|
+
applyLogBarVisibility();
|
|
1466
|
+
}
|
|
1467
|
+
|
|
527
1468
|
function clearLogs() {
|
|
528
1469
|
elements.logConsole.innerHTML = "";
|
|
529
1470
|
if (elements.bottomLogConsole) {
|
|
@@ -586,6 +1527,7 @@ function updateActionButtons() {
|
|
|
586
1527
|
|
|
587
1528
|
function setBuildButtons(enabled) {
|
|
588
1529
|
elements.buildButton.disabled = !enabled;
|
|
1530
|
+
elements.usbDebugButton.disabled = !enabled;
|
|
589
1531
|
}
|
|
590
1532
|
|
|
591
1533
|
function packageSegment(value) {
|
|
@@ -612,12 +1554,16 @@ function toFileUrl(filePath) {
|
|
|
612
1554
|
return `file:///${String(filePath).replace(/\\/g, "/").replace(/^\/+/, "")}`;
|
|
613
1555
|
}
|
|
614
1556
|
|
|
1557
|
+
function defaultIconPath() {
|
|
1558
|
+
return state.defaultIconPath || "";
|
|
1559
|
+
}
|
|
1560
|
+
|
|
615
1561
|
function isAbsolutePath(filePath) {
|
|
616
1562
|
return /^[a-zA-Z]:[\\/]/.test(String(filePath || "")) || String(filePath || "").startsWith("/");
|
|
617
1563
|
}
|
|
618
1564
|
|
|
619
1565
|
function iconPreviewPath(iconPath) {
|
|
620
|
-
if (!iconPath
|
|
1566
|
+
if (!iconPath) {
|
|
621
1567
|
return "../../../html2apk.png";
|
|
622
1568
|
}
|
|
623
1569
|
if (isAbsolutePath(iconPath)) {
|
|
@@ -626,6 +1572,18 @@ function iconPreviewPath(iconPath) {
|
|
|
626
1572
|
return toFileUrl(`${state.project.projectRoot}\\${iconPath}`);
|
|
627
1573
|
}
|
|
628
1574
|
|
|
1575
|
+
function displayIconValue(iconPath) {
|
|
1576
|
+
const value = String(iconPath || "").trim();
|
|
1577
|
+
if (!value || (state.defaultIconPath && value === state.defaultIconPath)) {
|
|
1578
|
+
return text("defaultIcon");
|
|
1579
|
+
}
|
|
1580
|
+
return value;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
function isConfigFilePath(filePath) {
|
|
1584
|
+
return /(^|[\\/])(app|config)\.json$/i.test(String(filePath || ""));
|
|
1585
|
+
}
|
|
1586
|
+
|
|
629
1587
|
function escapeHtml(value) {
|
|
630
1588
|
return String(value || "")
|
|
631
1589
|
.replace(/&/g, "&")
|
|
@@ -634,6 +1592,61 @@ function escapeHtml(value) {
|
|
|
634
1592
|
.replace(/"/g, """);
|
|
635
1593
|
}
|
|
636
1594
|
|
|
1595
|
+
function recipeForCode(index) {
|
|
1596
|
+
const language = currentLanguage();
|
|
1597
|
+
const recipe = nativeCodeRecipes[index] || {};
|
|
1598
|
+
return {
|
|
1599
|
+
when: recipe.when ? recipe.when[language] || recipe.when.pt : "",
|
|
1600
|
+
example: recipe.example ? recipe.example[language] || recipe.example.pt : ""
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
async function copyToClipboard(value) {
|
|
1605
|
+
const textValue = String(value || "");
|
|
1606
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
1607
|
+
await navigator.clipboard.writeText(textValue);
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
const textarea = document.createElement("textarea");
|
|
1612
|
+
textarea.value = textValue;
|
|
1613
|
+
textarea.setAttribute("readonly", "");
|
|
1614
|
+
textarea.style.position = "fixed";
|
|
1615
|
+
textarea.style.left = "-9999px";
|
|
1616
|
+
document.body.appendChild(textarea);
|
|
1617
|
+
textarea.select();
|
|
1618
|
+
document.execCommand("copy");
|
|
1619
|
+
textarea.remove();
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
async function handleNativeCodeCopy(event) {
|
|
1623
|
+
const button = event.target.closest("[data-copy-code]");
|
|
1624
|
+
if (!button) {
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
const index = Number.parseInt(button.dataset.copyCode, 10);
|
|
1629
|
+
const recipe = recipeForCode(index);
|
|
1630
|
+
if (!recipe.example) {
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
const originalText = button.textContent;
|
|
1635
|
+
try {
|
|
1636
|
+
await copyToClipboard(recipe.example);
|
|
1637
|
+
button.textContent = text("copiedCode");
|
|
1638
|
+
button.classList.add("copied");
|
|
1639
|
+
} catch (error) {
|
|
1640
|
+
button.textContent = text("copyFailed");
|
|
1641
|
+
button.classList.add("copy-error");
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
setTimeout(() => {
|
|
1645
|
+
button.textContent = originalText || text("copyCode");
|
|
1646
|
+
button.classList.remove("copied", "copy-error");
|
|
1647
|
+
}, 1400);
|
|
1648
|
+
}
|
|
1649
|
+
|
|
637
1650
|
function selectedOptionText(select) {
|
|
638
1651
|
if (!select || !select.selectedOptions || !select.selectedOptions.length) {
|
|
639
1652
|
return select ? select.value : "";
|
|
@@ -706,19 +1719,69 @@ function normalizeMinSdkVersion(value) {
|
|
|
706
1719
|
return MIN_SDK_OPTIONS.includes(parsed) ? parsed : DEFAULT_MIN_SDK_VERSION;
|
|
707
1720
|
}
|
|
708
1721
|
|
|
1722
|
+
function normalizeThemeMode(value) {
|
|
1723
|
+
return String(value || "").trim().toLowerCase() === "auto" ? "auto" : "fixed";
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
function normalizeOneSignalAppId(value) {
|
|
1727
|
+
return String(value || "").trim();
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
function oneSignalAppIdFromConfig(config = {}) {
|
|
1731
|
+
return normalizeOneSignalAppId(
|
|
1732
|
+
config.oneSignalAppId
|
|
1733
|
+
|| config.onesignalAppId
|
|
1734
|
+
|| (config.oneSignal && config.oneSignal.appId)
|
|
1735
|
+
|| (config.onesignal && config.onesignal.appId)
|
|
1736
|
+
|| ""
|
|
1737
|
+
);
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
function isValidOptionalOneSignalAppId(value) {
|
|
1741
|
+
const appId = normalizeOneSignalAppId(value);
|
|
1742
|
+
return appId.length === 0 || /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(appId);
|
|
1743
|
+
}
|
|
1744
|
+
|
|
709
1745
|
function renderNativeCodeGrid() {
|
|
710
1746
|
if (!elements.nativeCodeGrid) {
|
|
711
1747
|
return;
|
|
712
1748
|
}
|
|
713
1749
|
|
|
714
1750
|
const language = currentLanguage();
|
|
715
|
-
|
|
1751
|
+
const javaLabel = text("javaLabel");
|
|
1752
|
+
const doesLabel = text("doesLabel");
|
|
1753
|
+
const whenUseLabel = text("whenUseLabel");
|
|
1754
|
+
const returnsLabel = text("returnsLabel");
|
|
1755
|
+
const handlingLabel = text("handlingLabel");
|
|
1756
|
+
const exampleLabel = text("exampleLabel");
|
|
1757
|
+
const copyCodeLabel = text("copyCode");
|
|
1758
|
+
elements.nativeCodeGrid.innerHTML = nativeCodeEntries.map((entry, index) => {
|
|
1759
|
+
const syntax = entry.syntax ? entry.syntax[language] || entry.syntax.pt : entry.js;
|
|
1760
|
+
const description = entry.description[language] || entry.description.pt;
|
|
1761
|
+
const returns = entry.returns[language] || entry.returns.pt;
|
|
1762
|
+
const handling = entry.handling[language] || entry.handling.pt;
|
|
1763
|
+
const recipe = recipeForCode(index);
|
|
1764
|
+
|
|
1765
|
+
return `
|
|
716
1766
|
<article class="code-card">
|
|
717
|
-
<code
|
|
718
|
-
|
|
719
|
-
|
|
1767
|
+
<div class="code-card-top">
|
|
1768
|
+
<code>${escapeHtml(syntax)}</code>
|
|
1769
|
+
<span>${escapeHtml(javaLabel)}: ${escapeHtml(entry.java)}</span>
|
|
1770
|
+
</div>
|
|
1771
|
+
<p><strong>${escapeHtml(doesLabel)}:</strong> ${escapeHtml(description)}</p>
|
|
1772
|
+
<small><strong>${escapeHtml(whenUseLabel)}:</strong> ${escapeHtml(recipe.when)}</small>
|
|
1773
|
+
<small><strong>${escapeHtml(returnsLabel)}:</strong> ${escapeHtml(returns)}</small>
|
|
1774
|
+
<small class="handling"><strong>${escapeHtml(handlingLabel)}:</strong> ${escapeHtml(handling)}</small>
|
|
1775
|
+
<div class="copy-example">
|
|
1776
|
+
<div class="copy-example-header">
|
|
1777
|
+
<strong>${escapeHtml(exampleLabel)}</strong>
|
|
1778
|
+
<button type="button" class="copy-code-button" data-copy-code="${index}">${escapeHtml(copyCodeLabel)}</button>
|
|
1779
|
+
</div>
|
|
1780
|
+
<pre><code>${escapeHtml(recipe.example)}</code></pre>
|
|
1781
|
+
</div>
|
|
720
1782
|
</article>
|
|
721
|
-
|
|
1783
|
+
`;
|
|
1784
|
+
}).join("");
|
|
722
1785
|
}
|
|
723
1786
|
|
|
724
1787
|
function populateSettings(config = {}, project = state.project) {
|
|
@@ -730,12 +1793,15 @@ function populateSettings(config = {}, project = state.project) {
|
|
|
730
1793
|
elements.orientationInput.value = normalizeOrientationInputValue(config.orientation);
|
|
731
1794
|
elements.minSdkVersionInput.value = String(normalizeMinSdkVersion(config.minSdkVersion || config.androidMinSdkVersion));
|
|
732
1795
|
elements.androidPlatformInput.value = config.androidPlatform || "android@15.0.0";
|
|
1796
|
+
elements.themeModeInput.value = normalizeThemeMode(config.themeMode || config.theme || (String(config.themeColor || "").toLowerCase() === "auto" ? "auto" : "fixed"));
|
|
733
1797
|
const themeColor = normalizeHexColor(config.themeColor || config.splashBackgroundColor || config.backgroundColor);
|
|
734
1798
|
elements.themeColorInput.value = themeColor;
|
|
735
1799
|
elements.themeColorTextInput.value = themeColor;
|
|
1800
|
+
elements.oneSignalAppIdInput.value = oneSignalAppIdFromConfig(config);
|
|
736
1801
|
renderPermissionOptions(Array.isArray(config.permissions) && config.permissions.length ? config.permissions : DEFAULT_PERMISSIONS);
|
|
737
|
-
|
|
738
|
-
elements.
|
|
1802
|
+
const iconPath = String(config.icon || "").trim() || defaultIconPath();
|
|
1803
|
+
elements.iconPathInput.value = iconPath;
|
|
1804
|
+
elements.iconPreview.src = iconPreviewPath(iconPath);
|
|
739
1805
|
elements.debugInput.checked = Boolean(config.debug);
|
|
740
1806
|
elements.releaseInput.checked = Boolean(config.release);
|
|
741
1807
|
}
|
|
@@ -766,9 +1832,10 @@ function validateSettings() {
|
|
|
766
1832
|
if (!/^#[0-9a-fA-F]{6}$/.test(elements.themeColorTextInput.value.trim())) {
|
|
767
1833
|
errors.push(text("invalidThemeColor"));
|
|
768
1834
|
}
|
|
769
|
-
if (!elements.
|
|
770
|
-
errors.push(text("
|
|
771
|
-
}
|
|
1835
|
+
if (!isValidOptionalOneSignalAppId(elements.oneSignalAppIdInput.value)) {
|
|
1836
|
+
errors.push(text("invalidOneSignalAppId"));
|
|
1837
|
+
}
|
|
1838
|
+
if (elements.iconPathInput.value.trim() && !/\.png$/i.test(elements.iconPathInput.value.trim())) {
|
|
772
1839
|
errors.push(text("invalidIconType"));
|
|
773
1840
|
}
|
|
774
1841
|
|
|
@@ -811,9 +1878,11 @@ function renderReview() {
|
|
|
811
1878
|
[text("mode"), selectedOptionText(elements.modeInput)],
|
|
812
1879
|
[text("orientation"), selectedOptionText(elements.orientationInput)],
|
|
813
1880
|
[text("minSdkVersion"), selectedOptionText(elements.minSdkVersionInput)],
|
|
1881
|
+
[text("themeMode"), selectedOptionText(elements.themeModeInput)],
|
|
814
1882
|
[text("appThemeColor"), elements.themeColorTextInput.value.trim()],
|
|
1883
|
+
[text("oneSignalAppId"), elements.oneSignalAppIdInput.value.trim() || "-"],
|
|
815
1884
|
[text("androidPermissions"), selectedPermissions().join(", ")],
|
|
816
|
-
[text("appIcon"), elements.iconPathInput.value.trim()]
|
|
1885
|
+
[text("appIcon"), displayIconValue(elements.iconPathInput.value.trim())]
|
|
817
1886
|
];
|
|
818
1887
|
|
|
819
1888
|
elements.reviewGrid.innerHTML = items.map(([label, value]) => `
|
|
@@ -976,22 +2045,67 @@ async function ensureEnvironmentBeforeSettings() {
|
|
|
976
2045
|
return ready;
|
|
977
2046
|
}
|
|
978
2047
|
|
|
979
|
-
|
|
2048
|
+
function renderProjectSnapshot(project) {
|
|
980
2049
|
state.project = project;
|
|
981
|
-
state.doctorOk = false;
|
|
982
|
-
state.environmentOk = false;
|
|
983
|
-
state.environmentChecking = false;
|
|
984
|
-
state.environmentFailed = false;
|
|
985
|
-
state.settingsValid = false;
|
|
986
|
-
state.lastApkPath = null;
|
|
987
2050
|
state.lastDistPath = project.distPath;
|
|
988
|
-
|
|
989
2051
|
elements.projectSummary.classList.remove("hidden");
|
|
990
2052
|
elements.projectName.textContent = project.name;
|
|
991
2053
|
elements.projectPath.textContent = project.projectRoot;
|
|
992
2054
|
elements.configStatus.textContent = project.hasAppJson || project.hasConfigJson ? text("detected") : text("notDetected");
|
|
993
2055
|
elements.entryStatus.textContent = project.hasEntryFile ? text("detected") : text("missing");
|
|
994
2056
|
elements.entryPath.textContent = project.entryPath;
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
async function watchCurrentProject() {
|
|
2060
|
+
if (!state.project || !api.watchProject) {
|
|
2061
|
+
return;
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
try {
|
|
2065
|
+
const result = await api.watchProject(state.project.projectRoot);
|
|
2066
|
+
if (result && !result.ok) {
|
|
2067
|
+
appendLog(`${text("projectWatcherFail")}: ${result.message}`, "error");
|
|
2068
|
+
}
|
|
2069
|
+
} catch (error) {
|
|
2070
|
+
appendLog(`${text("projectWatcherFail")}: ${error.message}`, "error");
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
function applyProjectChange(payload) {
|
|
2075
|
+
if (!payload || !payload.project || !state.project) {
|
|
2076
|
+
return;
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
if (payload.project.projectRoot !== state.project.projectRoot) {
|
|
2080
|
+
return;
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
renderProjectSnapshot(payload.project);
|
|
2084
|
+
|
|
2085
|
+
const reloadSettings = isConfigFilePath(payload.changedPath);
|
|
2086
|
+
if (reloadSettings && !state.buildRunning) {
|
|
2087
|
+
populateSettings(payload.project.config || {}, payload.project);
|
|
2088
|
+
appendLog(text("projectConfigReloaded"), "system");
|
|
2089
|
+
} else {
|
|
2090
|
+
appendLog(`${text("projectAutoUpdated")}: ${payload.changedPath || payload.project.projectRoot}`, "system");
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
setStep("folder", payload.project.hasEntryFile ? "done" : "active", payload.project.hasEntryFile ? text("folderReady") : text("missing"));
|
|
2094
|
+
validateSettings();
|
|
2095
|
+
if (document.querySelector(".nav-item.active")?.dataset.view === "build") {
|
|
2096
|
+
renderReview();
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
async function summarizeProject(project) {
|
|
2101
|
+
renderProjectSnapshot(project);
|
|
2102
|
+
state.doctorOk = false;
|
|
2103
|
+
state.environmentOk = false;
|
|
2104
|
+
state.environmentChecking = false;
|
|
2105
|
+
state.environmentFailed = false;
|
|
2106
|
+
state.settingsValid = false;
|
|
2107
|
+
state.lastApkPath = null;
|
|
2108
|
+
|
|
995
2109
|
elements.nextSettingsButton.disabled = false;
|
|
996
2110
|
elements.doctorButton.disabled = true;
|
|
997
2111
|
setBuildButtons(false);
|
|
@@ -1005,6 +2119,7 @@ async function summarizeProject(project) {
|
|
|
1005
2119
|
appendLog(`${text("droppedFolder")}: ${project.projectRoot}`, "system");
|
|
1006
2120
|
validateSettings();
|
|
1007
2121
|
setProgress(project.hasEntryFile ? 25 : 15, project.hasEntryFile ? text("progressFolder") : text("missing"), project.hasEntryFile ? "" : "error");
|
|
2122
|
+
await watchCurrentProject();
|
|
1008
2123
|
await ensureEnvironmentBeforeSettings();
|
|
1009
2124
|
}
|
|
1010
2125
|
|
|
@@ -1049,6 +2164,9 @@ function buildOptions() {
|
|
|
1049
2164
|
orientation: elements.orientationInput.value,
|
|
1050
2165
|
minSdkVersion: normalizeMinSdkVersion(elements.minSdkVersionInput.value),
|
|
1051
2166
|
themeColor: normalizeHexColor(elements.themeColorTextInput.value),
|
|
2167
|
+
themeMode: normalizeThemeMode(elements.themeModeInput.value),
|
|
2168
|
+
theme: normalizeThemeMode(elements.themeModeInput.value),
|
|
2169
|
+
oneSignalAppId: normalizeOneSignalAppId(elements.oneSignalAppIdInput.value),
|
|
1052
2170
|
permissions: selectedPermissions(),
|
|
1053
2171
|
icon: elements.iconPathInput.value.trim(),
|
|
1054
2172
|
androidPlatform: elements.androidPlatformInput.value.trim(),
|
|
@@ -1082,6 +2200,7 @@ async function runBuildFlow() {
|
|
|
1082
2200
|
return;
|
|
1083
2201
|
}
|
|
1084
2202
|
|
|
2203
|
+
showLogBar();
|
|
1085
2204
|
state.buildRunning = true;
|
|
1086
2205
|
updateActionButtons();
|
|
1087
2206
|
elements.resultPanel.classList.add("hidden");
|
|
@@ -1118,6 +2237,8 @@ async function runBuildFlow() {
|
|
|
1118
2237
|
state.lastApkPath = result.apkPath;
|
|
1119
2238
|
state.lastDistPath = state.project.distPath;
|
|
1120
2239
|
elements.apkPath.textContent = result.apkPath;
|
|
2240
|
+
elements.successTitle.textContent = text("successTitle");
|
|
2241
|
+
elements.successText.textContent = text("successText");
|
|
1121
2242
|
elements.successApkPath.textContent = result.apkPath;
|
|
1122
2243
|
elements.resultPanel.classList.remove("hidden");
|
|
1123
2244
|
setStep("build", "done", text("buildOk"));
|
|
@@ -1137,6 +2258,74 @@ async function runBuildFlow() {
|
|
|
1137
2258
|
}
|
|
1138
2259
|
}
|
|
1139
2260
|
|
|
2261
|
+
async function runUsbDebugFlow() {
|
|
2262
|
+
if (!state.project || state.buildRunning) {
|
|
2263
|
+
setStatus("error", state.project ? text("usbDebugRunning") : text("chooseProjectFirst"));
|
|
2264
|
+
return;
|
|
2265
|
+
}
|
|
2266
|
+
if (!goToReview()) {
|
|
2267
|
+
return;
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
showLogBar();
|
|
2271
|
+
state.buildRunning = true;
|
|
2272
|
+
updateActionButtons();
|
|
2273
|
+
elements.resultPanel.classList.add("hidden");
|
|
2274
|
+
setView("build");
|
|
2275
|
+
setStep("folder", "done", text("folderReady"));
|
|
2276
|
+
|
|
2277
|
+
const doctorOk = await runDoctorOnly();
|
|
2278
|
+
if (!doctorOk) {
|
|
2279
|
+
state.buildRunning = false;
|
|
2280
|
+
updateActionButtons();
|
|
2281
|
+
return;
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
setStatus("busy", text("usbDebugRunning"));
|
|
2285
|
+
setStep("build", "active", text("usbDebugRunning"));
|
|
2286
|
+
setProgress(90, text("progressBuild"), "active");
|
|
2287
|
+
startAnimatedLogs();
|
|
2288
|
+
|
|
2289
|
+
try {
|
|
2290
|
+
const response = await api.runUsbDebugBuild(buildOptions());
|
|
2291
|
+
stopAnimatedLogs();
|
|
2292
|
+
if (!response.ok) {
|
|
2293
|
+
setStep("build", "error", text("usbDebugFail"));
|
|
2294
|
+
setStatus("error", text("usbDebugFail"));
|
|
2295
|
+
setProgress(90, text("progressError"), "error");
|
|
2296
|
+
appendLog(response.message || text("usbDebugFail"), "error");
|
|
2297
|
+
if (response.buildDir) {
|
|
2298
|
+
appendLog(`Build directory kept: ${response.buildDir}`, "system");
|
|
2299
|
+
}
|
|
2300
|
+
return;
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
const result = response.result;
|
|
2304
|
+
state.lastApkPath = result.apkPath;
|
|
2305
|
+
state.lastDistPath = state.project.distPath;
|
|
2306
|
+
elements.apkPath.textContent = result.apkPath;
|
|
2307
|
+
elements.successTitle.textContent = text("usbSuccessTitle");
|
|
2308
|
+
elements.successText.textContent = text("usbSuccessText");
|
|
2309
|
+
elements.successApkPath.textContent = result.apkPath;
|
|
2310
|
+
elements.resultPanel.classList.remove("hidden");
|
|
2311
|
+
setStep("build", "done", text("usbDebugOk"));
|
|
2312
|
+
setStatus("ready", text("usbDebugOk"));
|
|
2313
|
+
setProgress(100, text("progressDone"));
|
|
2314
|
+
appendLog(`${text("usbDebugOk")}: ${result.device && result.device.id ? result.device.id : "Android USB"}`, "success");
|
|
2315
|
+
appendLog(`${text("buildOk")}: ${result.apkPath}`, "success");
|
|
2316
|
+
setView("success");
|
|
2317
|
+
} catch (error) {
|
|
2318
|
+
stopAnimatedLogs();
|
|
2319
|
+
setStep("build", "error", text("usbDebugFail"));
|
|
2320
|
+
setStatus("error", error.message);
|
|
2321
|
+
setProgress(90, text("progressError"), "error");
|
|
2322
|
+
appendLog(error.message, "error");
|
|
2323
|
+
} finally {
|
|
2324
|
+
state.buildRunning = false;
|
|
2325
|
+
updateActionButtons();
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
|
|
1140
2329
|
function toggleTheme() {
|
|
1141
2330
|
state.theme = state.theme === "dark" ? "light" : "dark";
|
|
1142
2331
|
applyTheme();
|
|
@@ -1186,6 +2375,7 @@ function bindEvents() {
|
|
|
1186
2375
|
elements.settingsNextButton.addEventListener("click", goToReview);
|
|
1187
2376
|
elements.doctorButton.addEventListener("click", runDoctorOnly);
|
|
1188
2377
|
elements.buildButton.addEventListener("click", runBuildFlow);
|
|
2378
|
+
elements.usbDebugButton.addEventListener("click", runUsbDebugFlow);
|
|
1189
2379
|
elements.clearLogsButton.addEventListener("click", clearLogs);
|
|
1190
2380
|
elements.toggleLogsButton.addEventListener("click", toggleLogBar);
|
|
1191
2381
|
elements.bottomToggleLogsButton.addEventListener("click", toggleLogBar);
|
|
@@ -1220,6 +2410,7 @@ function bindEvents() {
|
|
|
1220
2410
|
validateSettings();
|
|
1221
2411
|
});
|
|
1222
2412
|
elements.permissionGrid.addEventListener("change", validateSettings);
|
|
2413
|
+
elements.nativeCodeGrid.addEventListener("click", handleNativeCodeCopy);
|
|
1223
2414
|
[
|
|
1224
2415
|
elements.appNameInput,
|
|
1225
2416
|
elements.packageIdInput,
|
|
@@ -1227,6 +2418,8 @@ function bindEvents() {
|
|
|
1227
2418
|
elements.orientationInput,
|
|
1228
2419
|
elements.minSdkVersionInput,
|
|
1229
2420
|
elements.androidPlatformInput,
|
|
2421
|
+
elements.themeModeInput,
|
|
2422
|
+
elements.oneSignalAppIdInput,
|
|
1230
2423
|
elements.debugInput,
|
|
1231
2424
|
elements.releaseInput
|
|
1232
2425
|
].forEach((input) => {
|
|
@@ -1262,6 +2455,11 @@ function bindEvents() {
|
|
|
1262
2455
|
elements.resultPanel.classList.add("hidden");
|
|
1263
2456
|
goToReview();
|
|
1264
2457
|
});
|
|
2458
|
+
elements.devInstagramButton.addEventListener("click", () => {
|
|
2459
|
+
api.openExternalUrl(DEVELOPER_INSTAGRAM_URL).catch(() => {
|
|
2460
|
+
setStatus("error", "Nao foi possivel abrir o Instagram.");
|
|
2461
|
+
});
|
|
2462
|
+
});
|
|
1265
2463
|
|
|
1266
2464
|
elements.dropZone.addEventListener("dragover", (event) => {
|
|
1267
2465
|
event.preventDefault();
|
|
@@ -1289,6 +2487,16 @@ function bindEvents() {
|
|
|
1289
2487
|
api.onInstallLog((payload) => {
|
|
1290
2488
|
appendLog(payload.line, payload.kind || "raw");
|
|
1291
2489
|
});
|
|
2490
|
+
|
|
2491
|
+
if (api.onProjectChanged) {
|
|
2492
|
+
api.onProjectChanged(applyProjectChange);
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
if (api.onProjectWatchError) {
|
|
2496
|
+
api.onProjectWatchError((payload) => {
|
|
2497
|
+
appendLog(`${text("projectWatcherFail")}: ${payload.message}`, "error");
|
|
2498
|
+
});
|
|
2499
|
+
}
|
|
1292
2500
|
}
|
|
1293
2501
|
|
|
1294
2502
|
async function init() {
|
|
@@ -1305,6 +2513,11 @@ async function init() {
|
|
|
1305
2513
|
try {
|
|
1306
2514
|
const info = await api.appInfo();
|
|
1307
2515
|
elements.appVersion.textContent = `v${info.version}`;
|
|
2516
|
+
state.defaultIconPath = info.iconPath || "";
|
|
2517
|
+
if (elements.iconPathInput && !elements.iconPathInput.value.trim() && state.defaultIconPath) {
|
|
2518
|
+
elements.iconPathInput.value = state.defaultIconPath;
|
|
2519
|
+
elements.iconPreview.src = iconPreviewPath(state.defaultIconPath);
|
|
2520
|
+
}
|
|
1308
2521
|
} catch {
|
|
1309
2522
|
elements.appVersion.textContent = "v0.1.0";
|
|
1310
2523
|
}
|