html2apk 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +318 -3
- package/examples/minimal/app.json +5 -0
- package/examples/minimal/dist/MeuApp-1.0.0-debug.apk +0 -0
- package/package.json +3 -2
- package/src/cli/index.js +22 -1
- package/src/cordova/config-xml.js +98 -6
- package/src/core/build-apk.js +150 -1
- package/src/core/config.js +74 -5
- package/src/core/defaults.js +15 -1
- package/src/desktop/main.js +30 -4
- package/src/desktop/preload.js +1 -0
- package/src/desktop/renderer/index.html +69 -2
- package/src/desktop/renderer/renderer.js +583 -2
- package/src/desktop/renderer/styles.css +131 -4
- package/src/templates/cordova-plugin-html2apk-bridge/plugin.xml +6 -0
- package/src/templates/cordova-plugin-html2apk-bridge/src/android/FloatingIconService.java +141 -0
- package/src/templates/cordova-plugin-html2apk-bridge/src/android/Html2ApkBridge.java +1674 -45
- package/src/templates/cordova-plugin-html2apk-bridge/src/android/NotificationReceiver.java +19 -5
- package/src/templates/cordova-plugin-html2apk-bridge/src/android/NotificationStore.java +13 -1
- package/src/templates/cordova-plugin-html2apk-bridge/www/html2apk-bridge.js +525 -1
- package/src/templates/html2apk-auto-theme.js +144 -0
- package/src/templates/html2apk-onesignal.js +155 -0
|
@@ -10,6 +10,7 @@ const i18n = {
|
|
|
10
10
|
navSettings: "Configuracoes",
|
|
11
11
|
navAppearance: "Aparencia",
|
|
12
12
|
navBuild: "Build",
|
|
13
|
+
navCodes: "Codigos",
|
|
13
14
|
navLogs: "Logs",
|
|
14
15
|
navHelp: "Ajuda",
|
|
15
16
|
theme: "Tema",
|
|
@@ -33,7 +34,22 @@ const i18n = {
|
|
|
33
34
|
appVersion: "Versao do app",
|
|
34
35
|
mode: "Modo",
|
|
35
36
|
chooseMode: "Escolha o modo",
|
|
37
|
+
modeFullscreen: "Tela cheia",
|
|
38
|
+
modeStandalone: "Normal",
|
|
39
|
+
modeFloating: "Flutuante",
|
|
40
|
+
orientation: "Orientacao",
|
|
41
|
+
orientationDefault: "Automatico",
|
|
42
|
+
orientationPortrait: "Vertical",
|
|
43
|
+
orientationLandscape: "Horizontal",
|
|
44
|
+
minSdkVersion: "Android minimo",
|
|
36
45
|
appIcon: "Icone do app",
|
|
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.",
|
|
52
|
+
androidPermissions: "Permissoes Android",
|
|
37
53
|
chooseIcon: "Escolher icone PNG",
|
|
38
54
|
reviewBuild: "Revisar build",
|
|
39
55
|
debugBuild: "Debug tecnico",
|
|
@@ -58,6 +74,11 @@ const i18n = {
|
|
|
58
74
|
showApk: "Mostrar APK",
|
|
59
75
|
logsEyebrow: "Ao vivo",
|
|
60
76
|
logsTitle: "Logs do processo",
|
|
77
|
+
codesEyebrow: "Bridge nativa",
|
|
78
|
+
codesTitle: "Codigos interpretados",
|
|
79
|
+
codesIntro: "Estas funcoes chamadas no JavaScript do app sao interpretadas pelo plugin Cordova e executadas no Java Android.",
|
|
80
|
+
returnsLabel: "Retorno",
|
|
81
|
+
handlingLabel: "Como tratar",
|
|
61
82
|
clearLogs: "Limpar logs",
|
|
62
83
|
helpEyebrow: "Sem misterio",
|
|
63
84
|
helpTitle: "Doctor, build e dependencias",
|
|
@@ -65,6 +86,9 @@ const i18n = {
|
|
|
65
86
|
helpBuild: "Cria um projeto Cordova temporario, copia seu app web e gera o arquivo APK em dist.",
|
|
66
87
|
helpDepsTitle: "Dependencias no EXE",
|
|
67
88
|
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.",
|
|
89
|
+
helpCreatorTitle: "Criador",
|
|
90
|
+
helpCreatorText: "Conheca o Dev Caio Multiversando, criador desta linda aplicacao html2apk.",
|
|
91
|
+
openInstagram: "Conhecer o dev",
|
|
68
92
|
ready: "Pronto",
|
|
69
93
|
selected: "Selecionado",
|
|
70
94
|
missing: "Nao encontrado",
|
|
@@ -92,6 +116,9 @@ const i18n = {
|
|
|
92
116
|
missingMode: "Escolha o modo do app.",
|
|
93
117
|
missingIcon: "Escolha o icone do app.",
|
|
94
118
|
invalidIconType: "Use um icone PNG para evitar falhas no Android.",
|
|
119
|
+
invalidThemeColor: "Use uma cor hexadecimal valida, exemplo: #126fff.",
|
|
120
|
+
invalidOneSignalAppId: "Use um OneSignal App ID valido ou deixe vazio.",
|
|
121
|
+
invalidMinSdkVersion: "Escolha uma versao minima do Android entre API 24 e API 36.",
|
|
95
122
|
iconSelected: "Icone selecionado",
|
|
96
123
|
progressLabel: "Progresso",
|
|
97
124
|
progressIdle: "Aguardando pasta",
|
|
@@ -123,6 +150,7 @@ const i18n = {
|
|
|
123
150
|
navSettings: "Settings",
|
|
124
151
|
navAppearance: "Appearance",
|
|
125
152
|
navBuild: "Build",
|
|
153
|
+
navCodes: "Code",
|
|
126
154
|
navLogs: "Logs",
|
|
127
155
|
navHelp: "Help",
|
|
128
156
|
theme: "Theme",
|
|
@@ -146,7 +174,22 @@ const i18n = {
|
|
|
146
174
|
appVersion: "App version",
|
|
147
175
|
mode: "Mode",
|
|
148
176
|
chooseMode: "Choose mode",
|
|
177
|
+
modeFullscreen: "Fullscreen",
|
|
178
|
+
modeStandalone: "Normal",
|
|
179
|
+
modeFloating: "Floating",
|
|
180
|
+
orientation: "Orientation",
|
|
181
|
+
orientationDefault: "Auto",
|
|
182
|
+
orientationPortrait: "Portrait",
|
|
183
|
+
orientationLandscape: "Landscape",
|
|
184
|
+
minSdkVersion: "Minimum Android",
|
|
149
185
|
appIcon: "App icon",
|
|
186
|
+
appThemeColor: "Theme/fallback color",
|
|
187
|
+
themeMode: "APK theme",
|
|
188
|
+
themeModeFixed: "Fixed color",
|
|
189
|
+
themeModeAuto: "Auto from screen",
|
|
190
|
+
oneSignalAppId: "OneSignal App ID",
|
|
191
|
+
oneSignalAppIdHint: "Optional. Use the OneSignal App ID, not the REST API Key.",
|
|
192
|
+
androidPermissions: "Android permissions",
|
|
150
193
|
chooseIcon: "Choose PNG icon",
|
|
151
194
|
reviewBuild: "Review build",
|
|
152
195
|
debugBuild: "Technical debug",
|
|
@@ -171,6 +214,11 @@ const i18n = {
|
|
|
171
214
|
showApk: "Show APK",
|
|
172
215
|
logsEyebrow: "Live",
|
|
173
216
|
logsTitle: "Process logs",
|
|
217
|
+
codesEyebrow: "Native bridge",
|
|
218
|
+
codesTitle: "Interpreted code",
|
|
219
|
+
codesIntro: "These functions called in your app JavaScript are interpreted by the Cordova plugin and executed in Android Java.",
|
|
220
|
+
returnsLabel: "Returns",
|
|
221
|
+
handlingLabel: "How to handle",
|
|
174
222
|
clearLogs: "Clear logs",
|
|
175
223
|
helpEyebrow: "Plain and simple",
|
|
176
224
|
helpTitle: "Doctor, build and dependencies",
|
|
@@ -178,6 +226,9 @@ const i18n = {
|
|
|
178
226
|
helpBuild: "Creates a temporary Cordova project, copies your web app and generates the APK in dist.",
|
|
179
227
|
helpDepsTitle: "Dependencies in the EXE",
|
|
180
228
|
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.",
|
|
229
|
+
helpCreatorTitle: "Creator",
|
|
230
|
+
helpCreatorText: "Meet Dev Caio Multiversando, creator of this lovely html2apk app.",
|
|
231
|
+
openInstagram: "Meet the dev",
|
|
181
232
|
ready: "Ready",
|
|
182
233
|
selected: "Selected",
|
|
183
234
|
missing: "Missing",
|
|
@@ -205,6 +256,9 @@ const i18n = {
|
|
|
205
256
|
missingMode: "Choose the app mode.",
|
|
206
257
|
missingIcon: "Choose the app icon.",
|
|
207
258
|
invalidIconType: "Use a PNG icon to avoid Android build failures.",
|
|
259
|
+
invalidThemeColor: "Use a valid hex color, example: #126fff.",
|
|
260
|
+
invalidOneSignalAppId: "Use a valid OneSignal App ID or leave it empty.",
|
|
261
|
+
invalidMinSdkVersion: "Choose a minimum Android version between API 24 and API 36.",
|
|
208
262
|
iconSelected: "Icon selected",
|
|
209
263
|
progressLabel: "Progress",
|
|
210
264
|
progressIdle: "Waiting for folder",
|
|
@@ -241,6 +295,323 @@ const animatedBuildLines = [
|
|
|
241
295
|
"Procurando APK final / Finding final APK"
|
|
242
296
|
];
|
|
243
297
|
|
|
298
|
+
const DEFAULT_PERMISSIONS = ["INTERNET", "POST_NOTIFICATIONS", "VIBRATE"];
|
|
299
|
+
const DEFAULT_MIN_SDK_VERSION = 24;
|
|
300
|
+
const MIN_SDK_OPTIONS = [24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36];
|
|
301
|
+
const DEVELOPER_INSTAGRAM_URL = "https://www.instagram.com/caiomutiversando?igsh=MWFpcmYzZDB3YTNzZQ==";
|
|
302
|
+
|
|
303
|
+
const permissionOptions = [
|
|
304
|
+
{
|
|
305
|
+
value: "INTERNET",
|
|
306
|
+
label: { pt: "Internet", en: "Internet" },
|
|
307
|
+
detail: { pt: "Acesso a rede", en: "Network access" }
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
value: "POST_NOTIFICATIONS",
|
|
311
|
+
label: { pt: "Notificacoes", en: "Notifications" },
|
|
312
|
+
detail: { pt: "Android 13+", en: "Android 13+" }
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
value: "VIBRATE",
|
|
316
|
+
label: { pt: "Vibracao", en: "Vibration" },
|
|
317
|
+
detail: { pt: "Permite vibrar", en: "Allows vibration" }
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
value: "CAMERA",
|
|
321
|
+
label: { pt: "Camera", en: "Camera" },
|
|
322
|
+
detail: { pt: "Captura de imagem", en: "Image capture" }
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
value: "RECORD_AUDIO",
|
|
326
|
+
label: { pt: "Microfone", en: "Microphone" },
|
|
327
|
+
detail: { pt: "Captura de audio", en: "Audio capture" }
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
value: "ACCESS_FINE_LOCATION",
|
|
331
|
+
label: { pt: "Localizacao precisa", en: "Precise location" },
|
|
332
|
+
detail: { pt: "GPS e rede", en: "GPS and network" }
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
value: "ACCESS_COARSE_LOCATION",
|
|
336
|
+
label: { pt: "Localizacao aproximada", en: "Approximate location" },
|
|
337
|
+
detail: { pt: "Rede", en: "Network" }
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
value: "SYSTEM_ALERT_WINDOW",
|
|
341
|
+
label: { pt: "Sobrepor apps", en: "Draw over apps" },
|
|
342
|
+
detail: { pt: "Necessaria no modo flutuante", en: "Required for floating mode" }
|
|
343
|
+
}
|
|
344
|
+
];
|
|
345
|
+
|
|
346
|
+
const nativeCodeEntries = [
|
|
347
|
+
{
|
|
348
|
+
syntax: { pt: "toast('Mensagem')", en: "toast('Message')" },
|
|
349
|
+
java: "toast",
|
|
350
|
+
description: { pt: "Mostra uma mensagem rapida nativa.", en: "Shows a native short message." },
|
|
351
|
+
returns: { pt: "Promise<void>.", en: "Promise<void>." },
|
|
352
|
+
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." }
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
syntax: { pt: "vibrar(250)", en: "vibrate(250)" },
|
|
356
|
+
java: "vibrate",
|
|
357
|
+
description: { pt: "Aciona a vibracao do aparelho por milissegundos.", en: "Vibrates the device for milliseconds." },
|
|
358
|
+
returns: { pt: "Promise<void>.", en: "Promise<void>." },
|
|
359
|
+
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
|
+
},
|
|
361
|
+
{
|
|
362
|
+
syntax: { pt: "notificar({ titulo, texto, acoes })", en: "notify({ title, text, actions })" },
|
|
363
|
+
java: "notify",
|
|
364
|
+
description: { pt: "Cria notificacao Android imediata. `acoes` ou `actions` viram botoes clicaveis.", en: "Creates an immediate Android notification. `actions` or `acoes` become clickable buttons." },
|
|
365
|
+
returns: { pt: "Promise<void>; cliques chegam em `aoEvento('notificacao:clicada')` ou `aoClicarNotificacao()`.", en: "Promise<void>; clicks arrive through `onEvent('notification:clicked')` or `onNotificationClick()`." },
|
|
366
|
+
handling: { pt: "Antes de usar, chame `solicitarPermissaoNotificacoes()` no Android 13+. Trate o clique pela acao enviada em `aoClicar`/`onClick`.", en: "Before using it, call `requestNotificationPermission()` on Android 13+. Handle clicks using the action sent in `onClick`/`aoClicar`." }
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
syntax: { pt: "agendarNotificacao({ titulo, texto, quando }) / agendarNotificacoes([...])", en: "scheduleNotification({ title, text, when }) / scheduleNotifications([...])" },
|
|
370
|
+
java: "scheduleNotification",
|
|
371
|
+
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." },
|
|
372
|
+
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." },
|
|
373
|
+
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()`." }
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
syntax: { pt: "agendarLoopNotificacoes({ aCada: '12h', notificacoes: [...] })", en: "scheduleNotificationLoop({ every: '12h', notifications: [...] })" },
|
|
377
|
+
java: "AlarmManager + NotificationStore",
|
|
378
|
+
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
|
+
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: "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)`." }
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
syntax: { pt: "solicitarPermissaoNotificacoes()", en: "requestNotificationPermission()" },
|
|
384
|
+
java: "requestNotificationPermission",
|
|
385
|
+
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 }." },
|
|
387
|
+
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
|
+
},
|
|
389
|
+
{
|
|
390
|
+
syntax: { pt: "solicitarPermissaoPush() / aoClicarPush(fn)", en: "requestPushPermission() / onPushClick(fn)" },
|
|
391
|
+
java: "onesignal-cordova-plugin",
|
|
392
|
+
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." },
|
|
393
|
+
returns: { pt: "`solicitarPermissaoPush` retorna boolean. `aoClicarPush` retorna funcao para parar de escutar.", en: "`requestPushPermission` returns boolean. `onPushClick` returns an unsubscribe function." },
|
|
394
|
+
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." }
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
syntax: { pt: "statusPermissoes(['CAMERA'])", en: "permissionStatus(['CAMERA'])" },
|
|
398
|
+
java: "permissionStatus",
|
|
399
|
+
description: { pt: "Consulta se permissoes Android estao liberadas.", en: "Checks whether Android permissions are granted." },
|
|
400
|
+
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 antes de liberar recursos sensiveis; quando vier falso, mostre uma etapa de permissao em vez de falhar silenciosamente.", en: "Use it before enabling sensitive features; when false, show a permission step instead of failing silently." }
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
syntax: { pt: "aoEvento('app:background', fn)", en: "onEvent('app:background', fn)" },
|
|
405
|
+
java: "dispatchEvent",
|
|
406
|
+
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." },
|
|
407
|
+
returns: { pt: "Funcao para cancelar a escuta.", en: "Unsubscribe function." },
|
|
408
|
+
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." }
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
syntax: { pt: "aoMinimizar(fn) / aoVoltarParaApp(fn)", en: "onMinimize(fn) / onAppResume(fn)" },
|
|
412
|
+
java: "lifecycle",
|
|
413
|
+
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." },
|
|
414
|
+
returns: { pt: "Funcao para cancelar a escuta.", en: "Unsubscribe function." },
|
|
415
|
+
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." }
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
syntax: { pt: "obterLinkInicial() / aoAbrirLink(fn)", en: "getInitialLink() / onOpenLink(fn)" },
|
|
419
|
+
java: "getInitialLink",
|
|
420
|
+
description: { pt: "Lida com deep links/app links que abriram o APK.", en: "Handles deep links/app links that opened the APK." },
|
|
421
|
+
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." },
|
|
422
|
+
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." }
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
syntax: { pt: "solicitarPermissaoCamera()", en: "requestCameraPermission()" },
|
|
426
|
+
java: "requestCameraPermission",
|
|
427
|
+
description: { pt: "Pede permissao de camera antes de usar lanterna.", en: "Requests camera permission before using flashlight." },
|
|
428
|
+
returns: { pt: "{ permission, required, granted }.", en: "{ permission, required, granted }." },
|
|
429
|
+
handling: { pt: "Chame antes de `lanterna()`. Se `granted` for falso, mostre uma mensagem e mantenha a lanterna desativada.", en: "Call before `flashlight()`. If `granted` is false, show a message and keep the flashlight disabled." }
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
syntax: { pt: "await ouvirMic(); const audio = await pararMic()", en: "await listenMic(); const audio = await stopMic()" },
|
|
433
|
+
java: "MediaRecorder",
|
|
434
|
+
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: "Chame `solicitarPermissaoMicrofone()` ou deixe `ouvirMic()` pedir a permissao. Para tocar/enviar, monte `data:${audio.mimeType};base64,${audio.base64}`; se parar rapido demais, tente aguardar alguns instantes antes de `pararMic()`.", en: "Call `requestMicrophonePermission()` or let `listenMic()` request permission. To play/upload, build `data:${audio.mimeType};base64,${audio.base64}`; if you stop too quickly, wait a little before `stopMic()`." }
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
syntax: { pt: "lanterna(true) / statusLanterna()", en: "flashlight(true) / flashlightStatus()" },
|
|
440
|
+
java: "flashlight",
|
|
441
|
+
description: { pt: "Liga, desliga e consulta a lanterna do aparelho.", en: "Turns the device flashlight on/off and reads its status." },
|
|
442
|
+
returns: { pt: "{ available, enabled, permissionGranted }.", en: "{ available, enabled, permissionGranted }." },
|
|
443
|
+
handling: { pt: "Se `available` for falso, esconda o controle. Se `permissionGranted` for falso, volte para a etapa de permissao.", en: "If `available` is false, hide the control. If `permissionGranted` is false, return to the permission step." }
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
syntax: { pt: "alternarLanterna()", en: "toggleFlashlight()" },
|
|
447
|
+
java: "toggleFlashlight",
|
|
448
|
+
description: { pt: "Inverte o estado atual da lanterna.", en: "Toggles the current flashlight state." },
|
|
449
|
+
returns: { pt: "{ available, enabled, permissionGranted }.", en: "{ available, enabled, permissionGranted }." },
|
|
450
|
+
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." }
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
syntax: { pt: "escolherImagem()", en: "pickImage()" },
|
|
454
|
+
java: "pickFile",
|
|
455
|
+
description: { pt: "Abre o seletor nativo para o usuario escolher uma imagem.", en: "Opens the native picker for one image." },
|
|
456
|
+
returns: { pt: "{ uri, name, nome, size, tamanho, mimeType } ou null.", en: "{ uri, name, nome, size, tamanho, mimeType } or null." },
|
|
457
|
+
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." }
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
syntax: { pt: "escolherImagens({ multiplo: true })", en: "pickImages({ multiple: true })" },
|
|
461
|
+
java: "pickFile",
|
|
462
|
+
description: { pt: "Abre galeria/seletor para varias imagens.", en: "Opens gallery/picker for multiple images." },
|
|
463
|
+
returns: { pt: "Array de arquivos; vazio se o usuario cancelar.", en: "Array of files; empty when the user cancels." },
|
|
464
|
+
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." }
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
syntax: { pt: "escolherArquivo({ tipos: ['application/pdf'] })", en: "pickFile({ types: ['application/pdf'] })" },
|
|
468
|
+
java: "pickFile",
|
|
469
|
+
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." },
|
|
470
|
+
returns: { pt: "{ uri, name, nome, size, tamanho, mimeType } ou null.", en: "{ uri, name, nome, size, tamanho, mimeType } or null." },
|
|
471
|
+
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." }
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
syntax: { pt: "escolherArquivos({ multiplo: true })", en: "pickFiles({ multiple: true })" },
|
|
475
|
+
java: "pickFile",
|
|
476
|
+
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." },
|
|
477
|
+
returns: { pt: "Array de arquivos.", en: "Array of files." },
|
|
478
|
+
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." }
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
syntax: { pt: "escolherVideo()", en: "pickVideo()" },
|
|
482
|
+
java: "pickFile",
|
|
483
|
+
description: { pt: "Abre o seletor nativo filtrando videos.", en: "Opens the native picker filtered to videos." },
|
|
484
|
+
returns: { pt: "{ uri, name, size, mimeType } ou null.", en: "{ uri, name, size, mimeType } or null." },
|
|
485
|
+
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." }
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
syntax: { pt: "escolherPasta()", en: "pickFolder()" },
|
|
489
|
+
java: "pickFolder",
|
|
490
|
+
description: { pt: "Abre o seletor nativo de pasta quando o Android permitir.", en: "Opens the native folder picker when Android allows it." },
|
|
491
|
+
returns: { pt: "{ uri } ou objeto vazio se cancelar.", en: "{ uri } or an empty object when canceled." },
|
|
492
|
+
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." }
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
syntax: { pt: "salvarArquivo({ nome, conteudo })", en: "saveFile({ name, content })" },
|
|
496
|
+
java: "saveFile",
|
|
497
|
+
description: { pt: "Abre o modal nativo para o usuario escolher onde salvar.", en: "Opens the native modal so the user chooses where to save." },
|
|
498
|
+
returns: { pt: "{ uri, name, size, mimeType, saved } ou objeto vazio se cancelar.", en: "{ uri, name, size, mimeType, saved } or an empty object when canceled." },
|
|
499
|
+
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`." }
|
|
500
|
+
},
|
|
501
|
+
{
|
|
502
|
+
syntax: { pt: "compartilhar({ texto, url })", en: "share({ text, url })" },
|
|
503
|
+
java: "share",
|
|
504
|
+
description: { pt: "Abre a folha nativa de compartilhamento.", en: "Opens the native share sheet." },
|
|
505
|
+
returns: { pt: "Promise<void>.", en: "Promise<void>." },
|
|
506
|
+
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." }
|
|
507
|
+
},
|
|
508
|
+
{
|
|
509
|
+
syntax: { pt: "copiarTexto('texto') / lerTextoCopiado()", en: "copyText('text') / readText()" },
|
|
510
|
+
java: "copyText/readText",
|
|
511
|
+
description: { pt: "Escreve e le a area de transferencia.", en: "Writes and reads the clipboard." },
|
|
512
|
+
returns: { pt: "`copiarTexto` retorna void; `lerTextoCopiado` retorna string.", en: "`copyText` returns void; `readText` returns string." },
|
|
513
|
+
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." }
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
syntax: { pt: "manterTelaLigada(true)", en: "keepScreenOn(true)" },
|
|
517
|
+
java: "keepScreenAwake",
|
|
518
|
+
description: { pt: "Impede a tela de apagar enquanto o app esta aberto.", en: "Keeps the screen awake while the app is open." },
|
|
519
|
+
returns: { pt: "Promise<void>.", en: "Promise<void>." },
|
|
520
|
+
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." }
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
syntax: { pt: "brilhoTela(0.8)", en: "setScreenBrightness(0.8)" },
|
|
524
|
+
java: "setScreenBrightness",
|
|
525
|
+
description: { pt: "Ajusta o brilho apenas da janela do app.", en: "Adjusts only this app window brightness." },
|
|
526
|
+
returns: { pt: "Promise<void>.", en: "Promise<void>." },
|
|
527
|
+
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." }
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
syntax: { pt: "definirCorTema({ statusBarColor, navigationBarColor })", en: "setThemeColor({ statusBarColor, navigationBarColor })" },
|
|
531
|
+
java: "setSystemBarsColor",
|
|
532
|
+
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." },
|
|
533
|
+
returns: { pt: "{ statusBarColor, navigationBarColor, darkIcons, applied }.", en: "{ statusBarColor, navigationBarColor, darkIcons, applied }." },
|
|
534
|
+
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." }
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
syntax: { pt: "abrirNoApp('/pagina.html')", en: "openInApp('/page.html')" },
|
|
538
|
+
java: "window.location",
|
|
539
|
+
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." },
|
|
540
|
+
returns: { pt: "{ url, target: 'app', opened, replace }.", en: "{ url, target: 'app', opened, replace }." },
|
|
541
|
+
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." }
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
syntax: { pt: "abrirForaDoApp('https://...')", en: "openOutsideApp('https://...')" },
|
|
545
|
+
java: "openUrl",
|
|
546
|
+
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." },
|
|
547
|
+
returns: { pt: "Promise<void>.", en: "Promise<void>." },
|
|
548
|
+
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." }
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
syntax: { pt: "abrirWhatsapp('559999999999')", en: "openWhatsapp('559999999999')" },
|
|
552
|
+
java: "openWhatsapp",
|
|
553
|
+
description: { pt: "Abre conversa do WhatsApp pelo numero.", en: "Opens a WhatsApp conversation by phone number." },
|
|
554
|
+
returns: { pt: "Promise<void>.", en: "Promise<void>." },
|
|
555
|
+
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." }
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
syntax: { pt: "discar('11999999999') / abrirMapa('Sao Paulo')", en: "dial('11999999999') / openMap('Sao Paulo')" },
|
|
559
|
+
java: "dial/openMap",
|
|
560
|
+
description: { pt: "Abre discador ou mapa nativo sem executar acao sensivel sozinho.", en: "Opens dialer or native map without performing sensitive action alone." },
|
|
561
|
+
returns: { pt: "Promise<void>.", en: "Promise<void>." },
|
|
562
|
+
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." }
|
|
563
|
+
},
|
|
564
|
+
{
|
|
565
|
+
syntax: { pt: "infoDispositivo()", en: "deviceInfo()" },
|
|
566
|
+
java: "deviceInfo",
|
|
567
|
+
description: { pt: "Retorna fabricante, modelo, Android, SDK e package name.", en: "Returns manufacturer, model, Android, SDK and package name." },
|
|
568
|
+
returns: { pt: "{ manufacturer, fabricante, model, modelo, androidVersion, sdkInt, packageName }.", en: "{ manufacturer, fabricante, model, modelo, androidVersion, sdkInt, packageName }." },
|
|
569
|
+
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." }
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
syntax: { pt: "infoRede() / infoBateria()", en: "networkInfo() / batteryInfo()" },
|
|
573
|
+
java: "networkInfo/batteryInfo",
|
|
574
|
+
description: { pt: "Consulta conexao atual e bateria.", en: "Reads current connection and battery." },
|
|
575
|
+
returns: { pt: "rede: { online, tipo/type }; bateria: { level, charging }.", en: "network: { online, tipo/type }; battery: { level, charging }." },
|
|
576
|
+
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." }
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
syntax: { pt: "infoMemoria()", en: "memoryInfo()" },
|
|
580
|
+
java: "memoryInfo",
|
|
581
|
+
description: { pt: "Monitora RAM disponivel, total, baixa memoria e heap do app.", en: "Monitors available RAM, total RAM, low-memory state and app heap." },
|
|
582
|
+
returns: { pt: "{ availableBytes, totalBytes, lowMemory, appUsedBytes, appMaxBytes }.", en: "{ availableBytes, totalBytes, lowMemory, appUsedBytes, appMaxBytes }." },
|
|
583
|
+
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." }
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
syntax: { pt: "infoArmazenamento()", en: "storageInfo()" },
|
|
587
|
+
java: "storageInfo",
|
|
588
|
+
description: { pt: "Retorna armazenamento interno/cache disponivel e usado.", en: "Returns available and used internal/cache storage." },
|
|
589
|
+
returns: { pt: "{ internal, cache, appExternal } com bytes.", en: "{ internal, cache, appExternal } in bytes." },
|
|
590
|
+
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." }
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
syntax: { pt: "infoDesempenho()", en: "performanceInfo()" },
|
|
594
|
+
java: "performanceInfo",
|
|
595
|
+
description: { pt: "Agrupa memoria, armazenamento, bateria, rede e timestamp.", en: "Groups memory, storage, battery, network and timestamp." },
|
|
596
|
+
returns: { pt: "{ timestamp, memory, storage, battery, network }.", en: "{ timestamp, memory, storage, battery, network }." },
|
|
597
|
+
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." }
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
syntax: { pt: "appsAbertos()", en: "openAppsMemory()" },
|
|
601
|
+
java: "openAppsMemory",
|
|
602
|
+
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." },
|
|
603
|
+
returns: { pt: "{ apps: [{ name, packageName, ramBytes, ramMb }], porNome, limited, observacao }.", en: "{ apps: [{ name, packageName, ramBytes, ramMb }], byName, limited, note }." },
|
|
604
|
+
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." }
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
syntax: { pt: "iniciarIconeFlutuante() / pararIconeFlutuante()", en: "startFloatingIcon() / stopFloatingIcon()" },
|
|
608
|
+
java: "FloatingIconService",
|
|
609
|
+
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
|
+
returns: { pt: "Promise<void>.", en: "Promise<void>." },
|
|
611
|
+
handling: { pt: "Antes de iniciar, confira `statusPermissaoSobreposicao()` e chame `solicitarPermissaoSobreposicao()` se faltar permissao.", en: "Before starting, check `overlayPermissionStatus()` and call `requestOverlayPermission()` when permission is missing." }
|
|
612
|
+
}
|
|
613
|
+
];
|
|
614
|
+
|
|
244
615
|
const state = {
|
|
245
616
|
language: localStorage.getItem("html2apk.language") || null,
|
|
246
617
|
theme: localStorage.getItem("html2apk.theme") || "light",
|
|
@@ -292,7 +663,14 @@ function collectElements() {
|
|
|
292
663
|
"packageIdInput",
|
|
293
664
|
"versionInput",
|
|
294
665
|
"modeInput",
|
|
666
|
+
"orientationInput",
|
|
667
|
+
"minSdkVersionInput",
|
|
295
668
|
"androidPlatformInput",
|
|
669
|
+
"themeModeInput",
|
|
670
|
+
"themeColorInput",
|
|
671
|
+
"themeColorTextInput",
|
|
672
|
+
"oneSignalAppIdInput",
|
|
673
|
+
"permissionGrid",
|
|
296
674
|
"iconPathInput",
|
|
297
675
|
"iconPreview",
|
|
298
676
|
"selectIconButton",
|
|
@@ -308,6 +686,7 @@ function collectElements() {
|
|
|
308
686
|
"progressBar",
|
|
309
687
|
"progressPercent",
|
|
310
688
|
"reviewGrid",
|
|
689
|
+
"nativeCodeGrid",
|
|
311
690
|
"resultPanel",
|
|
312
691
|
"apkPath",
|
|
313
692
|
"openDistButton",
|
|
@@ -322,6 +701,7 @@ function collectElements() {
|
|
|
322
701
|
"toggleLogsButton",
|
|
323
702
|
"bottomToggleLogsButton",
|
|
324
703
|
"bottomClearLogsButton",
|
|
704
|
+
"devInstagramButton",
|
|
325
705
|
"minimizeButton",
|
|
326
706
|
"maximizeButton",
|
|
327
707
|
"closeButton",
|
|
@@ -349,6 +729,8 @@ function applyLanguage() {
|
|
|
349
729
|
});
|
|
350
730
|
applyTheme();
|
|
351
731
|
applyLogBarVisibility();
|
|
732
|
+
renderPermissionOptions(selectedPermissions());
|
|
733
|
+
renderNativeCodeGrid();
|
|
352
734
|
if (state.project) {
|
|
353
735
|
validateSettings();
|
|
354
736
|
renderReview();
|
|
@@ -421,6 +803,15 @@ function toggleLogBar() {
|
|
|
421
803
|
applyLogBarVisibility();
|
|
422
804
|
}
|
|
423
805
|
|
|
806
|
+
function showLogBar() {
|
|
807
|
+
if (state.logsVisible) {
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
state.logsVisible = true;
|
|
812
|
+
applyLogBarVisibility();
|
|
813
|
+
}
|
|
814
|
+
|
|
424
815
|
function clearLogs() {
|
|
425
816
|
elements.logConsole.innerHTML = "";
|
|
426
817
|
if (elements.bottomLogConsole) {
|
|
@@ -492,6 +883,16 @@ function packageSegment(value) {
|
|
|
492
883
|
.replace(/^[^a-z]+/, "") || "app";
|
|
493
884
|
}
|
|
494
885
|
|
|
886
|
+
function normalizeHexColor(value, fallback = "#126fff") {
|
|
887
|
+
const textValue = String(value || "").trim();
|
|
888
|
+
const normalized = textValue.startsWith("#") ? textValue : `#${textValue}`;
|
|
889
|
+
return /^#[0-9a-fA-F]{6}$/.test(normalized) ? normalized.toLowerCase() : fallback;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function currentLanguage() {
|
|
893
|
+
return state.language || "pt";
|
|
894
|
+
}
|
|
895
|
+
|
|
495
896
|
function toFileUrl(filePath) {
|
|
496
897
|
if (!filePath) {
|
|
497
898
|
return "../../../html2apk.png";
|
|
@@ -521,13 +922,142 @@ function escapeHtml(value) {
|
|
|
521
922
|
.replace(/"/g, """);
|
|
522
923
|
}
|
|
523
924
|
|
|
925
|
+
function selectedOptionText(select) {
|
|
926
|
+
if (!select || !select.selectedOptions || !select.selectedOptions.length) {
|
|
927
|
+
return select ? select.value : "";
|
|
928
|
+
}
|
|
929
|
+
return select.selectedOptions[0].textContent || select.value;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function selectedPermissions() {
|
|
933
|
+
if (!elements.permissionGrid) {
|
|
934
|
+
return DEFAULT_PERMISSIONS.slice();
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
const inputs = Array.from(elements.permissionGrid.querySelectorAll("input[data-permission-option]"));
|
|
938
|
+
if (!inputs.length) {
|
|
939
|
+
return DEFAULT_PERMISSIONS.slice();
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const selected = inputs
|
|
943
|
+
.filter((input) => input.checked)
|
|
944
|
+
.map((input) => input.value);
|
|
945
|
+
|
|
946
|
+
if (elements.modeInput && elements.modeInput.value === "floating" && !selected.includes("SYSTEM_ALERT_WINDOW")) {
|
|
947
|
+
selected.push("SYSTEM_ALERT_WINDOW");
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
return Array.from(new Set(selected));
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function renderPermissionOptions(selected = DEFAULT_PERMISSIONS) {
|
|
954
|
+
if (!elements.permissionGrid) {
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
const selectedSet = new Set(selected);
|
|
959
|
+
if (elements.modeInput && elements.modeInput.value === "floating") {
|
|
960
|
+
selectedSet.add("SYSTEM_ALERT_WINDOW");
|
|
961
|
+
}
|
|
962
|
+
const language = currentLanguage();
|
|
963
|
+
|
|
964
|
+
elements.permissionGrid.innerHTML = permissionOptions.map((permission) => {
|
|
965
|
+
const checked = selectedSet.has(permission.value) ? " checked" : "";
|
|
966
|
+
const disabled = elements.modeInput && elements.modeInput.value === "floating" && permission.value === "SYSTEM_ALERT_WINDOW"
|
|
967
|
+
? " disabled"
|
|
968
|
+
: "";
|
|
969
|
+
|
|
970
|
+
return `
|
|
971
|
+
<label class="permission-option">
|
|
972
|
+
<input type="checkbox" data-permission-option value="${escapeHtml(permission.value)}"${checked}${disabled}>
|
|
973
|
+
<span>
|
|
974
|
+
<strong>${escapeHtml(permission.label[language] || permission.label.pt)}</strong>
|
|
975
|
+
<small>${escapeHtml(permission.detail[language] || permission.detail.pt)}</small>
|
|
976
|
+
</span>
|
|
977
|
+
</label>
|
|
978
|
+
`;
|
|
979
|
+
}).join("");
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function normalizeOrientationInputValue(value) {
|
|
983
|
+
if (value === "vertical") {
|
|
984
|
+
return "portrait";
|
|
985
|
+
}
|
|
986
|
+
if (value === "horizontal") {
|
|
987
|
+
return "landscape";
|
|
988
|
+
}
|
|
989
|
+
return ["portrait", "landscape"].includes(value) ? value : "default";
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
function normalizeMinSdkVersion(value) {
|
|
993
|
+
const parsed = Number.parseInt(value, 10);
|
|
994
|
+
return MIN_SDK_OPTIONS.includes(parsed) ? parsed : DEFAULT_MIN_SDK_VERSION;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function normalizeThemeMode(value) {
|
|
998
|
+
return String(value || "").trim().toLowerCase() === "auto" ? "auto" : "fixed";
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function normalizeOneSignalAppId(value) {
|
|
1002
|
+
return String(value || "").trim();
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
function oneSignalAppIdFromConfig(config = {}) {
|
|
1006
|
+
return normalizeOneSignalAppId(
|
|
1007
|
+
config.oneSignalAppId
|
|
1008
|
+
|| config.onesignalAppId
|
|
1009
|
+
|| (config.oneSignal && config.oneSignal.appId)
|
|
1010
|
+
|| (config.onesignal && config.onesignal.appId)
|
|
1011
|
+
|| ""
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function isValidOptionalOneSignalAppId(value) {
|
|
1016
|
+
const appId = normalizeOneSignalAppId(value);
|
|
1017
|
+
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);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
function renderNativeCodeGrid() {
|
|
1021
|
+
if (!elements.nativeCodeGrid) {
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
const language = currentLanguage();
|
|
1026
|
+
const returnsLabel = text("returnsLabel");
|
|
1027
|
+
const handlingLabel = text("handlingLabel");
|
|
1028
|
+
elements.nativeCodeGrid.innerHTML = nativeCodeEntries.map((entry) => {
|
|
1029
|
+
const syntax = entry.syntax ? entry.syntax[language] || entry.syntax.pt : entry.js;
|
|
1030
|
+
const description = entry.description[language] || entry.description.pt;
|
|
1031
|
+
const returns = entry.returns[language] || entry.returns.pt;
|
|
1032
|
+
const handling = entry.handling[language] || entry.handling.pt;
|
|
1033
|
+
|
|
1034
|
+
return `
|
|
1035
|
+
<article class="code-card">
|
|
1036
|
+
<code>${escapeHtml(syntax)}</code>
|
|
1037
|
+
<span>Java: ${escapeHtml(entry.java)}</span>
|
|
1038
|
+
<p>${escapeHtml(description)}</p>
|
|
1039
|
+
<small><strong>${escapeHtml(returnsLabel)}:</strong> ${escapeHtml(returns)}</small>
|
|
1040
|
+
<small class="handling"><strong>${escapeHtml(handlingLabel)}:</strong> ${escapeHtml(handling)}</small>
|
|
1041
|
+
</article>
|
|
1042
|
+
`;
|
|
1043
|
+
}).join("");
|
|
1044
|
+
}
|
|
1045
|
+
|
|
524
1046
|
function populateSettings(config = {}, project = state.project) {
|
|
525
1047
|
const projectName = project ? project.name : "MeuApp";
|
|
526
1048
|
elements.appNameInput.value = config.appName || projectName || "";
|
|
527
1049
|
elements.packageIdInput.value = config.packageId || `com.html2apk.${packageSegment(projectName)}`;
|
|
528
1050
|
elements.versionInput.value = config.version || "1.0.0";
|
|
529
1051
|
elements.modeInput.value = config.mode || "fullscreen";
|
|
1052
|
+
elements.orientationInput.value = normalizeOrientationInputValue(config.orientation);
|
|
1053
|
+
elements.minSdkVersionInput.value = String(normalizeMinSdkVersion(config.minSdkVersion || config.androidMinSdkVersion));
|
|
530
1054
|
elements.androidPlatformInput.value = config.androidPlatform || "android@15.0.0";
|
|
1055
|
+
elements.themeModeInput.value = normalizeThemeMode(config.themeMode || config.theme || (String(config.themeColor || "").toLowerCase() === "auto" ? "auto" : "fixed"));
|
|
1056
|
+
const themeColor = normalizeHexColor(config.themeColor || config.splashBackgroundColor || config.backgroundColor);
|
|
1057
|
+
elements.themeColorInput.value = themeColor;
|
|
1058
|
+
elements.themeColorTextInput.value = themeColor;
|
|
1059
|
+
elements.oneSignalAppIdInput.value = oneSignalAppIdFromConfig(config);
|
|
1060
|
+
renderPermissionOptions(Array.isArray(config.permissions) && config.permissions.length ? config.permissions : DEFAULT_PERMISSIONS);
|
|
531
1061
|
elements.iconPathInput.value = config.icon || "";
|
|
532
1062
|
elements.iconPreview.src = iconPreviewPath(config.icon || "");
|
|
533
1063
|
elements.debugInput.checked = Boolean(config.debug);
|
|
@@ -554,6 +1084,15 @@ function validateSettings() {
|
|
|
554
1084
|
if (!elements.modeInput.value) {
|
|
555
1085
|
errors.push(text("missingMode"));
|
|
556
1086
|
}
|
|
1087
|
+
if (!MIN_SDK_OPTIONS.includes(Number.parseInt(elements.minSdkVersionInput.value, 10))) {
|
|
1088
|
+
errors.push(text("invalidMinSdkVersion"));
|
|
1089
|
+
}
|
|
1090
|
+
if (!/^#[0-9a-fA-F]{6}$/.test(elements.themeColorTextInput.value.trim())) {
|
|
1091
|
+
errors.push(text("invalidThemeColor"));
|
|
1092
|
+
}
|
|
1093
|
+
if (!isValidOptionalOneSignalAppId(elements.oneSignalAppIdInput.value)) {
|
|
1094
|
+
errors.push(text("invalidOneSignalAppId"));
|
|
1095
|
+
}
|
|
557
1096
|
if (!elements.iconPathInput.value.trim()) {
|
|
558
1097
|
errors.push(text("missingIcon"));
|
|
559
1098
|
} else if (!/\.png$/i.test(elements.iconPathInput.value.trim())) {
|
|
@@ -596,7 +1135,13 @@ function renderReview() {
|
|
|
596
1135
|
[text("appName"), elements.appNameInput.value.trim()],
|
|
597
1136
|
[text("packageId"), elements.packageIdInput.value.trim()],
|
|
598
1137
|
[text("appVersion"), elements.versionInput.value.trim()],
|
|
599
|
-
[text("mode"), elements.modeInput
|
|
1138
|
+
[text("mode"), selectedOptionText(elements.modeInput)],
|
|
1139
|
+
[text("orientation"), selectedOptionText(elements.orientationInput)],
|
|
1140
|
+
[text("minSdkVersion"), selectedOptionText(elements.minSdkVersionInput)],
|
|
1141
|
+
[text("themeMode"), selectedOptionText(elements.themeModeInput)],
|
|
1142
|
+
[text("appThemeColor"), elements.themeColorTextInput.value.trim()],
|
|
1143
|
+
[text("oneSignalAppId"), elements.oneSignalAppIdInput.value.trim() || "-"],
|
|
1144
|
+
[text("androidPermissions"), selectedPermissions().join(", ")],
|
|
600
1145
|
[text("appIcon"), elements.iconPathInput.value.trim()]
|
|
601
1146
|
];
|
|
602
1147
|
|
|
@@ -830,6 +1375,13 @@ function buildOptions() {
|
|
|
830
1375
|
packageId: elements.packageIdInput.value.trim(),
|
|
831
1376
|
version: elements.versionInput.value.trim(),
|
|
832
1377
|
mode: elements.modeInput.value,
|
|
1378
|
+
orientation: elements.orientationInput.value,
|
|
1379
|
+
minSdkVersion: normalizeMinSdkVersion(elements.minSdkVersionInput.value),
|
|
1380
|
+
themeColor: normalizeHexColor(elements.themeColorTextInput.value),
|
|
1381
|
+
themeMode: normalizeThemeMode(elements.themeModeInput.value),
|
|
1382
|
+
theme: normalizeThemeMode(elements.themeModeInput.value),
|
|
1383
|
+
oneSignalAppId: normalizeOneSignalAppId(elements.oneSignalAppIdInput.value),
|
|
1384
|
+
permissions: selectedPermissions(),
|
|
833
1385
|
icon: elements.iconPathInput.value.trim(),
|
|
834
1386
|
androidPlatform: elements.androidPlatformInput.value.trim(),
|
|
835
1387
|
debug: elements.debugInput.checked,
|
|
@@ -862,6 +1414,7 @@ async function runBuildFlow() {
|
|
|
862
1414
|
return;
|
|
863
1415
|
}
|
|
864
1416
|
|
|
1417
|
+
showLogBar();
|
|
865
1418
|
state.buildRunning = true;
|
|
866
1419
|
updateActionButtons();
|
|
867
1420
|
elements.resultPanel.classList.add("hidden");
|
|
@@ -980,12 +1533,35 @@ function bindEvents() {
|
|
|
980
1533
|
appendLog(`${text("iconSelected")}: ${iconPath}`, "system");
|
|
981
1534
|
validateSettings();
|
|
982
1535
|
});
|
|
1536
|
+
elements.themeColorInput.addEventListener("input", () => {
|
|
1537
|
+
elements.themeColorTextInput.value = elements.themeColorInput.value;
|
|
1538
|
+
validateSettings();
|
|
1539
|
+
});
|
|
1540
|
+
elements.themeColorTextInput.addEventListener("input", () => {
|
|
1541
|
+
if (/^#[0-9a-fA-F]{6}$/.test(elements.themeColorTextInput.value.trim())) {
|
|
1542
|
+
elements.themeColorInput.value = elements.themeColorTextInput.value.trim();
|
|
1543
|
+
}
|
|
1544
|
+
validateSettings();
|
|
1545
|
+
});
|
|
1546
|
+
elements.modeInput.addEventListener("change", () => {
|
|
1547
|
+
const overlayInput = elements.permissionGrid.querySelector("input[value='SYSTEM_ALERT_WINDOW']");
|
|
1548
|
+
const selected = selectedPermissions();
|
|
1549
|
+
const nextPermissions = elements.modeInput.value === "floating" || !overlayInput || !overlayInput.disabled
|
|
1550
|
+
? selected
|
|
1551
|
+
: selected.filter((permission) => permission !== "SYSTEM_ALERT_WINDOW");
|
|
1552
|
+
renderPermissionOptions(nextPermissions);
|
|
1553
|
+
validateSettings();
|
|
1554
|
+
});
|
|
1555
|
+
elements.permissionGrid.addEventListener("change", validateSettings);
|
|
983
1556
|
[
|
|
984
1557
|
elements.appNameInput,
|
|
985
1558
|
elements.packageIdInput,
|
|
986
1559
|
elements.versionInput,
|
|
987
|
-
elements.
|
|
1560
|
+
elements.orientationInput,
|
|
1561
|
+
elements.minSdkVersionInput,
|
|
988
1562
|
elements.androidPlatformInput,
|
|
1563
|
+
elements.themeModeInput,
|
|
1564
|
+
elements.oneSignalAppIdInput,
|
|
989
1565
|
elements.debugInput,
|
|
990
1566
|
elements.releaseInput
|
|
991
1567
|
].forEach((input) => {
|
|
@@ -1021,6 +1597,11 @@ function bindEvents() {
|
|
|
1021
1597
|
elements.resultPanel.classList.add("hidden");
|
|
1022
1598
|
goToReview();
|
|
1023
1599
|
});
|
|
1600
|
+
elements.devInstagramButton.addEventListener("click", () => {
|
|
1601
|
+
api.openExternalUrl(DEVELOPER_INSTAGRAM_URL).catch(() => {
|
|
1602
|
+
setStatus("error", "Nao foi possivel abrir o Instagram.");
|
|
1603
|
+
});
|
|
1604
|
+
});
|
|
1024
1605
|
|
|
1025
1606
|
elements.dropZone.addEventListener("dragover", (event) => {
|
|
1026
1607
|
event.preventDefault();
|