html2apk 0.8.0 → 0.11.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 +340 -4
- package/package.json +15 -4
- package/src/desktop/main.js +902 -0
- package/src/desktop/preload.js +1 -0
- package/src/desktop/renderer/index.html +12 -6
- package/src/desktop/renderer/renderer.js +1164 -59
- package/src/desktop/renderer/styles.css +199 -30
- package/src/runtime-manager/index.js +79 -17
- package/src/templates/cordova-plugin-html2apk-bridge/plugin.xml +50 -0
- package/src/templates/cordova-plugin-html2apk-bridge/src/android/FloatingIconService.java +33 -0
- package/src/templates/cordova-plugin-html2apk-bridge/src/android/Html2ApkBridge.java +5929 -403
- package/src/templates/cordova-plugin-html2apk-bridge/src/android/xml/html2apk_file_paths.xml +6 -0
- package/src/templates/cordova-plugin-html2apk-bridge/www/html2apk-bridge.js +882 -4
- package/src/templates/html2apk-early-bridge.js +879 -4
- package/src/templates/html2apk-runtime-console.js +321 -29
- package/examples/minimal/dist/MeuApp-1.0.0-debug.apk +0 -0
- package/examples/minimal/dist/MeuApp-1.0.0-release.aab +0 -0
|
@@ -105,6 +105,35 @@ const i18n = {
|
|
|
105
105
|
codesEyebrow: "Bridge nativa",
|
|
106
106
|
codesTitle: "Codigos interpretados",
|
|
107
107
|
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.",
|
|
108
|
+
testNativeFunctions: "Testar funcoes",
|
|
109
|
+
functionLabRunning: "Gerando app de teste e abrindo no USB",
|
|
110
|
+
functionLabOk: "App de teste aberto no celular",
|
|
111
|
+
functionLabFail: "Nao foi possivel testar as funcoes",
|
|
112
|
+
functionLabProject: "App de teste criado",
|
|
113
|
+
functionLabSettings: "Configuracao de teste pronta",
|
|
114
|
+
functionLabUsbCheck: "Conferindo celular USB",
|
|
115
|
+
functionLabSuccessTitle: "App de teste aberto no celular",
|
|
116
|
+
functionLabSuccessText: "Use o celular conectado para experimentar as funcoes interpretadas e ver os retornos no proprio app.",
|
|
117
|
+
codesAll: "Tudo",
|
|
118
|
+
codesAllText: "Todas as funcoes interpretadas disponiveis no APK.",
|
|
119
|
+
codesFeedback: "Feedback",
|
|
120
|
+
codesFeedbackText: "Mensagens rapidas, vibracao e retornos simples.",
|
|
121
|
+
codesNotifications: "Notificacoes",
|
|
122
|
+
codesNotificationsText: "Notificacoes locais, agendamentos, loops e push remoto.",
|
|
123
|
+
codesPermissionsEvents: "Permissoes e eventos",
|
|
124
|
+
codesPermissionsEventsText: "Permissoes manuais, ciclo de vida, deep links e listeners nativos.",
|
|
125
|
+
codesMedia: "Midia e hardware",
|
|
126
|
+
codesMediaText: "Camera, QR Code, microfone, lanterna, imagens e videos.",
|
|
127
|
+
codesFiles: "Arquivos e dados",
|
|
128
|
+
codesFilesText: "Seletores, salvamento, CRUD interno, downloads e abertura de arquivos.",
|
|
129
|
+
codesShareNav: "Compartilhar e abrir",
|
|
130
|
+
codesShareNavText: "Compartilhamento, clipboard, URLs, WhatsApp, discador e mapas.",
|
|
131
|
+
codesDevice: "Tela e diagnostico",
|
|
132
|
+
codesDeviceText: "Tela ligada, brilho, tema, aparelho, rede, bateria, memoria e modo flutuante.",
|
|
133
|
+
codesSecurity: "Localizacao e seguranca",
|
|
134
|
+
codesSecurityText: "GPS, biometria e dados cifrados pelo Android Keystore.",
|
|
135
|
+
codesShowing: "Mostrando",
|
|
136
|
+
codesItems: "funcoes",
|
|
108
137
|
javaLabel: "Motor nativo",
|
|
109
138
|
doesLabel: "O que faz",
|
|
110
139
|
whenUseLabel: "Quando usar",
|
|
@@ -291,6 +320,35 @@ const i18n = {
|
|
|
291
320
|
codesEyebrow: "Native bridge",
|
|
292
321
|
codesTitle: "Interpreted code",
|
|
293
322
|
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.",
|
|
323
|
+
testNativeFunctions: "Test functions",
|
|
324
|
+
functionLabRunning: "Generating test app and opening it over USB",
|
|
325
|
+
functionLabOk: "Test app opened on the phone",
|
|
326
|
+
functionLabFail: "Could not test the functions",
|
|
327
|
+
functionLabProject: "Test app created",
|
|
328
|
+
functionLabSettings: "Test settings ready",
|
|
329
|
+
functionLabUsbCheck: "Checking USB phone",
|
|
330
|
+
functionLabSuccessTitle: "Test app opened on the phone",
|
|
331
|
+
functionLabSuccessText: "Use the connected phone to try the interpreted functions and see the results inside the app.",
|
|
332
|
+
codesAll: "All",
|
|
333
|
+
codesAllText: "All interpreted functions available in the APK.",
|
|
334
|
+
codesFeedback: "Feedback",
|
|
335
|
+
codesFeedbackText: "Short messages, vibration and simple native feedback.",
|
|
336
|
+
codesNotifications: "Notifications",
|
|
337
|
+
codesNotificationsText: "Local notifications, schedules, loops and remote push.",
|
|
338
|
+
codesPermissionsEvents: "Permissions and events",
|
|
339
|
+
codesPermissionsEventsText: "Manual permissions, lifecycle, deep links and native listeners.",
|
|
340
|
+
codesMedia: "Media and hardware",
|
|
341
|
+
codesMediaText: "Camera, QR Code, microphone, flashlight, images and videos.",
|
|
342
|
+
codesFiles: "Files and data",
|
|
343
|
+
codesFilesText: "Pickers, saving, internal CRUD, downloads and file opening.",
|
|
344
|
+
codesShareNav: "Share and open",
|
|
345
|
+
codesShareNavText: "Sharing, clipboard, URLs, WhatsApp, dialer and maps.",
|
|
346
|
+
codesDevice: "Screen and diagnostics",
|
|
347
|
+
codesDeviceText: "Keep awake, brightness, theme, device, network, battery, memory and floating mode.",
|
|
348
|
+
codesSecurity: "Location and security",
|
|
349
|
+
codesSecurityText: "GPS, biometrics and Android Keystore encrypted data.",
|
|
350
|
+
codesShowing: "Showing",
|
|
351
|
+
codesItems: "functions",
|
|
294
352
|
javaLabel: "Native engine",
|
|
295
353
|
doesLabel: "What it does",
|
|
296
354
|
whenUseLabel: "When to use",
|
|
@@ -408,6 +466,11 @@ const permissionOptions = [
|
|
|
408
466
|
label: { pt: "Vibracao", en: "Vibration" },
|
|
409
467
|
detail: { pt: "Permite vibrar", en: "Allows vibration" }
|
|
410
468
|
},
|
|
469
|
+
{
|
|
470
|
+
value: "SET_WALLPAPER",
|
|
471
|
+
label: { pt: "Papel de parede", en: "Wallpaper" },
|
|
472
|
+
detail: { pt: "Permite definir imagem estatica", en: "Allows setting a static image" }
|
|
473
|
+
},
|
|
411
474
|
{
|
|
412
475
|
value: "CAMERA",
|
|
413
476
|
label: { pt: "Camera", en: "Camera" },
|
|
@@ -431,10 +494,23 @@ const permissionOptions = [
|
|
|
431
494
|
{
|
|
432
495
|
value: "SYSTEM_ALERT_WINDOW",
|
|
433
496
|
label: { pt: "Sobrepor apps", en: "Draw over apps" },
|
|
434
|
-
detail: { pt: "Necessaria
|
|
497
|
+
detail: { pt: "Necessaria para icone flutuante", en: "Required for floating icon" }
|
|
435
498
|
}
|
|
436
499
|
];
|
|
437
500
|
|
|
501
|
+
const nativeCodeCategories = [
|
|
502
|
+
{ id: "all", title: { pt: "Tudo", en: "All" }, description: { pt: "Todas as funcoes interpretadas disponiveis no APK.", en: "All interpreted functions available in the APK." } },
|
|
503
|
+
{ id: "feedback", title: { pt: "Feedback", en: "Feedback" }, description: { pt: "Mensagens rapidas, vibracao e retornos simples.", en: "Short messages, vibration and simple native feedback." } },
|
|
504
|
+
{ id: "notifications", title: { pt: "Notificacoes", en: "Notifications" }, description: { pt: "Notificacoes locais, agendamentos, loops e push remoto.", en: "Local notifications, schedules, loops and remote push." } },
|
|
505
|
+
{ id: "permissions", title: { pt: "Permissoes e eventos", en: "Permissions and events" }, description: { pt: "Permissoes manuais, ciclo de vida, deep links e listeners nativos.", en: "Manual permissions, lifecycle, deep links and native listeners." } },
|
|
506
|
+
{ id: "media", title: { pt: "Midia e hardware", en: "Media and hardware" }, description: { pt: "Camera, QR Code, microfone, lanterna, imagens e videos.", en: "Camera, QR Code, microphone, flashlight, images and videos." } },
|
|
507
|
+
{ id: "wallpaper", title: { pt: "Papel de parede", en: "Wallpaper" }, description: { pt: "Imagem estatica na tela inicial/bloqueio e atalho para ajustes nativos.", en: "Static images for home/lock screen and shortcut to native settings." } },
|
|
508
|
+
{ id: "files", title: { pt: "Arquivos e dados", en: "Files and data" }, description: { pt: "Seletores, salvamento, CRUD interno, downloads e abertura de arquivos.", en: "Pickers, saving, internal CRUD, downloads and file opening." } },
|
|
509
|
+
{ id: "share", title: { pt: "Compartilhar e abrir", en: "Share and open" }, description: { pt: "Compartilhamento, clipboard, URLs, WhatsApp, discador e mapas.", en: "Sharing, clipboard, URLs, WhatsApp, dialer and maps." } },
|
|
510
|
+
{ id: "device", title: { pt: "Tela e diagnostico", en: "Screen and diagnostics" }, description: { pt: "Tela ligada, brilho, tema, aparelho, rede, bateria, memoria e modo flutuante.", en: "Keep awake, brightness, theme, device, network, battery, memory and floating mode." } },
|
|
511
|
+
{ id: "security", title: { pt: "Localizacao e seguranca", en: "Location and security" }, description: { pt: "GPS, biometria e dados cifrados pelo Android Keystore.", en: "GPS, biometrics and Android Keystore encrypted data." } }
|
|
512
|
+
];
|
|
513
|
+
|
|
438
514
|
const nativeCodeEntries = [
|
|
439
515
|
{
|
|
440
516
|
syntax: { pt: "toast('Mensagem')", en: "toast('Message')" },
|
|
@@ -450,6 +526,13 @@ const nativeCodeEntries = [
|
|
|
450
526
|
returns: { pt: "Promise<void>.", en: "Promise<void>." },
|
|
451
527
|
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." }
|
|
452
528
|
},
|
|
529
|
+
{
|
|
530
|
+
syntax: { pt: "aguardar(5000) / loading(5000)", en: "loading(5000)" },
|
|
531
|
+
java: "JavaScript timer",
|
|
532
|
+
description: { pt: "Cria uma pausa assincrona entre linhas de codigo usando Promise.", en: "Creates an asynchronous pause between code lines using a Promise." },
|
|
533
|
+
returns: { pt: "{ ok, ms } depois do intervalo.", en: "{ ok, ms } after the interval." },
|
|
534
|
+
handling: { pt: "Use com `await` para esperar sem travar a WebView. O tempo e em milissegundos.", en: "Use with `await` to wait without blocking the WebView. The time is in milliseconds." }
|
|
535
|
+
},
|
|
453
536
|
{
|
|
454
537
|
syntax: { pt: "notificar({ titulo, texto, aoClicar?, acoes?, open? })", en: "notify({ title, text, onClick?, actions?, open? })" },
|
|
455
538
|
java: "notify",
|
|
@@ -495,7 +578,7 @@ const nativeCodeEntries = [
|
|
|
495
578
|
{
|
|
496
579
|
syntax: { pt: "aoEvento('app:background', fn)", en: "onEvent('app:background', fn)" },
|
|
497
580
|
java: "dispatchEvent",
|
|
498
|
-
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." },
|
|
581
|
+
description: { pt: "Escuta eventos nativos: app:pronto, app:background, app:voltou, botao:voltar, link:aberto, rede:mudou, bateria:mudou, usb:conectado, fone:conectado, volume:mudou, teclado:abriu, orientacao:mudou, celular:sacudido, print:tela, nfc:recebido, notificacao:recebida e notificacao:clicada.", en: "Listens to native events: app:ready, app:background, app:resumed, back:button, link:opened, network:changed, battery:changed, usb:connected, headphone:connected, volume:changed, keyboard:opened, orientation:changed, phone:shaken, screenshot:taken, nfc:received, notification:received and notification:clicked." },
|
|
499
582
|
returns: { pt: "Funcao para cancelar a escuta.", en: "Unsubscribe function." },
|
|
500
583
|
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." }
|
|
501
584
|
},
|
|
@@ -506,6 +589,90 @@ const nativeCodeEntries = [
|
|
|
506
589
|
returns: { pt: "Funcao para cancelar a escuta.", en: "Unsubscribe function." },
|
|
507
590
|
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." }
|
|
508
591
|
},
|
|
592
|
+
{
|
|
593
|
+
syntax: { pt: "aoConectarUSB(fn) / aoDesconectarUSB(fn)", en: "onUSBConnect(fn) / onUSBDisconnect(fn)" },
|
|
594
|
+
java: "UsbManager + BatteryManager",
|
|
595
|
+
description: { pt: "Escuta conexão/desconexão USB por energia USB ou dispositivo USB/OTG anexado ao Android.", en: "Listens for USB connect/disconnect from USB power or USB/OTG devices attached to Android." },
|
|
596
|
+
returns: { pt: "Callback recebe { conectado, origem, dispositivo? }. Retorna função para cancelar.", en: "Callback receives { connected, source, device? }. Returns an unsubscribe function." },
|
|
597
|
+
handling: { pt: "Para cabo no computador, o evento vem como origem `power`; para OTG, vem com dados do dispositivo quando o Android entregar.", en: "For a computer cable, the event source is `power`; for OTG, device data is included when Android provides it." }
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
syntax: { pt: "aoConectarFone(fn) / aoDesconectarFone(fn)", en: "onHeadphoneConnect(fn) / onHeadphoneDisconnect(fn)" },
|
|
601
|
+
java: "AudioManager",
|
|
602
|
+
description: { pt: "Escuta fone com fio, Bluetooth, USB headset e aparelhos de áudio reconhecidos pelo Android.", en: "Listens for wired, Bluetooth, USB headset and other Android audio output devices." },
|
|
603
|
+
returns: { pt: "Callback recebe { conectado, dispositivo? }. Retorna função para cancelar.", en: "Callback receives { connected, device? }. Returns an unsubscribe function." },
|
|
604
|
+
handling: { pt: "Bluetooth pode chegar pelo callback de áudio quando vira saída de som; pare a escuta ao sair da tela.", en: "Bluetooth may arrive through the audio callback when it becomes an output device; stop listening when the screen unmounts." }
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
syntax: { pt: "aoMudarVolume(fn)", en: "onVolumeChange(fn)" },
|
|
608
|
+
java: "AudioManager + Settings Observer",
|
|
609
|
+
description: { pt: "Escuta mudanças nos volumes de mídia, toque, notificação, alarme e chamada.", en: "Listens for changes in media, ring, notification, alarm and voice-call volumes." },
|
|
610
|
+
returns: { pt: "Callback recebe volumes atuais e máximos por stream. Retorna função para cancelar.", en: "Callback receives current and max volumes by stream. Returns an unsubscribe function." },
|
|
611
|
+
handling: { pt: "O Android pode agrupar streams dependendo do modo de som do aparelho; trate como estado atual, não como histórico completo.", en: "Android may group streams depending on device sound mode; treat it as current state, not complete history." }
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
syntax: { pt: "volumeAtual() / definirVolume('midia', 0.5)", en: "getVolume() / setVolume('music', 0.5)" },
|
|
615
|
+
java: "AudioManager",
|
|
616
|
+
description: { pt: "Consulta e controla volumes de mídia, toque, notificação, alarme, chamada e sistema.", en: "Reads and controls media, ring, notification, alarm, voice-call and system volumes." },
|
|
617
|
+
returns: { pt: "`volumeAtual` retorna volumes atuais/máximos por stream. `definirVolume`, `aumentarVolume` e `diminuirVolume` retornam o novo estado.", en: "`getVolume` returns current/max volumes by stream. `setVolume`, `increaseVolume` and `decreaseVolume` return the new state." },
|
|
618
|
+
handling: { pt: "Use valores entre 0 e 1 para porcentagem ou inteiros para passos absolutos. Passe `{ mostrarUI: true }` se quiser exibir a barra nativa.", en: "Use values from 0 to 1 for percentage or integers for absolute steps. Pass `{ showUi: true }` to show the native volume panel." }
|
|
619
|
+
},
|
|
620
|
+
{
|
|
621
|
+
syntax: { pt: "aoAbrirTeclado(fn) / aoFecharTeclado(fn)", en: "onKeyboardOpen(fn) / onKeyboardClose(fn)" },
|
|
622
|
+
java: "ViewTreeObserver",
|
|
623
|
+
description: { pt: "Detecta abertura/fechamento do teclado pela área visível da WebView.", en: "Detects keyboard open/close through the visible WebView area." },
|
|
624
|
+
returns: { pt: "Callback recebe { aberto, alturaTeclado }. Retorna função para cancelar.", en: "Callback receives { open, keyboardHeight }. Returns an unsubscribe function." },
|
|
625
|
+
handling: { pt: "É uma heurística visual; modo tela cheia, teclado flutuante ou fabricante podem alterar a altura detectada.", en: "This is a visual heuristic; fullscreen mode, floating keyboards or vendors may change detected height." }
|
|
626
|
+
},
|
|
627
|
+
{
|
|
628
|
+
syntax: { pt: "aoMudarOrientacao(fn)", en: "onOrientationChange(fn)" },
|
|
629
|
+
java: "ViewTreeObserver + Configuration",
|
|
630
|
+
description: { pt: "Escuta troca entre portrait e landscape enquanto a WebView muda de tamanho.", en: "Listens for portrait/landscape changes while the WebView changes size." },
|
|
631
|
+
returns: { pt: "Callback recebe { orientacao, largura, altura }. Retorna função para cancelar.", en: "Callback receives { orientation, width, height }. Returns an unsubscribe function." },
|
|
632
|
+
handling: { pt: "Se o app travar orientação no config, o evento naturalmente não muda.", en: "If the app locks orientation in config, the event naturally does not change." }
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
syntax: { pt: "aoSacudirCelular(fn) / aoVirarCelularParaBaixo(fn)", en: "onPhoneShake(fn) / onPhoneFaceDown(fn)" },
|
|
636
|
+
java: "SensorManager",
|
|
637
|
+
description: { pt: "Escuta acelerômetro para detectar sacudida forte e tela virada para baixo.", en: "Uses the accelerometer to detect a strong shake and face-down posture." },
|
|
638
|
+
returns: { pt: "Callback recebe leituras x/y/z e força quando fizer sentido. Retorna função para cancelar.", en: "Callback receives x/y/z readings and force when relevant. Returns an unsubscribe function." },
|
|
639
|
+
handling: { pt: "Sensores variam por aparelho; use para interações leves e sempre mantenha um botão alternativo.", en: "Sensors vary by device; use for lightweight interactions and always keep a button fallback." }
|
|
640
|
+
},
|
|
641
|
+
{
|
|
642
|
+
syntax: { pt: "aoAproximarObjeto(fn)", en: "onProximityNear(fn)" },
|
|
643
|
+
java: "SensorManager",
|
|
644
|
+
description: { pt: "Escuta o sensor de proximidade quando algo se aproxima da tela.", en: "Listens to the proximity sensor when something gets near the screen." },
|
|
645
|
+
returns: { pt: "Callback recebe { perto, distancia, alcanceMaximo }. Retorna função para cancelar.", en: "Callback receives { near, distance, maximumRange }. Returns an unsubscribe function." },
|
|
646
|
+
handling: { pt: "Nem todo aparelho tem sensor de proximidade; trate ausência simplesmente como evento que nunca dispara.", en: "Not every device has a proximity sensor; treat absence as an event that simply never fires." }
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
syntax: { pt: "aoTirarPrint(fn)", en: "onScreenshot(fn)" },
|
|
650
|
+
java: "MediaStore Observer",
|
|
651
|
+
description: { pt: "Tenta detectar captura de tela observando novas imagens com nome/pasta de screenshot.", en: "Tries to detect screenshots by observing new images with screenshot-like names/folders." },
|
|
652
|
+
returns: { pt: "Callback recebe { uri, nome, caminho? }. Retorna função para cancelar.", en: "Callback receives { uri, name, path? }. Returns an unsubscribe function." },
|
|
653
|
+
handling: { pt: "Android moderno limita leitura de midia; alguns fabricantes mudam o nome da pasta, então esse evento é melhor esforço.", en: "Modern Android limits media reads; some vendors rename folders, so this event is best-effort." }
|
|
654
|
+
},
|
|
655
|
+
{
|
|
656
|
+
syntax: { pt: "capturarTela() / tirarPrint()", en: "captureScreen() / takeScreenshot()" },
|
|
657
|
+
java: "View.draw + Bitmap",
|
|
658
|
+
description: { pt: "Captura a tela atual do próprio app/WebView e devolve imagem em base64.", en: "Captures the current app/WebView screen and returns a base64 image." },
|
|
659
|
+
returns: { pt: "{ base64, dataUrl, width, height, mimeType, formato }.", en: "{ base64, dataUrl, width, height, mimeType, format }." },
|
|
660
|
+
handling: { pt: "Não captura outros apps nem áreas protegidas do sistema. Use depois da tela renderizar para evitar imagem vazia.", en: "Does not capture other apps or protected system areas. Call it after the screen renders to avoid an empty image." }
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
syntax: { pt: "aoNFC(fn)", en: "onNFC(fn)" },
|
|
664
|
+
java: "NfcAdapter",
|
|
665
|
+
description: { pt: "Escuta tags NFC enquanto o app está aberto em primeiro plano.", en: "Listens for NFC tags while the app is open in the foreground." },
|
|
666
|
+
returns: { pt: "Callback recebe { id, tecnologias, mensagens }. Retorna função para cancelar.", en: "Callback receives { id, technologies, messages }. Returns an unsubscribe function." },
|
|
667
|
+
handling: { pt: "Exige aparelho com NFC ligado. Tags que abrem o app enquanto ele estava fechado podem precisar de fluxo inicial em uma evolução futura.", en: "Requires a device with NFC enabled. Tags that launch the app from closed state may need an initial-flow helper in a future iteration." }
|
|
668
|
+
},
|
|
669
|
+
{
|
|
670
|
+
syntax: { pt: "aoReceberNotificacao(fn)", en: "onNotificationReceived(fn)" },
|
|
671
|
+
java: "dispatchEvent",
|
|
672
|
+
description: { pt: "Escuta quando uma notificação local do app é emitida pela bridge.", en: "Listens when a local app notification is emitted by the bridge." },
|
|
673
|
+
returns: { pt: "Callback recebe os dados da notificação. Retorna função para cancelar.", en: "Callback receives the notification data. Returns an unsubscribe function." },
|
|
674
|
+
handling: { pt: "Use para atualizar tela/estado quando `notificar()` ou uma notificacao agendada passar pela bridge. Para clique, use `aoClicarNotificacao()`.", en: "Use it to update UI/state when `notify()` or a scheduled notification goes through the bridge. For clicks, use `onNotificationClick()`." }
|
|
675
|
+
},
|
|
509
676
|
{
|
|
510
677
|
syntax: { pt: "obterLinkInicial() / aoAbrirLink(fn)", en: "getInitialLink() / onOpenLink(fn)" },
|
|
511
678
|
java: "getInitialLink",
|
|
@@ -543,15 +710,15 @@ const nativeCodeEntries = [
|
|
|
543
710
|
},
|
|
544
711
|
{
|
|
545
712
|
syntax: { pt: "escolherImagem()", en: "pickImage()" },
|
|
546
|
-
java: "
|
|
547
|
-
description: { pt: "Abre o
|
|
713
|
+
java: "Photo Picker / ACTION_OPEN_DOCUMENT",
|
|
714
|
+
description: { pt: "Abre o Photo Picker moderno no Android 13+ e usa SAF automaticamente nos Androids antigos.", en: "Opens the modern Photo Picker on Android 13+ and automatically falls back to SAF on older Android versions." },
|
|
548
715
|
returns: { pt: "{ uri, name, nome, size, tamanho, mimeType } ou null.", en: "{ uri, name, nome, size, tamanho, mimeType } or null." },
|
|
549
|
-
handling: { pt: "
|
|
716
|
+
handling: { pt: "Nao pede permissao ampla de armazenamento quando o Photo Picker esta disponivel. Use `uri`, nao caminho absoluto.", en: "Does not request broad storage permission when Photo Picker is available. Use `uri`, not an absolute file path." }
|
|
550
717
|
},
|
|
551
718
|
{
|
|
552
|
-
syntax: { pt: "escolherImagens({
|
|
553
|
-
java: "
|
|
554
|
-
description: { pt: "Abre
|
|
719
|
+
syntax: { pt: "escolherImagens({ multiplas: true })", en: "pickImages({ multiple: true })" },
|
|
720
|
+
java: "Photo Picker / ACTION_OPEN_DOCUMENT",
|
|
721
|
+
description: { pt: "Abre selecao multipla de imagens usando Photo Picker moderno quando possivel.", en: "Opens multiple image selection using the modern Photo Picker when possible." },
|
|
555
722
|
returns: { pt: "Array de arquivos; vazio se o usuario cancelar.", en: "Array of files; empty when the user cancels." },
|
|
556
723
|
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." }
|
|
557
724
|
},
|
|
@@ -580,7 +747,7 @@ const nativeCodeEntries = [
|
|
|
580
747
|
syntax: { pt: "escolherPasta()", en: "pickFolder()" },
|
|
581
748
|
java: "pickFolder",
|
|
582
749
|
description: { pt: "Abre o seletor nativo de pasta quando o Android permitir.", en: "Opens the native folder picker when Android allows it." },
|
|
583
|
-
returns: { pt: "{ uri } ou objeto vazio se cancelar.", en: "{ uri } or an empty object when canceled." },
|
|
750
|
+
returns: { pt: "{ uri, nome } ou objeto vazio se cancelar.", en: "{ uri, name } or an empty object when canceled." },
|
|
584
751
|
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." }
|
|
585
752
|
},
|
|
586
753
|
{
|
|
@@ -591,11 +758,60 @@ const nativeCodeEntries = [
|
|
|
591
758
|
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`." }
|
|
592
759
|
},
|
|
593
760
|
{
|
|
594
|
-
syntax: { pt: "compartilhar({ texto, url })", en: "share({ text, url })" },
|
|
761
|
+
syntax: { pt: "compartilhar({ texto, url, arquivo, arquivos })", en: "share({ text, url, file, files })" },
|
|
595
762
|
java: "share",
|
|
596
|
-
description: { pt: "Abre a folha nativa
|
|
597
|
-
returns: { pt: "
|
|
598
|
-
handling: { pt: "
|
|
763
|
+
description: { pt: "Abre a folha nativa para texto, link, imagem, video, PDF, arquivo unico ou multiplos arquivos.", en: "Opens the native share sheet for text, link, image, video, PDF, one file or multiple files." },
|
|
764
|
+
returns: { pt: "{ ok, shared, items, mimeType }.", en: "{ ok, shared, items, mimeType }." },
|
|
765
|
+
handling: { pt: "Aceita objeto retornado por `escolherArquivo()`, URI ou nome salvo no armazenamento do app.", en: "Accepts an object returned by `pickFile()`, a URI or a file name saved in app storage." }
|
|
766
|
+
},
|
|
767
|
+
{
|
|
768
|
+
syntax: { pt: "aoReceberCompartilhamento(callback)", en: "onShareReceived(callback)" },
|
|
769
|
+
java: "ACTION_SEND / ACTION_SEND_MULTIPLE",
|
|
770
|
+
description: { pt: "Permite que o app criado apareca no menu Compartilhar do Android e receba texto, imagem, video, PDF ou arquivo.", en: "Lets the generated app appear in Android's Share menu and receive text, image, video, PDF or file data." },
|
|
771
|
+
returns: { pt: "Callback recebe { tipo, uri, mimeType, texto, items }.", en: "Callback receives { type, uri, mimeType, text, items }." },
|
|
772
|
+
handling: { pt: "Use `obterCompartilhamentoInicial()` no boot e `aoReceberCompartilhamento()` para intents recebidas com o app aberto.", en: "Use `getInitialShare()` on boot and `onShareReceived()` for intents received while the app is open." }
|
|
773
|
+
},
|
|
774
|
+
{
|
|
775
|
+
syntax: { pt: "procurarBT() / conectarBT(id) / enviarBT(objeto) / aoDarErroBT(callback)", en: "scanBluetooth() / connectBluetooth(id) / sendBluetooth(object) / onBluetoothError(callback)" },
|
|
776
|
+
java: "Bluetooth RFCOMM",
|
|
777
|
+
description: { pt: "Comunica dois apps html2apk por Bluetooth classico usando JSON.", en: "Communicates two html2apk apps over classic Bluetooth using JSON." },
|
|
778
|
+
returns: { pt: "`procurarBT` retorna lista de dispositivos; `conectarBT` retorna o dispositivo conectado; `enviarBT` retorna { ok, enviado }; `aoDarErroBT` recebe o erro.", en: "`scanBluetooth` returns a device list; `connectBluetooth` returns the connected device; `sendBluetooth` returns { ok, sent }; `onBluetoothError` receives the error." },
|
|
779
|
+
handling: { pt: "No aparelho que recebe, registre `aoConectarBT()`, `aoReceberDadosBT()` e `aoDarErroBT()`. Para descoberta, o outro aparelho precisa estar pareado ou visivel no Bluetooth do Android.", en: "On the receiving device, register `onBluetoothConnect()`, `onBluetoothData()` and `onBluetoothError()`. For discovery, the other device must be paired or visible in Android Bluetooth." }
|
|
780
|
+
},
|
|
781
|
+
{
|
|
782
|
+
syntax: { pt: "procurarWiFi() / conectarWiFi(id) / enviarWiFi(objeto) / aoDarErroWiFi(callback)", en: "scanWiFi() / connectWiFi(id) / sendWiFi(object) / onWiFiError(callback)" },
|
|
783
|
+
java: "NSD + Socket TCP local",
|
|
784
|
+
description: { pt: "Comunica dois apps html2apk pela mesma rede Wi-Fi ou hotspot usando JSON.", en: "Communicates two html2apk apps on the same Wi-Fi network or hotspot using JSON." },
|
|
785
|
+
returns: { pt: "`procurarWiFi` retorna lista de dispositivos; `conectarWiFi` retorna o dispositivo conectado; `enviarWiFi` retorna { ok, enviado }; `aoDarErroWiFi` recebe o erro.", en: "`scanWiFi` returns a device list; `connectWiFi` returns the connected device; `sendWiFi` returns { ok, sent }; `onWiFiError` receives the error." },
|
|
786
|
+
handling: { pt: "No aparelho que recebe, registre `aoConectarWiFi()`, `aoReceberDadosWiFi()` e `aoDarErroWiFi()`. Os dois aparelhos precisam estar na mesma rede local ou hotspot.", en: "On the receiving device, register `onWiFiConnect()`, `onWiFiData()` and `onWiFiError()`. Both devices must be on the same local network or hotspot." }
|
|
787
|
+
},
|
|
788
|
+
{
|
|
789
|
+
syntax: { pt: "ocr(imagem)", en: "recognizeText(image)" },
|
|
790
|
+
java: "ML Kit TextRecognition local",
|
|
791
|
+
description: { pt: "Reconhece texto em imagem usando ML Kit local, sem enviar dados para servidor.", en: "Recognizes text in an image using local ML Kit, without sending data to a server." },
|
|
792
|
+
returns: { pt: "{ texto, text, offline, blocks }.", en: "{ texto, text, offline, blocks }." },
|
|
793
|
+
handling: { pt: "Aceita objeto de `escolherImagem()`, URI, base64 ou nome salvo. O modelo latino atende portugues.", en: "Accepts a `pickImage()` object, URI, base64 or saved file name. The Latin model supports Portuguese." }
|
|
794
|
+
},
|
|
795
|
+
{
|
|
796
|
+
syntax: { pt: "falar('Ola', { idioma: 'pt-BR', velocidade: 1 }) / pararFala()", en: "speak('Hello', { language: 'en-US', speed: 1 }) / stopSpeaking()" },
|
|
797
|
+
java: "TextToSpeech",
|
|
798
|
+
description: { pt: "Usa o motor TTS instalado no Android para falar texto.", en: "Uses Android's installed TTS engine to speak text." },
|
|
799
|
+
returns: { pt: "{ ok, speaking, idioma, velocidade }.", en: "{ ok, speaking, language, speed }." },
|
|
800
|
+
handling: { pt: "Se o idioma nao estiver instalado/suportado, a Promise rejeita. Use `pararFala()` para interromper.", en: "If the language is not installed/supported, the Promise rejects. Use `stopSpeaking()` to interrupt." }
|
|
801
|
+
},
|
|
802
|
+
{
|
|
803
|
+
syntax: { pt: "ouvir({ idioma: 'pt-BR' })", en: "recognizeSpeech({ language: 'en-US' })" },
|
|
804
|
+
java: "RecognizerIntent",
|
|
805
|
+
description: { pt: "Abre o reconhecimento de voz nativo do Android e retorna o texto reconhecido.", en: "Opens Android native speech recognition and returns recognized text." },
|
|
806
|
+
returns: { pt: "{ texto, resultados, confidence }.", en: "{ text, results, confidence }." },
|
|
807
|
+
handling: { pt: "Use `idioma: 'auto'` ou omita idioma para deixar o Android escolher. Depende do servico de voz disponivel no aparelho.", en: "Use `language: 'auto'` or omit language to let Android choose. Depends on the voice service available on the device." }
|
|
808
|
+
},
|
|
809
|
+
{
|
|
810
|
+
syntax: { pt: "share_me() / compartilharApp()", en: "share_me() / shareApp()" },
|
|
811
|
+
java: "shareCurrentApp",
|
|
812
|
+
description: { pt: "Compartilha o APK do proprio app aberto usando a folha nativa do Android.", en: "Shares the APK of the currently open app through the native Android share sheet." },
|
|
813
|
+
returns: { pt: "{ shared, name, uri, size, packageName, splitApks, installableAsSingleApk }.", en: "{ shared, name, uri, size, packageName, splitApks, installableAsSingleApk }." },
|
|
814
|
+
handling: { pt: "Funciona melhor em APK unico gerado pelo html2apk. Se o app veio de AAB/loja com split APKs, o retorno avisa que compartilhar apenas o APK base pode nao reinstalar tudo.", en: "Works best with a single APK generated by html2apk. If the app came from an AAB/store with split APKs, the return warns that sharing only the base APK may not reinstall everything." }
|
|
599
815
|
},
|
|
600
816
|
{
|
|
601
817
|
syntax: { pt: "copiarTexto('texto') / lerTextoCopiado()", en: "copyText('text') / readText()" },
|
|
@@ -663,7 +879,7 @@ const nativeCodeEntries = [
|
|
|
663
879
|
{
|
|
664
880
|
syntax: { pt: "infoRede() / infoBateria()", en: "networkInfo() / batteryInfo()" },
|
|
665
881
|
java: "networkInfo/batteryInfo",
|
|
666
|
-
description: { pt: "Consulta
|
|
882
|
+
description: { pt: "Consulta conexão atual e bateria.", en: "Reads current connection and battery." },
|
|
667
883
|
returns: { pt: "rede: { online, tipo/type }; bateria: { level, charging }.", en: "network: { online, tipo/type }; battery: { level, charging }." },
|
|
668
884
|
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." }
|
|
669
885
|
},
|
|
@@ -698,12 +914,185 @@ const nativeCodeEntries = [
|
|
|
698
914
|
{
|
|
699
915
|
syntax: { pt: "iniciarIconeFlutuante() / pararIconeFlutuante()", en: "startFloatingIcon() / stopFloatingIcon()" },
|
|
700
916
|
java: "FloatingIconService",
|
|
701
|
-
description: { pt: "Controla o
|
|
702
|
-
returns: { pt: "
|
|
703
|
-
handling: { pt: "`iniciarIconeFlutuante()` abre a tela de
|
|
917
|
+
description: { pt: "Controla o ícone flutuante e permite ajustar opacidade quando a sobreposição estiver liberada no Android.", en: "Controls the floating icon and allows opacity changes when draw-over-apps is allowed on Android." },
|
|
918
|
+
returns: { pt: "{ granted, requiresSettings, opacity } para iniciar/configurar; `pararIconeFlutuante` finaliza o serviço.", en: "{ granted, requiresSettings, opacity } for start/configure; `stopFloatingIcon` stops the service." },
|
|
919
|
+
handling: { pt: "`iniciarIconeFlutuante()` abre a tela de permissão automaticamente se faltar sobreposição. Use `definirOpacidadeIconeFlutuante(0.6)` para mudar sem recriar outro fluxo.", en: "`startFloatingIcon()` opens the permission screen automatically if draw-over-apps is missing. Use `setFloatingIconOpacity(0.6)` to change it without creating another flow." }
|
|
920
|
+
},
|
|
921
|
+
{
|
|
922
|
+
syntax: { pt: "minimizarApp() / fecharApp()", en: "minimizeApp() / closeApp()" },
|
|
923
|
+
java: "Activity",
|
|
924
|
+
description: { pt: "Envia o app para segundo plano ou fecha a Activity atual.", en: "Sends the app to the background or closes the current Activity." },
|
|
925
|
+
returns: { pt: "`minimizarApp`: { minimizado }. `fecharApp`: { fechado } antes de finalizar.", en: "`minimizeApp`: { minimized }. `closeApp`: { closed } before finishing." },
|
|
926
|
+
handling: { pt: "Use com uma ação clara do usuário. `fecharApp()` encerra a tela do APK, então salve estado antes.", en: "Use from an explicit user action. `closeApp()` finishes the APK screen, so save state first." }
|
|
927
|
+
},
|
|
928
|
+
{
|
|
929
|
+
syntax: { pt: "tirarFoto() / capturarVideo()", en: "takePhoto() / captureVideo()" },
|
|
930
|
+
java: "ACTION_IMAGE_CAPTURE / ACTION_VIDEO_CAPTURE",
|
|
931
|
+
description: { pt: "Abre a camera nativa para capturar foto ou video e retorna um arquivo app-scoped.", en: "Opens the native camera to capture a photo or video and returns an app-scoped file." },
|
|
932
|
+
returns: { pt: "{ uri, name, size, mimeType, kind }. Com `{ base64: true }`, tambem retorna base64.", en: "{ uri, name, size, mimeType, kind }. With `{ base64: true }`, it also returns base64." },
|
|
933
|
+
handling: { pt: "A permissao de camera e pedida automaticamente. Trate objeto vazio como cancelamento do usuario.", en: "Camera permission is requested automatically. Treat an empty object as user cancellation." }
|
|
934
|
+
},
|
|
935
|
+
{
|
|
936
|
+
syntax: { pt: "escanearQRCode()", en: "scanQRCode()" },
|
|
937
|
+
java: "BarcodeDetector + camera",
|
|
938
|
+
description: { pt: "Tira uma foto e tenta ler QR Code pelo WebView quando `BarcodeDetector` estiver disponivel.", en: "Takes a photo and tries to read a QR code through the WebView when `BarcodeDetector` is available." },
|
|
939
|
+
returns: { pt: "{ text, rawValue, format, photo } ou null se nenhum QR for encontrado.", en: "{ text, rawValue, format, photo } or null when no QR is found." },
|
|
940
|
+
handling: { pt: "Se o WebView do aparelho nao tiver `BarcodeDetector`, a Promise rejeita; mantenha fallback para digitar/colar o codigo.", en: "If the device WebView does not have `BarcodeDetector`, the Promise rejects; keep a fallback to type/paste the code." }
|
|
941
|
+
},
|
|
942
|
+
{
|
|
943
|
+
syntax: { pt: "salvarArquivo('nome.json', minhaVariavel) / lerArquivo('nome.json')", en: "saveFile('name.json', myValue) / readFile('name.json')" },
|
|
944
|
+
java: "app-scoped external files",
|
|
945
|
+
description: { pt: "CRUD de arquivos persistentes no armazenamento app-scoped, sem abrir seletor de documento.", en: "Persistent file CRUD in app-scoped storage, without opening a document picker." },
|
|
946
|
+
returns: { pt: "`salvarArquivo` retorna metadados; `lerArquivo` retorna o valor salvo; `listarArquivos` retorna a lista.", en: "`saveFile` returns metadata; `readFile` returns the saved value; `listFiles` returns the list." },
|
|
947
|
+
handling: { pt: "A chamada antiga `salvarArquivo({ nome, conteudo })` continua abrindo o seletor nativo. Use string no primeiro argumento para o CRUD interno.", en: "The old `saveFile({ name, content })` call still opens the native picker. Use a string first argument for internal CRUD." }
|
|
948
|
+
},
|
|
949
|
+
{
|
|
950
|
+
syntax: { pt: "baixarArquivo(url, 'foto.png', { galeria: true }) / abrirArquivo('foto.png')", en: "downloadFile(url, 'photo.png', { gallery: true }) / openFile('photo.png')" },
|
|
951
|
+
java: "HttpURLConnection + NotificationCompat",
|
|
952
|
+
description: { pt: "Baixa por URL para o armazenamento persistente do app e mostra notificacao Android com barra de progresso.", en: "Downloads from a URL to the app's persistent storage and shows an Android progress notification." },
|
|
953
|
+
returns: { pt: "`baixarArquivo` retorna metadados, tamanho, origem, notificacao e, com `{ galeria: true }`, `publicUri`.", en: "`downloadFile` returns metadata, size, source, notification state and, with `{ gallery: true }`, `publicUri`." },
|
|
954
|
+
handling: { pt: "Sem `{ galeria: true }`, imagens ficam privadas do app e nao aparecem na galeria. No Android 13+, se a permissao de notificacao for negada, o download continua.", en: "Without `{ gallery: true }`, images stay private to the app and do not appear in the gallery. On Android 13+, if notification permission is denied, the download continues." }
|
|
955
|
+
},
|
|
956
|
+
{
|
|
957
|
+
syntax: { pt: "baixarBase64('foto.png', base64, { galeria: true }) / baixarArquivoLocal(arquivo, 'copia.pdf')", en: "downloadBase64('photo.png', base64, { gallery: true }) / downloadLocalFile(file, 'copy.pdf')" },
|
|
958
|
+
java: "InputStream + NotificationCompat",
|
|
959
|
+
description: { pt: "Cria um download a partir de base64, data URL, URI/caminho de arquivo ou objeto retornado por `escolherArquivo()`.", en: "Creates a download from base64, data URL, file URI/path or an object returned by `pickFile()`." },
|
|
960
|
+
returns: { pt: "Metadados do arquivo salvo, `sourceType`, `notificationShown`, permissao de notificacao e publicacao opcional.", en: "Saved file metadata, `sourceType`, `notificationShown`, notification permission and optional public publication." },
|
|
961
|
+
handling: { pt: "Para imagem/video aparecer na galeria, passe `{ galeria: true }`. Para esconder a notificacao, passe `{ notificacao: false }`.", en: "For image/video to appear in the gallery, pass `{ gallery: true }`. To hide the notification, pass `{ notification: false }`." }
|
|
962
|
+
},
|
|
963
|
+
{
|
|
964
|
+
syntax: { pt: "obterLocalizacao() / acompanharLocalizacao()", en: "getLocation() / watchLocation()" },
|
|
965
|
+
java: "LocationManager",
|
|
966
|
+
description: { pt: "Le a localizacao atual ou inicia acompanhamento por evento `localizacao:mudou`.", en: "Reads current location or starts watching through the `location:changed` event." },
|
|
967
|
+
returns: { pt: "{ latitude, longitude, accuracy, provider } ou { watchId }.", en: "{ latitude, longitude, accuracy, provider } or { watchId }." },
|
|
968
|
+
handling: { pt: "A permissao e pedida automaticamente. Pare acompanhamento com `pararLocalizacao(watchId)` quando sair da tela.", en: "Permission is requested automatically. Stop watching with `stopLocationWatch(watchId)` when leaving the screen." }
|
|
969
|
+
},
|
|
970
|
+
{
|
|
971
|
+
syntax: { pt: "autenticarBiometria({ titulo })", en: "authenticateBiometric({ title })" },
|
|
972
|
+
java: "BiometricPrompt",
|
|
973
|
+
description: { pt: "Abre a biometria/tela segura do Android para confirmar identidade.", en: "Opens Android biometric/secure prompt to confirm identity." },
|
|
974
|
+
returns: { pt: "{ supported, authenticated, canceled, message }.", en: "{ supported, authenticated, canceled, message }." },
|
|
975
|
+
handling: { pt: "Funciona em Android 9+. Se `supported` vier falso, use PIN/senha do proprio app como fallback.", en: "Works on Android 9+. If `supported` is false, use your app's own PIN/password fallback." }
|
|
976
|
+
},
|
|
977
|
+
{
|
|
978
|
+
syntax: { pt: "salvarSeguro('token', valor) / lerSeguro('token')", en: "saveSecure('token', value) / readSecure('token')" },
|
|
979
|
+
java: "Android Keystore",
|
|
980
|
+
description: { pt: "Guarda pequenos segredos cifrados com chave do Android Keystore.", en: "Stores small secrets encrypted with an Android Keystore key." },
|
|
981
|
+
returns: { pt: "`lerSeguro` retorna o valor salvo; `listarSeguro` retorna chaves; `removerSeguro` apaga.", en: "`readSecure` returns the saved value; `listSecureKeys` returns keys; `deleteSecure` removes." },
|
|
982
|
+
handling: { pt: "Use para tokens e dados pequenos. Nao use para arquivos grandes; para isso, use o CRUD de arquivos.", en: "Use it for tokens and small data. Do not use it for large files; use file CRUD for that." }
|
|
983
|
+
},
|
|
984
|
+
{
|
|
985
|
+
syntax: { pt: "definirPapelParede('foto.jpg', { alvo: 'inicio' })", en: "setWallpaper('photo.jpg', { target: 'home' })" },
|
|
986
|
+
java: "WallpaperManager",
|
|
987
|
+
description: { pt: "Define imagem estatica como papel de parede da tela inicial, bloqueio ou ambas.", en: "Sets a static image as home, lock or both wallpapers." },
|
|
988
|
+
returns: { pt: "{ applied, systemApplied, lockApplied, lockSupported, mimeType }.", en: "{ applied, systemApplied, lockApplied, lockSupported, mimeType }." },
|
|
989
|
+
handling: { pt: "Aceita nome salvo por `salvarArquivo`, `content://`/`file://` ou `{ base64, mimeType }`. Video retorna aviso e deve seguir pelo ajuste/live wallpaper do Android.", en: "Accepts a name saved by `saveFile`, `content://`/`file://` or `{ base64, mimeType }`. Video returns a warning and must use Android settings/live wallpaper flow." },
|
|
990
|
+
recipe: {
|
|
991
|
+
when: { pt: "Para aplicar uma imagem ja salva no armazenamento do app como papel de parede inicial.", en: "To apply an image already saved in app storage as the home wallpaper." },
|
|
992
|
+
example: {
|
|
993
|
+
pt: `const resultado = await definirPapelParede("foto.jpg", {
|
|
994
|
+
alvo: "inicio"
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
if (!resultado.applied) {
|
|
998
|
+
console.log(resultado);
|
|
999
|
+
}`,
|
|
1000
|
+
en: `const result = await setWallpaper("photo.jpg", {
|
|
1001
|
+
target: "home"
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
if (!result.applied) {
|
|
1005
|
+
console.log(result);
|
|
1006
|
+
}`
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
},
|
|
1010
|
+
{
|
|
1011
|
+
syntax: { pt: "infoPapelParede() / abrirConfiguracaoPapelParede()", en: "wallpaperInfo() / openWallpaperSettings()" },
|
|
1012
|
+
java: "android.settings.WALLPAPER_SETTINGS",
|
|
1013
|
+
description: { pt: "Consulta capacidades do aparelho e abre a tela nativa para escolhas manuais.", en: "Checks device capabilities and opens the native screen for manual choices." },
|
|
1014
|
+
returns: { pt: "`infoPapelParede` retorna suporte a imagem/bloqueio/video. `abrirConfiguracaoPapelParede` retorna se a tela abriu.", en: "`wallpaperInfo` returns image/lock/video support. `openWallpaperSettings` returns whether the screen opened." },
|
|
1015
|
+
handling: { pt: "Use para casos fora do caminho simples, como video wallpaper ou aparelhos que exigem confirmacao do usuario.", en: "Use it for cases outside the simple path, such as video wallpaper or devices that require user confirmation." },
|
|
1016
|
+
recipe: {
|
|
1017
|
+
when: { pt: "Para lidar com video wallpaper ou aparelhos que pedem escolha manual do usuario.", en: "To handle video wallpaper or devices that require a manual user choice." },
|
|
1018
|
+
example: {
|
|
1019
|
+
pt: `const info = await infoPapelParede();
|
|
1020
|
+
|
|
1021
|
+
if (!info.videoSupported) {
|
|
1022
|
+
await abrirConfiguracaoPapelParede();
|
|
1023
|
+
}`,
|
|
1024
|
+
en: `const info = await wallpaperInfo();
|
|
1025
|
+
|
|
1026
|
+
if (!info.videoSupported) {
|
|
1027
|
+
await openWallpaperSettings();
|
|
1028
|
+
}`
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
704
1031
|
}
|
|
705
1032
|
];
|
|
706
1033
|
|
|
1034
|
+
[
|
|
1035
|
+
"feedback",
|
|
1036
|
+
"feedback",
|
|
1037
|
+
"feedback",
|
|
1038
|
+
"notifications",
|
|
1039
|
+
"notifications",
|
|
1040
|
+
"notifications",
|
|
1041
|
+
"notifications",
|
|
1042
|
+
"notifications",
|
|
1043
|
+
"permissions",
|
|
1044
|
+
"permissions",
|
|
1045
|
+
"permissions",
|
|
1046
|
+
"permissions",
|
|
1047
|
+
"permissions",
|
|
1048
|
+
"media",
|
|
1049
|
+
"media",
|
|
1050
|
+
"media",
|
|
1051
|
+
"media",
|
|
1052
|
+
"media",
|
|
1053
|
+
"files",
|
|
1054
|
+
"files",
|
|
1055
|
+
"media",
|
|
1056
|
+
"files",
|
|
1057
|
+
"files",
|
|
1058
|
+
"share",
|
|
1059
|
+
"share",
|
|
1060
|
+
"share",
|
|
1061
|
+
"media",
|
|
1062
|
+
"media",
|
|
1063
|
+
"media",
|
|
1064
|
+
"share",
|
|
1065
|
+
"share",
|
|
1066
|
+
"device",
|
|
1067
|
+
"device",
|
|
1068
|
+
"device",
|
|
1069
|
+
"share",
|
|
1070
|
+
"share",
|
|
1071
|
+
"share",
|
|
1072
|
+
"share",
|
|
1073
|
+
"device",
|
|
1074
|
+
"device",
|
|
1075
|
+
"device",
|
|
1076
|
+
"device",
|
|
1077
|
+
"device",
|
|
1078
|
+
"device",
|
|
1079
|
+
"device",
|
|
1080
|
+
"media",
|
|
1081
|
+
"media",
|
|
1082
|
+
"files",
|
|
1083
|
+
"files",
|
|
1084
|
+
"files",
|
|
1085
|
+
"security",
|
|
1086
|
+
"security",
|
|
1087
|
+
"security",
|
|
1088
|
+
"wallpaper",
|
|
1089
|
+
"wallpaper"
|
|
1090
|
+
].forEach((category, index) => {
|
|
1091
|
+
if (nativeCodeEntries[index]) {
|
|
1092
|
+
nativeCodeEntries[index].category = category;
|
|
1093
|
+
}
|
|
1094
|
+
});
|
|
1095
|
+
|
|
707
1096
|
const nativeCodeRecipes = [
|
|
708
1097
|
{
|
|
709
1098
|
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." },
|
|
@@ -727,6 +1116,17 @@ const nativeCodeRecipes = [
|
|
|
727
1116
|
en: `await vibrate(250);`
|
|
728
1117
|
}
|
|
729
1118
|
},
|
|
1119
|
+
{
|
|
1120
|
+
when: { pt: "Para esperar entre duas linhas sem bloquear a interface.", en: "To wait between two lines without blocking the interface." },
|
|
1121
|
+
example: {
|
|
1122
|
+
pt: `await toast("Comecando");
|
|
1123
|
+
await aguardar(5000);
|
|
1124
|
+
await toast("Continuando");`,
|
|
1125
|
+
en: `await toast("Starting");
|
|
1126
|
+
await loading(5000);
|
|
1127
|
+
await toast("Continuing");`
|
|
1128
|
+
}
|
|
1129
|
+
},
|
|
730
1130
|
{
|
|
731
1131
|
when: { pt: "Para mostrar uma notificacao simples agora. `aoClicar`, `acoes` e `open` sao opcionais.", en: "To show a simple notification now. `onClick`, `actions` and `open` are optional." },
|
|
732
1132
|
example: {
|
|
@@ -951,6 +1351,111 @@ console.log(permissions);`
|
|
|
951
1351
|
// stop();`
|
|
952
1352
|
}
|
|
953
1353
|
},
|
|
1354
|
+
{
|
|
1355
|
+
when: { pt: "Para reagir a cabo/dispositivo USB e notificacoes locais.", en: "To react to USB cable/device changes and local notifications." },
|
|
1356
|
+
example: {
|
|
1357
|
+
pt: `aoConectarUSB((dados) => {
|
|
1358
|
+
console.log("USB conectado", dados.origem, dados.dispositivo);
|
|
1359
|
+
});
|
|
1360
|
+
|
|
1361
|
+
aoDesconectarUSB(() => {
|
|
1362
|
+
console.log("USB desconectado");
|
|
1363
|
+
});
|
|
1364
|
+
|
|
1365
|
+
aoReceberNotificacao((dados) => {
|
|
1366
|
+
console.log("Notificação recebida", dados.titulo || dados.title);
|
|
1367
|
+
});`,
|
|
1368
|
+
en: `onUSBConnect((data) => {
|
|
1369
|
+
console.log("USB connected", data.source, data.device);
|
|
1370
|
+
});
|
|
1371
|
+
|
|
1372
|
+
onUSBDisconnect(() => {
|
|
1373
|
+
console.log("USB disconnected");
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1376
|
+
onNotificationReceived((data) => {
|
|
1377
|
+
console.log("Notification received", data.title);
|
|
1378
|
+
});`
|
|
1379
|
+
}
|
|
1380
|
+
},
|
|
1381
|
+
{
|
|
1382
|
+
when: { pt: "Para reagir a fone, volume, teclado e orientação da tela.", en: "To react to headphones, volume, keyboard and screen orientation." },
|
|
1383
|
+
example: {
|
|
1384
|
+
pt: `aoConectarFone((dados) => {
|
|
1385
|
+
console.log("Fone conectado", dados.dispositivo);
|
|
1386
|
+
});
|
|
1387
|
+
|
|
1388
|
+
aoMudarVolume((dados) => {
|
|
1389
|
+
console.log("Volume de mídia", dados.midia.atual);
|
|
1390
|
+
});
|
|
1391
|
+
|
|
1392
|
+
aoAbrirTeclado((dados) => {
|
|
1393
|
+
console.log("Teclado abriu", dados.alturaTeclado);
|
|
1394
|
+
});
|
|
1395
|
+
|
|
1396
|
+
aoMudarOrientacao((dados) => {
|
|
1397
|
+
console.log("Orientação", dados.orientacao);
|
|
1398
|
+
});`,
|
|
1399
|
+
en: `onHeadphoneConnect((data) => {
|
|
1400
|
+
console.log("Headphone connected", data.device);
|
|
1401
|
+
});
|
|
1402
|
+
|
|
1403
|
+
onVolumeChange((data) => {
|
|
1404
|
+
console.log("Media volume", data.music.current);
|
|
1405
|
+
});
|
|
1406
|
+
|
|
1407
|
+
onKeyboardOpen((data) => {
|
|
1408
|
+
console.log("Keyboard opened", data.keyboardHeight);
|
|
1409
|
+
});
|
|
1410
|
+
|
|
1411
|
+
onOrientationChange((data) => {
|
|
1412
|
+
console.log("Orientation", data.orientation);
|
|
1413
|
+
});`
|
|
1414
|
+
}
|
|
1415
|
+
},
|
|
1416
|
+
{
|
|
1417
|
+
when: { pt: "Para interações legais com sensores, print e NFC.", en: "For playful interactions with sensors, screenshots and NFC." },
|
|
1418
|
+
example: {
|
|
1419
|
+
pt: `aoSacudirCelular((dados) => {
|
|
1420
|
+
console.log("Sacudiu", dados.forca);
|
|
1421
|
+
});
|
|
1422
|
+
|
|
1423
|
+
aoVirarCelularParaBaixo(() => {
|
|
1424
|
+
console.log("Tela virada para baixo");
|
|
1425
|
+
});
|
|
1426
|
+
|
|
1427
|
+
aoAproximarObjeto((dados) => {
|
|
1428
|
+
console.log("Algo chegou perto", dados.distancia);
|
|
1429
|
+
});
|
|
1430
|
+
|
|
1431
|
+
aoTirarPrint((dados) => {
|
|
1432
|
+
console.log("Print detectado", dados.uri);
|
|
1433
|
+
});
|
|
1434
|
+
|
|
1435
|
+
aoNFC((dados) => {
|
|
1436
|
+
console.log("Tag NFC", dados.id, dados.mensagens);
|
|
1437
|
+
});`,
|
|
1438
|
+
en: `onPhoneShake((data) => {
|
|
1439
|
+
console.log("Shaken", data.force);
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1442
|
+
onPhoneFaceDown(() => {
|
|
1443
|
+
console.log("Phone is face down");
|
|
1444
|
+
});
|
|
1445
|
+
|
|
1446
|
+
onProximityNear((data) => {
|
|
1447
|
+
console.log("Something is near", data.distance);
|
|
1448
|
+
});
|
|
1449
|
+
|
|
1450
|
+
onScreenshot((data) => {
|
|
1451
|
+
console.log("Screenshot detected", data.uri);
|
|
1452
|
+
});
|
|
1453
|
+
|
|
1454
|
+
onNFC((data) => {
|
|
1455
|
+
console.log("NFC tag", data.id, data.messages);
|
|
1456
|
+
});`
|
|
1457
|
+
}
|
|
1458
|
+
},
|
|
954
1459
|
{
|
|
955
1460
|
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." },
|
|
956
1461
|
example: {
|
|
@@ -1083,7 +1588,7 @@ if (image) {
|
|
|
1083
1588
|
{
|
|
1084
1589
|
when: { pt: "Para galeria com selecao multipla, como anexar varias fotos.", en: "For multiple gallery selection, such as attaching many photos." },
|
|
1085
1590
|
example: {
|
|
1086
|
-
pt: `const imagens = await escolherImagens({
|
|
1591
|
+
pt: `const imagens = await escolherImagens({ multiplas: true });
|
|
1087
1592
|
|
|
1088
1593
|
for (const imagem of imagens) {
|
|
1089
1594
|
console.log(imagem.nome, imagem.tamanho);
|
|
@@ -1187,18 +1692,141 @@ if (saved.saved) {
|
|
|
1187
1692
|
}
|
|
1188
1693
|
},
|
|
1189
1694
|
{
|
|
1190
|
-
when: { pt: "Para abrir o compartilhamento nativo do Android com texto e
|
|
1695
|
+
when: { pt: "Para abrir o compartilhamento nativo do Android com texto, link e arquivos.", en: "To open Android native sharing with text, link and files." },
|
|
1191
1696
|
example: {
|
|
1192
|
-
pt: `await
|
|
1697
|
+
pt: `const imagem = await escolherImagem();
|
|
1698
|
+
|
|
1699
|
+
await compartilhar({
|
|
1193
1700
|
texto: "Veja esse app",
|
|
1194
|
-
url: "https://exemplo.com"
|
|
1701
|
+
url: "https://exemplo.com",
|
|
1702
|
+
arquivo: imagem
|
|
1195
1703
|
});`,
|
|
1196
|
-
en: `await
|
|
1704
|
+
en: `const image = await pickImage();
|
|
1705
|
+
|
|
1706
|
+
await share({
|
|
1197
1707
|
text: "Check this app",
|
|
1198
|
-
url: "https://example.com"
|
|
1708
|
+
url: "https://example.com",
|
|
1709
|
+
file: image
|
|
1199
1710
|
});`
|
|
1200
1711
|
}
|
|
1201
1712
|
},
|
|
1713
|
+
{
|
|
1714
|
+
when: { pt: "Para receber dados enviados pelo menu Compartilhar do Android.", en: "To receive data sent through Android's Share menu." },
|
|
1715
|
+
example: {
|
|
1716
|
+
pt: `aoReceberCompartilhamento((dados) => {
|
|
1717
|
+
console.log(dados.tipo, dados.uri || dados.texto);
|
|
1718
|
+
});
|
|
1719
|
+
|
|
1720
|
+
const inicial = await obterCompartilhamentoInicial();`,
|
|
1721
|
+
en: `onShareReceived((data) => {
|
|
1722
|
+
console.log(data.type, data.uri || data.text);
|
|
1723
|
+
});
|
|
1724
|
+
|
|
1725
|
+
const initial = await getInitialShare();`
|
|
1726
|
+
}
|
|
1727
|
+
},
|
|
1728
|
+
{
|
|
1729
|
+
when: { pt: "Para trocar objetos entre dois celulares com apps html2apk abertos.", en: "To exchange objects between two phones running html2apk apps." },
|
|
1730
|
+
example: {
|
|
1731
|
+
pt: `aoConectarBT((dispositivo) => {
|
|
1732
|
+
console.log("Conectado", dispositivo.nome);
|
|
1733
|
+
});
|
|
1734
|
+
|
|
1735
|
+
aoReceberDadosBT((dados) => {
|
|
1736
|
+
console.log("Recebido", dados);
|
|
1737
|
+
});
|
|
1738
|
+
|
|
1739
|
+
aoDarErroBT((erro) => {
|
|
1740
|
+
console.log("Erro Bluetooth", erro.mensagem || erro.message);
|
|
1741
|
+
});
|
|
1742
|
+
|
|
1743
|
+
const lista = await procurarBT();
|
|
1744
|
+
await conectarBT(lista[0].id);
|
|
1745
|
+
await enviarBT({ mensagem: "Ola" });`,
|
|
1746
|
+
en: `onBluetoothConnect((device) => {
|
|
1747
|
+
console.log("Connected", device.name);
|
|
1748
|
+
});
|
|
1749
|
+
|
|
1750
|
+
onBluetoothData((data) => {
|
|
1751
|
+
console.log("Received", data);
|
|
1752
|
+
});
|
|
1753
|
+
|
|
1754
|
+
onBluetoothError((error) => {
|
|
1755
|
+
console.log("Bluetooth error", error.message);
|
|
1756
|
+
});
|
|
1757
|
+
|
|
1758
|
+
const list = await scanBluetooth();
|
|
1759
|
+
await connectBluetooth(list[0].id);
|
|
1760
|
+
await sendBluetooth({ message: "Hello" });`
|
|
1761
|
+
}
|
|
1762
|
+
},
|
|
1763
|
+
{
|
|
1764
|
+
when: { pt: "Para trocar objetos entre dois celulares na mesma rede Wi-Fi ou hotspot.", en: "To exchange objects between two phones on the same Wi-Fi network or hotspot." },
|
|
1765
|
+
example: {
|
|
1766
|
+
pt: `aoConectarWiFi((dispositivo) => {
|
|
1767
|
+
console.log("Conectado por Wi-Fi", dispositivo.nome || dispositivo.host);
|
|
1768
|
+
});
|
|
1769
|
+
|
|
1770
|
+
aoReceberDadosWiFi((dados) => {
|
|
1771
|
+
console.log("Recebido por Wi-Fi", dados);
|
|
1772
|
+
});
|
|
1773
|
+
|
|
1774
|
+
aoDarErroWiFi((erro) => {
|
|
1775
|
+
console.log("Erro Wi-Fi", erro.mensagem || erro.message);
|
|
1776
|
+
});
|
|
1777
|
+
|
|
1778
|
+
const lista = await procurarWiFi();
|
|
1779
|
+
await conectarWiFi(lista[0].id);
|
|
1780
|
+
await enviarWiFi({ mensagem: "Ola por Wi-Fi" });`,
|
|
1781
|
+
en: `onWiFiConnect((device) => {
|
|
1782
|
+
console.log("Connected over Wi-Fi", device.name || device.host);
|
|
1783
|
+
});
|
|
1784
|
+
|
|
1785
|
+
onWiFiData((data) => {
|
|
1786
|
+
console.log("Received over Wi-Fi", data);
|
|
1787
|
+
});
|
|
1788
|
+
|
|
1789
|
+
onWiFiError((error) => {
|
|
1790
|
+
console.log("Wi-Fi error", error.message);
|
|
1791
|
+
});
|
|
1792
|
+
|
|
1793
|
+
const list = await scanWiFi();
|
|
1794
|
+
await connectWiFi(list[0].id);
|
|
1795
|
+
await sendWiFi({ message: "Hello over Wi-Fi" });`
|
|
1796
|
+
}
|
|
1797
|
+
},
|
|
1798
|
+
{
|
|
1799
|
+
when: { pt: "Para reconhecer texto de uma foto sem enviar a imagem para servidor.", en: "To recognize text from a photo without sending the image to a server." },
|
|
1800
|
+
example: {
|
|
1801
|
+
pt: `const imagem = await escolherImagem();
|
|
1802
|
+
const resultado = await ocr(imagem);
|
|
1803
|
+
|
|
1804
|
+
console.log(resultado.texto);`,
|
|
1805
|
+
en: `const image = await pickImage();
|
|
1806
|
+
const result = await recognizeText(image);
|
|
1807
|
+
|
|
1808
|
+
console.log(result.text);`
|
|
1809
|
+
}
|
|
1810
|
+
},
|
|
1811
|
+
{
|
|
1812
|
+
when: { pt: "Para falar texto e ouvir uma frase do usuario.", en: "To speak text and listen to a user phrase." },
|
|
1813
|
+
example: {
|
|
1814
|
+
pt: `await falar("Ola mundo", {
|
|
1815
|
+
idioma: "pt-BR",
|
|
1816
|
+
velocidade: 1
|
|
1817
|
+
});
|
|
1818
|
+
|
|
1819
|
+
const voz = await ouvir({ idioma: "pt-BR" });
|
|
1820
|
+
console.log(voz.texto);`,
|
|
1821
|
+
en: `await speak("Hello world", {
|
|
1822
|
+
language: "en-US",
|
|
1823
|
+
speed: 1
|
|
1824
|
+
});
|
|
1825
|
+
|
|
1826
|
+
const voice = await recognizeSpeech({ language: "en-US" });
|
|
1827
|
+
console.log(voice.text);`
|
|
1828
|
+
}
|
|
1829
|
+
},
|
|
1202
1830
|
{
|
|
1203
1831
|
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." },
|
|
1204
1832
|
example: {
|
|
@@ -1384,25 +2012,236 @@ for (const app of result.apps) {
|
|
|
1384
2012
|
}
|
|
1385
2013
|
},
|
|
1386
2014
|
{
|
|
1387
|
-
when: { pt: "Para
|
|
2015
|
+
when: { pt: "Para ajustar volume e capturar a tela atual do app.", en: "To adjust volume and capture the current app screen." },
|
|
1388
2016
|
example: {
|
|
1389
|
-
pt: `const
|
|
2017
|
+
pt: `const volume = await volumeAtual();
|
|
2018
|
+
console.log(volume.midia.atual, volume.midia.maximo);
|
|
2019
|
+
|
|
2020
|
+
await definirVolume("midia", 0.5, { mostrarUI: true });
|
|
2021
|
+
|
|
2022
|
+
const imagem = await capturarTela();
|
|
2023
|
+
document.querySelector("img.preview").src = imagem.dataUrl;`,
|
|
2024
|
+
en: `const volume = await getVolume();
|
|
2025
|
+
console.log(volume.music.current, volume.music.max);
|
|
2026
|
+
|
|
2027
|
+
await setVolume("music", 0.5, { showUi: true });
|
|
2028
|
+
|
|
2029
|
+
const image = await captureScreen();
|
|
2030
|
+
document.querySelector("img.preview").src = image.dataUrl;`
|
|
2031
|
+
}
|
|
2032
|
+
},
|
|
2033
|
+
{
|
|
2034
|
+
when: { pt: "Para apps que precisam mostrar/esconder o icone flutuante.", en: "For apps that need to show/hide the floating icon." },
|
|
2035
|
+
example: {
|
|
2036
|
+
pt: `const status = await iniciarIconeFlutuante({ opacidade: 0.85 });
|
|
1390
2037
|
|
|
1391
2038
|
if (status.requiresSettings) {
|
|
1392
2039
|
console.log("O Android abriu a tela de sobreposicao");
|
|
1393
2040
|
}
|
|
1394
2041
|
|
|
2042
|
+
await definirOpacidadeIconeFlutuante(0.55);
|
|
2043
|
+
|
|
1395
2044
|
// Para desligar:
|
|
1396
2045
|
// await pararIconeFlutuante();`,
|
|
1397
|
-
en: `const status = await startFloatingIcon();
|
|
2046
|
+
en: `const status = await startFloatingIcon({ opacity: 0.85 });
|
|
1398
2047
|
|
|
1399
2048
|
if (status.requiresSettings) {
|
|
1400
2049
|
console.log("Android opened the draw-over-apps screen");
|
|
1401
2050
|
}
|
|
1402
2051
|
|
|
2052
|
+
await setFloatingIconOpacity(0.55);
|
|
2053
|
+
|
|
1403
2054
|
// To turn it off:
|
|
1404
2055
|
// await stopFloatingIcon();`
|
|
1405
2056
|
}
|
|
2057
|
+
},
|
|
2058
|
+
{
|
|
2059
|
+
when: { pt: "Para capturar uma imagem ou video feito na hora pelo usuario.", en: "To capture an image or video made by the user right now." },
|
|
2060
|
+
example: {
|
|
2061
|
+
pt: `const foto = await tirarFoto({ base64: true });
|
|
2062
|
+
|
|
2063
|
+
if (foto.base64) {
|
|
2064
|
+
document.querySelector("img.preview").src =
|
|
2065
|
+
"data:" + foto.mimeType + ";base64," + foto.base64;
|
|
2066
|
+
}`,
|
|
2067
|
+
en: `const photo = await takePhoto({ base64: true });
|
|
2068
|
+
|
|
2069
|
+
if (photo.base64) {
|
|
2070
|
+
document.querySelector("img.preview").src =
|
|
2071
|
+
"data:" + photo.mimeType + ";base64," + photo.base64;
|
|
2072
|
+
}`
|
|
2073
|
+
}
|
|
2074
|
+
},
|
|
2075
|
+
{
|
|
2076
|
+
when: { pt: "Para ler um QR Code quando o WebView do aparelho oferecer BarcodeDetector.", en: "To read a QR code when the device WebView provides BarcodeDetector." },
|
|
2077
|
+
example: {
|
|
2078
|
+
pt: `try {
|
|
2079
|
+
const qr = await escanearQRCode();
|
|
2080
|
+
if (qr) {
|
|
2081
|
+
console.log("QR:", qr.text);
|
|
2082
|
+
}
|
|
2083
|
+
} catch (erro) {
|
|
2084
|
+
await toast("Digite ou cole o codigo");
|
|
2085
|
+
}`,
|
|
2086
|
+
en: `try {
|
|
2087
|
+
const qr = await scanQRCode();
|
|
2088
|
+
if (qr) {
|
|
2089
|
+
console.log("QR:", qr.text);
|
|
2090
|
+
}
|
|
2091
|
+
} catch (error) {
|
|
2092
|
+
await toast("Type or paste the code");
|
|
2093
|
+
}`
|
|
2094
|
+
}
|
|
2095
|
+
},
|
|
2096
|
+
{
|
|
2097
|
+
when: { pt: "Para gravar e recuperar uma variavel pelo nome, sem abrir seletor de arquivo.", en: "To save and recover a variable by name, without opening a file picker." },
|
|
2098
|
+
example: {
|
|
2099
|
+
pt: `await salvarArquivo("perfil.json", {
|
|
2100
|
+
nome: "Ana",
|
|
2101
|
+
plano: "premium"
|
|
2102
|
+
});
|
|
2103
|
+
|
|
2104
|
+
const perfil = await lerArquivo("perfil.json");
|
|
2105
|
+
console.log(perfil.nome);
|
|
2106
|
+
|
|
2107
|
+
const arquivos = await listarArquivos();`,
|
|
2108
|
+
en: `await saveFile("profile.json", {
|
|
2109
|
+
name: "Ana",
|
|
2110
|
+
plan: "premium"
|
|
2111
|
+
});
|
|
2112
|
+
|
|
2113
|
+
const profile = await readFile("profile.json");
|
|
2114
|
+
console.log(profile.name);
|
|
2115
|
+
|
|
2116
|
+
const files = await listFiles();`
|
|
2117
|
+
}
|
|
2118
|
+
},
|
|
2119
|
+
{
|
|
2120
|
+
when: { pt: "Para baixar um PDF ou imagem e abrir/compartilhar depois.", en: "To download a PDF or image and open/share it later." },
|
|
2121
|
+
example: {
|
|
2122
|
+
pt: `await baixarArquivo(
|
|
2123
|
+
"https://exemplo.com/relatorio.pdf",
|
|
2124
|
+
"relatorio.pdf"
|
|
2125
|
+
);
|
|
2126
|
+
|
|
2127
|
+
await baixarArquivo(
|
|
2128
|
+
"https://exemplo.com/foto.png",
|
|
2129
|
+
"foto.png",
|
|
2130
|
+
{ galeria: true }
|
|
2131
|
+
);
|
|
2132
|
+
|
|
2133
|
+
await abrirArquivo("relatorio.pdf");
|
|
2134
|
+
// await compartilharArquivo("relatorio.pdf");`,
|
|
2135
|
+
en: `await downloadFile(
|
|
2136
|
+
"https://example.com/report.pdf",
|
|
2137
|
+
"report.pdf"
|
|
2138
|
+
);
|
|
2139
|
+
|
|
2140
|
+
await downloadFile(
|
|
2141
|
+
"https://example.com/photo.png",
|
|
2142
|
+
"photo.png",
|
|
2143
|
+
{ gallery: true }
|
|
2144
|
+
);
|
|
2145
|
+
|
|
2146
|
+
await openFile("report.pdf");
|
|
2147
|
+
// await shareFile("report.pdf");`
|
|
2148
|
+
}
|
|
2149
|
+
},
|
|
2150
|
+
{
|
|
2151
|
+
when: { pt: "Para transformar base64 ou um arquivo escolhido em download com barra de progresso.", en: "To turn base64 or a picked file into a download with a progress bar." },
|
|
2152
|
+
example: {
|
|
2153
|
+
pt: `await baixarBase64("pixel.png", base64, {
|
|
2154
|
+
mimeType: "image/png",
|
|
2155
|
+
galeria: true
|
|
2156
|
+
});
|
|
2157
|
+
|
|
2158
|
+
const arquivo = await escolherArquivo();
|
|
2159
|
+
if (arquivo) {
|
|
2160
|
+
await baixarArquivoLocal(arquivo, "copia-" + arquivo.name);
|
|
2161
|
+
}`,
|
|
2162
|
+
en: `await downloadBase64("pixel.png", base64, {
|
|
2163
|
+
mimeType: "image/png",
|
|
2164
|
+
gallery: true
|
|
2165
|
+
});
|
|
2166
|
+
|
|
2167
|
+
const file = await pickFile();
|
|
2168
|
+
if (file) {
|
|
2169
|
+
await downloadLocalFile(file, "copy-" + file.name);
|
|
2170
|
+
}`
|
|
2171
|
+
}
|
|
2172
|
+
},
|
|
2173
|
+
{
|
|
2174
|
+
when: { pt: "Para preencher mapa, check-in ou entrega usando localizacao atual.", en: "To fill maps, check-ins or delivery flows using current location." },
|
|
2175
|
+
example: {
|
|
2176
|
+
pt: `const local = await obterLocalizacao({
|
|
2177
|
+
altaPrecisao: true,
|
|
2178
|
+
timeoutMs: 10000
|
|
2179
|
+
});
|
|
2180
|
+
|
|
2181
|
+
if (local.latitude) {
|
|
2182
|
+
console.log(local.latitude, local.longitude);
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
const watch = await acompanharLocalizacao();
|
|
2186
|
+
const parar = aoMudarLocalizacao(console.log);
|
|
2187
|
+
|
|
2188
|
+
// Ao sair da tela:
|
|
2189
|
+
await pararLocalizacao(watch.watchId);
|
|
2190
|
+
parar();`,
|
|
2191
|
+
en: `const location = await getLocation({
|
|
2192
|
+
highAccuracy: true,
|
|
2193
|
+
timeoutMs: 10000
|
|
2194
|
+
});
|
|
2195
|
+
|
|
2196
|
+
if (location.latitude) {
|
|
2197
|
+
console.log(location.latitude, location.longitude);
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
const watch = await watchLocation();
|
|
2201
|
+
const stopEvent = onLocationChange(console.log);
|
|
2202
|
+
|
|
2203
|
+
// When leaving the screen:
|
|
2204
|
+
await stopLocationWatch(watch.watchId);
|
|
2205
|
+
stopEvent();`
|
|
2206
|
+
}
|
|
2207
|
+
},
|
|
2208
|
+
{
|
|
2209
|
+
when: { pt: "Para confirmar uma acao sensivel antes de abrir dados ou finalizar pagamento.", en: "To confirm a sensitive action before opening data or finishing payment." },
|
|
2210
|
+
example: {
|
|
2211
|
+
pt: `const bio = await autenticarBiometria({
|
|
2212
|
+
titulo: "Confirmar acesso",
|
|
2213
|
+
descricao: "Use a biometria do aparelho"
|
|
2214
|
+
});
|
|
2215
|
+
|
|
2216
|
+
if (bio.authenticated) {
|
|
2217
|
+
abrirNoApp("#/seguro");
|
|
2218
|
+
}`,
|
|
2219
|
+
en: `const bio = await authenticateBiometric({
|
|
2220
|
+
title: "Confirm access",
|
|
2221
|
+
description: "Use this device biometrics"
|
|
2222
|
+
});
|
|
2223
|
+
|
|
2224
|
+
if (bio.authenticated) {
|
|
2225
|
+
openInApp("#/secure");
|
|
2226
|
+
}`
|
|
2227
|
+
}
|
|
2228
|
+
},
|
|
2229
|
+
{
|
|
2230
|
+
when: { pt: "Para guardar tokens ou preferencias sensiveis cifradas pelo Android Keystore.", en: "To store tokens or sensitive preferences encrypted by Android Keystore." },
|
|
2231
|
+
example: {
|
|
2232
|
+
pt: `await salvarSeguro("token", "abc123");
|
|
2233
|
+
|
|
2234
|
+
const token = await lerSeguro("token");
|
|
2235
|
+
console.log(token);
|
|
2236
|
+
|
|
2237
|
+
await removerSeguro("token");`,
|
|
2238
|
+
en: `await saveSecure("token", "abc123");
|
|
2239
|
+
|
|
2240
|
+
const token = await readSecure("token");
|
|
2241
|
+
console.log(token);
|
|
2242
|
+
|
|
2243
|
+
await deleteSecure("token");`
|
|
2244
|
+
}
|
|
1406
2245
|
}
|
|
1407
2246
|
];
|
|
1408
2247
|
|
|
@@ -1425,6 +2264,7 @@ const state = {
|
|
|
1425
2264
|
currentFileDirty: false,
|
|
1426
2265
|
animationTimer: null,
|
|
1427
2266
|
progress: 0,
|
|
2267
|
+
nativeCodeCategory: localStorage.getItem("html2apk.nativeCodeCategory") || "all",
|
|
1428
2268
|
logsVisible: localStorage.getItem("html2apk.logsVisible") === "true"
|
|
1429
2269
|
};
|
|
1430
2270
|
|
|
@@ -1493,7 +2333,10 @@ function collectElements() {
|
|
|
1493
2333
|
"progressBar",
|
|
1494
2334
|
"progressPercent",
|
|
1495
2335
|
"reviewGrid",
|
|
2336
|
+
"nativeCodeCategories",
|
|
2337
|
+
"nativeCodeSummary",
|
|
1496
2338
|
"nativeCodeGrid",
|
|
2339
|
+
"nativeFunctionLabButton",
|
|
1497
2340
|
"resultPanel",
|
|
1498
2341
|
"apkPath",
|
|
1499
2342
|
"openDistButton",
|
|
@@ -1595,6 +2438,9 @@ function appendLogTo(container, line, kind) {
|
|
|
1595
2438
|
}
|
|
1596
2439
|
|
|
1597
2440
|
container.scrollTop = container.scrollHeight;
|
|
2441
|
+
requestAnimationFrame(() => {
|
|
2442
|
+
container.scrollTop = container.scrollHeight;
|
|
2443
|
+
});
|
|
1598
2444
|
}
|
|
1599
2445
|
|
|
1600
2446
|
function appendLog(line, kind = "raw") {
|
|
@@ -1692,6 +2538,7 @@ function updateActionButtons() {
|
|
|
1692
2538
|
elements.doctorButton.disabled = !hasProject || isBusy;
|
|
1693
2539
|
elements.settingsNextButton.disabled = !hasProject || !state.settingsValid || !state.environmentOk || isBusy;
|
|
1694
2540
|
elements.newFileButton.disabled = !hasProject;
|
|
2541
|
+
elements.nativeFunctionLabButton.disabled = isBusy;
|
|
1695
2542
|
setBuildButtons(hasProject && state.settingsValid && state.environmentOk && !isBusy);
|
|
1696
2543
|
}
|
|
1697
2544
|
|
|
@@ -1762,51 +2609,134 @@ function escapeHtml(value) {
|
|
|
1762
2609
|
.replace(/"/g, """);
|
|
1763
2610
|
}
|
|
1764
2611
|
|
|
2612
|
+
function syntaxToken(type, value) {
|
|
2613
|
+
return `<span class="syntax-token-${type}">${escapeHtml(value)}</span>`;
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
function highlightByRegex(value, regex, classify) {
|
|
2617
|
+
const source = String(value || "");
|
|
2618
|
+
let html = "";
|
|
2619
|
+
let cursor = 0;
|
|
2620
|
+
|
|
2621
|
+
source.replace(regex, (match, ...args) => {
|
|
2622
|
+
const index = args[args.length - 2];
|
|
2623
|
+
const tokenType = classify(match, index, source);
|
|
2624
|
+
html += escapeHtml(source.slice(cursor, index));
|
|
2625
|
+
html += tokenType ? syntaxToken(tokenType, match) : escapeHtml(match);
|
|
2626
|
+
cursor = index + match.length;
|
|
2627
|
+
return match;
|
|
2628
|
+
});
|
|
2629
|
+
|
|
2630
|
+
html += escapeHtml(source.slice(cursor));
|
|
2631
|
+
return html;
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
function highlightJavaScript(value) {
|
|
2635
|
+
const keywordList = [
|
|
2636
|
+
"await", "async", "break", "case", "catch", "class", "const", "continue", "default", "delete",
|
|
2637
|
+
"do", "else", "export", "extends", "false", "finally", "for", "from", "function", "if",
|
|
2638
|
+
"import", "in", "instanceof", "let", "new", "null", "return", "super", "switch", "this",
|
|
2639
|
+
"throw", "true", "try", "typeof", "undefined", "var", "void", "while", "yield"
|
|
2640
|
+
];
|
|
2641
|
+
const keywords = new Set(keywordList);
|
|
2642
|
+
const keywordPattern = keywordList.join("|");
|
|
2643
|
+
const regex = new RegExp("\\/\\*[\\s\\S]*?\\*\\/|\\/\\/[^\\n\\r]*|\"(?:\\\\.|[^\"\\\\])*\"|'(?:\\\\.|[^'\\\\])*'|`(?:\\\\.|[^`\\\\])*`|\\b\\d+(?:\\.\\d+)?\\b|\\b(?:" + keywordPattern + ")\\b|\\b[A-Za-z_$][\\w$]*(?=\\s*\\()", "g");
|
|
2644
|
+
|
|
2645
|
+
return highlightByRegex(value, regex, (match) => {
|
|
2646
|
+
if (match.startsWith("//") || match.startsWith("/*")) {
|
|
2647
|
+
return "comment";
|
|
2648
|
+
}
|
|
2649
|
+
if (match.startsWith("\"") || match.startsWith("'") || match.startsWith("`")) {
|
|
2650
|
+
return "string";
|
|
2651
|
+
}
|
|
2652
|
+
if (/^\d/.test(match)) {
|
|
2653
|
+
return "number";
|
|
2654
|
+
}
|
|
2655
|
+
if (keywords.has(match)) {
|
|
2656
|
+
return "keyword";
|
|
2657
|
+
}
|
|
2658
|
+
return "function";
|
|
2659
|
+
});
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
function highlightHtmlLike(value) {
|
|
2663
|
+
return highlightByRegex(value, /<!--[\s\S]*?-->|<!doctype[^>]*>|<\/?[a-zA-Z][^>]*?>/gi, (match) => {
|
|
2664
|
+
if (match.startsWith("<!--")) {
|
|
2665
|
+
return "comment";
|
|
2666
|
+
}
|
|
2667
|
+
return "tag";
|
|
2668
|
+
});
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
function highlightCss(value) {
|
|
2672
|
+
const regex = /\/\*[\s\S]*?\*\/|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|#[0-9a-fA-F]{3,8}\b|\b\d+(?:\.\d+)?(?:px|rem|em|%|vh|vw|s|ms)?\b|[a-zA-Z-]+(?=\s*:)|[.#]?[a-zA-Z0-9_-]+(?=[^{;]*\{)/g;
|
|
2673
|
+
return highlightByRegex(value, regex, (match) => {
|
|
2674
|
+
if (match.startsWith("/*")) {
|
|
2675
|
+
return "comment";
|
|
2676
|
+
}
|
|
2677
|
+
if (match.startsWith("\"") || match.startsWith("'")) {
|
|
2678
|
+
return "string";
|
|
2679
|
+
}
|
|
2680
|
+
if (match.startsWith("#") || /^\d/.test(match)) {
|
|
2681
|
+
return "number";
|
|
2682
|
+
}
|
|
2683
|
+
if (/^[a-zA-Z-]+$/.test(match)) {
|
|
2684
|
+
return "keyword";
|
|
2685
|
+
}
|
|
2686
|
+
return "tag";
|
|
2687
|
+
});
|
|
2688
|
+
}
|
|
2689
|
+
|
|
2690
|
+
function highlightJson(value) {
|
|
2691
|
+
const regex = /"(?:\\.|[^"\\])*"(?=\s*:)|"(?:\\.|[^"\\])*"|-?\b\d+(?:\.\d+)?(?:e[+-]?\d+)?\b|\b(?:true|false|null)\b/gi;
|
|
2692
|
+
return highlightByRegex(value, regex, (match, index, source) => {
|
|
2693
|
+
if (match.startsWith("\"")) {
|
|
2694
|
+
return /^\s*:/.test(source.slice(index + match.length)) ? "keyword" : "string";
|
|
2695
|
+
}
|
|
2696
|
+
if (/^(true|false|null)$/i.test(match)) {
|
|
2697
|
+
return "keyword";
|
|
2698
|
+
}
|
|
2699
|
+
return "number";
|
|
2700
|
+
});
|
|
2701
|
+
}
|
|
2702
|
+
|
|
1765
2703
|
function highlightSource(value, language) {
|
|
1766
|
-
let html = escapeHtml(value);
|
|
1767
2704
|
const lang = String(language || "").toLowerCase();
|
|
1768
2705
|
|
|
1769
2706
|
if (["html", "xml", "svg"].includes(lang)) {
|
|
1770
|
-
|
|
1771
|
-
.replace(/(<!--[\s\S]*?-->)/g, "<span class=\"syntax-token-comment\">$1</span>")
|
|
1772
|
-
.replace(/(<\/?[a-zA-Z][^&]*?>)/g, "<span class=\"syntax-token-tag\">$1</span>");
|
|
1773
|
-
return html;
|
|
2707
|
+
return highlightHtmlLike(value);
|
|
1774
2708
|
}
|
|
1775
|
-
|
|
1776
2709
|
if (["js", "mjs", "cjs", "ts", "tsx", "jsx"].includes(lang)) {
|
|
1777
|
-
|
|
1778
|
-
.replace(/\b(await|async|const|let|var|function|return|if|else|for|while|try|catch|throw|new|class|import|export|from|true|false|null|undefined)\b/g, "<span class=\"syntax-token-keyword\">$1</span>")
|
|
1779
|
-
.replace(/\b(\d+(?:\.\d+)?)\b/g, "<span class=\"syntax-token-number\">$1</span>")
|
|
1780
|
-
.replace(/(\/\*[\s\S]*?\*\/|\/\/[^\n\r]*)/g, "<span class=\"syntax-token-comment\">$1</span>")
|
|
1781
|
-
.replace(/(".*?"|'.*?'|`[\s\S]*?`)/g, "<span class=\"syntax-token-string\">$1</span>");
|
|
1782
|
-
return html;
|
|
2710
|
+
return highlightJavaScript(value);
|
|
1783
2711
|
}
|
|
1784
|
-
|
|
1785
2712
|
if (lang === "css") {
|
|
1786
|
-
|
|
1787
|
-
.replace(/(\/\*[\s\S]*?\*\/)/g, "<span class=\"syntax-token-comment\">$1</span>")
|
|
1788
|
-
.replace(/([.#]?[a-zA-Z0-9_-]+)(\s*\{)/g, "<span class=\"syntax-token-tag\">$1</span>$2")
|
|
1789
|
-
.replace(/(:\s*)([^;\n]+)(;?)/g, "$1<span class=\"syntax-token-string\">$2</span>$3");
|
|
1790
|
-
return html;
|
|
2713
|
+
return highlightCss(value);
|
|
1791
2714
|
}
|
|
1792
|
-
|
|
1793
2715
|
if (lang === "json") {
|
|
1794
|
-
|
|
1795
|
-
.replace(/("[^&]+")(\s*:)/g, "<span class=\"syntax-token-keyword\">$1</span>$2")
|
|
1796
|
-
.replace(/(:\s*)(".*?")/g, "$1<span class=\"syntax-token-string\">$2</span>")
|
|
1797
|
-
.replace(/\b(true|false|null)\b/g, "<span class=\"syntax-token-keyword\">$1</span>")
|
|
1798
|
-
.replace(/\b(\d+(?:\.\d+)?)\b/g, "<span class=\"syntax-token-number\">$1</span>");
|
|
2716
|
+
return highlightJson(value);
|
|
1799
2717
|
}
|
|
1800
2718
|
|
|
1801
|
-
return
|
|
2719
|
+
return escapeHtml(value);
|
|
1802
2720
|
}
|
|
1803
2721
|
|
|
1804
2722
|
function updateFilePreview() {
|
|
1805
|
-
if (!elements.fileHighlight) {
|
|
2723
|
+
if (!elements.fileHighlight || !elements.fileEditorInput) {
|
|
1806
2724
|
return;
|
|
1807
2725
|
}
|
|
1808
2726
|
|
|
1809
|
-
|
|
2727
|
+
const value = elements.fileEditorInput.value || "";
|
|
2728
|
+
const highlighted = highlightSource(value.length ? value : " ", state.currentFileLanguage);
|
|
2729
|
+
elements.fileHighlight.innerHTML = `<code>${highlighted}</code>`;
|
|
2730
|
+
syncFileEditorHighlightScroll();
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
function syncFileEditorHighlightScroll() {
|
|
2734
|
+
if (!elements.fileHighlight || !elements.fileEditorInput) {
|
|
2735
|
+
return;
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
elements.fileHighlight.scrollTop = elements.fileEditorInput.scrollTop;
|
|
2739
|
+
elements.fileHighlight.scrollLeft = elements.fileEditorInput.scrollLeft;
|
|
1810
2740
|
}
|
|
1811
2741
|
|
|
1812
2742
|
function setCurrentFileDirty(value) {
|
|
@@ -1891,6 +2821,8 @@ async function openProjectFile(relativePath) {
|
|
|
1891
2821
|
elements.fileLanguageBadge.textContent = state.currentFileLanguage;
|
|
1892
2822
|
elements.fileEditorInput.disabled = false;
|
|
1893
2823
|
elements.fileEditorInput.value = file.content || "";
|
|
2824
|
+
elements.fileEditorInput.scrollTop = 0;
|
|
2825
|
+
elements.fileEditorInput.scrollLeft = 0;
|
|
1894
2826
|
setCurrentFileDirty(false);
|
|
1895
2827
|
updateFilePreview();
|
|
1896
2828
|
renderFileTree();
|
|
@@ -1938,9 +2870,59 @@ async function createNewProjectFile() {
|
|
|
1938
2870
|
}
|
|
1939
2871
|
}
|
|
1940
2872
|
|
|
2873
|
+
const nativeCodeRecipeCache = new Map();
|
|
2874
|
+
|
|
2875
|
+
function localizedRecipeText(value) {
|
|
2876
|
+
if (!value) {
|
|
2877
|
+
return "";
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2880
|
+
if (typeof value === "string") {
|
|
2881
|
+
return value;
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2884
|
+
return [value.pt, value.en].filter(Boolean).join("\n");
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
function recipeSearchText(recipe) {
|
|
2888
|
+
return [
|
|
2889
|
+
localizedRecipeText(recipe && recipe.when),
|
|
2890
|
+
localizedRecipeText(recipe && recipe.example)
|
|
2891
|
+
].join("\n");
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2894
|
+
function primaryCodeKey(entry) {
|
|
2895
|
+
const syntax = entry && entry.syntax
|
|
2896
|
+
? entry.syntax.pt || entry.syntax.en || ""
|
|
2897
|
+
: "";
|
|
2898
|
+
const match = String(syntax).match(/([A-Za-z_$][\w$]*)\s*\(/);
|
|
2899
|
+
return match ? match[1] : "";
|
|
2900
|
+
}
|
|
2901
|
+
|
|
2902
|
+
function findRecipeForEntry(entry) {
|
|
2903
|
+
const key = primaryCodeKey(entry);
|
|
2904
|
+
|
|
2905
|
+
if (!key) {
|
|
2906
|
+
return null;
|
|
2907
|
+
}
|
|
2908
|
+
|
|
2909
|
+
if (nativeCodeRecipeCache.has(key)) {
|
|
2910
|
+
return nativeCodeRecipeCache.get(key);
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2913
|
+
const normalizedKey = key.toLowerCase();
|
|
2914
|
+
const recipe = nativeCodeRecipes.find((item) => {
|
|
2915
|
+
return recipeSearchText(item).toLowerCase().includes(normalizedKey);
|
|
2916
|
+
}) || null;
|
|
2917
|
+
|
|
2918
|
+
nativeCodeRecipeCache.set(key, recipe);
|
|
2919
|
+
return recipe;
|
|
2920
|
+
}
|
|
2921
|
+
|
|
1941
2922
|
function recipeForCode(index) {
|
|
1942
2923
|
const language = currentLanguage();
|
|
1943
|
-
const
|
|
2924
|
+
const entry = nativeCodeEntries[index] || {};
|
|
2925
|
+
const recipe = entry.recipe || findRecipeForEntry(entry) || {};
|
|
1944
2926
|
return {
|
|
1945
2927
|
when: recipe.when ? recipe.when[language] || recipe.when.pt : "",
|
|
1946
2928
|
example: recipe.example ? recipe.example[language] || recipe.example.pt : ""
|
|
@@ -1966,6 +2948,14 @@ async function copyToClipboard(value) {
|
|
|
1966
2948
|
}
|
|
1967
2949
|
|
|
1968
2950
|
async function handleNativeCodeCopy(event) {
|
|
2951
|
+
const categoryButton = event.target.closest("[data-code-category]");
|
|
2952
|
+
if (categoryButton) {
|
|
2953
|
+
state.nativeCodeCategory = categoryButton.dataset.codeCategory || "all";
|
|
2954
|
+
localStorage.setItem("html2apk.nativeCodeCategory", state.nativeCodeCategory);
|
|
2955
|
+
renderNativeCodeGrid();
|
|
2956
|
+
return;
|
|
2957
|
+
}
|
|
2958
|
+
|
|
1969
2959
|
const button = event.target.closest("[data-copy-code]");
|
|
1970
2960
|
if (!button) {
|
|
1971
2961
|
return;
|
|
@@ -2125,6 +3115,20 @@ function renderNativeCodeGrid() {
|
|
|
2125
3115
|
}
|
|
2126
3116
|
|
|
2127
3117
|
const language = currentLanguage();
|
|
3118
|
+
const activeCategory = nativeCodeCategories.some((category) => category.id === state.nativeCodeCategory)
|
|
3119
|
+
? state.nativeCodeCategory
|
|
3120
|
+
: "all";
|
|
3121
|
+
state.nativeCodeCategory = activeCategory;
|
|
3122
|
+
const categoryCounts = nativeCodeEntries.reduce((counts, entry) => {
|
|
3123
|
+
const category = entry.category || "device";
|
|
3124
|
+
counts[category] = (counts[category] || 0) + 1;
|
|
3125
|
+
counts.all = (counts.all || 0) + 1;
|
|
3126
|
+
return counts;
|
|
3127
|
+
}, {});
|
|
3128
|
+
const activeCategoryMeta = nativeCodeCategories.find((category) => category.id === activeCategory) || nativeCodeCategories[0];
|
|
3129
|
+
const visibleEntries = nativeCodeEntries
|
|
3130
|
+
.map((entry, index) => ({ entry, index }))
|
|
3131
|
+
.filter((item) => activeCategory === "all" || item.entry.category === activeCategory);
|
|
2128
3132
|
const javaLabel = text("javaLabel");
|
|
2129
3133
|
const doesLabel = text("doesLabel");
|
|
2130
3134
|
const whenUseLabel = text("whenUseLabel");
|
|
@@ -2132,17 +3136,45 @@ function renderNativeCodeGrid() {
|
|
|
2132
3136
|
const handlingLabel = text("handlingLabel");
|
|
2133
3137
|
const exampleLabel = text("exampleLabel");
|
|
2134
3138
|
const copyCodeLabel = text("copyCode");
|
|
2135
|
-
|
|
3139
|
+
|
|
3140
|
+
if (elements.nativeCodeCategories) {
|
|
3141
|
+
elements.nativeCodeCategories.innerHTML = nativeCodeCategories.map((category) => {
|
|
3142
|
+
const title = category.title[language] || category.title.pt;
|
|
3143
|
+
const description = category.description[language] || category.description.pt;
|
|
3144
|
+
const count = categoryCounts[category.id] || 0;
|
|
3145
|
+
const active = category.id === activeCategory ? " active" : "";
|
|
3146
|
+
return `
|
|
3147
|
+
<button type="button" class="code-category-button${active}" data-code-category="${escapeHtml(category.id)}" aria-pressed="${category.id === activeCategory}">
|
|
3148
|
+
<strong>${escapeHtml(title)}</strong>
|
|
3149
|
+
<span class="code-category-count">${count}</span>
|
|
3150
|
+
<small>${escapeHtml(description)}</small>
|
|
3151
|
+
</button>
|
|
3152
|
+
`;
|
|
3153
|
+
}).join("");
|
|
3154
|
+
}
|
|
3155
|
+
|
|
3156
|
+
if (elements.nativeCodeSummary) {
|
|
3157
|
+
const title = activeCategoryMeta.title[language] || activeCategoryMeta.title.pt;
|
|
3158
|
+
const description = activeCategoryMeta.description[language] || activeCategoryMeta.description.pt;
|
|
3159
|
+
elements.nativeCodeSummary.innerHTML = `
|
|
3160
|
+
<strong>${escapeHtml(title)} · ${escapeHtml(text("codesShowing"))} ${visibleEntries.length} ${escapeHtml(text("codesItems"))}</strong>
|
|
3161
|
+
<p>${escapeHtml(description)}</p>
|
|
3162
|
+
`;
|
|
3163
|
+
}
|
|
3164
|
+
|
|
3165
|
+
elements.nativeCodeGrid.innerHTML = visibleEntries.map(({ entry, index }) => {
|
|
2136
3166
|
const syntax = entry.syntax ? entry.syntax[language] || entry.syntax.pt : entry.js;
|
|
2137
3167
|
const description = entry.description[language] || entry.description.pt;
|
|
2138
3168
|
const returns = entry.returns[language] || entry.returns.pt;
|
|
2139
3169
|
const handling = entry.handling[language] || entry.handling.pt;
|
|
2140
3170
|
const recipe = recipeForCode(index);
|
|
3171
|
+
const highlightedSyntax = highlightSource(syntax, "js");
|
|
3172
|
+
const highlightedExample = recipe.example ? highlightSource(recipe.example, "js") : "";
|
|
2141
3173
|
|
|
2142
3174
|
return `
|
|
2143
3175
|
<article class="code-card">
|
|
2144
3176
|
<div class="code-card-top">
|
|
2145
|
-
<code>${
|
|
3177
|
+
<code class="syntax-inline">${highlightedSyntax}</code>
|
|
2146
3178
|
<span>${escapeHtml(javaLabel)}: ${escapeHtml(entry.java)}</span>
|
|
2147
3179
|
</div>
|
|
2148
3180
|
<p><strong>${escapeHtml(doesLabel)}:</strong> ${escapeHtml(description)}</p>
|
|
@@ -2154,7 +3186,7 @@ function renderNativeCodeGrid() {
|
|
|
2154
3186
|
<strong>${escapeHtml(exampleLabel)}</strong>
|
|
2155
3187
|
<button type="button" class="copy-code-button" data-copy-code="${index}">${escapeHtml(copyCodeLabel)}</button>
|
|
2156
3188
|
</div>
|
|
2157
|
-
<pre><code>${
|
|
3189
|
+
<pre><code>${highlightedExample}</code></pre>
|
|
2158
3190
|
</div>
|
|
2159
3191
|
</article>
|
|
2160
3192
|
`;
|
|
@@ -2521,7 +3553,9 @@ async function summarizeProject(project) {
|
|
|
2521
3553
|
elements.fileLanguageBadge.textContent = "text";
|
|
2522
3554
|
elements.fileEditorInput.value = "";
|
|
2523
3555
|
elements.fileEditorInput.disabled = true;
|
|
2524
|
-
elements.
|
|
3556
|
+
elements.fileEditorInput.scrollTop = 0;
|
|
3557
|
+
elements.fileEditorInput.scrollLeft = 0;
|
|
3558
|
+
updateFilePreview();
|
|
2525
3559
|
elements.saveFileButton.disabled = true;
|
|
2526
3560
|
populateSettings(project.config || {}, project);
|
|
2527
3561
|
setStep("folder", project.hasEntryFile ? "done" : "active", project.hasEntryFile ? text("folderReady") : text("missing"));
|
|
@@ -2751,6 +3785,74 @@ async function runUsbDebugFlow() {
|
|
|
2751
3785
|
}
|
|
2752
3786
|
}
|
|
2753
3787
|
|
|
3788
|
+
async function runNativeFunctionLabFlow() {
|
|
3789
|
+
if (state.buildRunning) {
|
|
3790
|
+
setStatus("error", text("functionLabRunning"));
|
|
3791
|
+
return;
|
|
3792
|
+
}
|
|
3793
|
+
if (!api.runNativeFunctionLab) {
|
|
3794
|
+
setStatus("error", text("functionLabFail"));
|
|
3795
|
+
return;
|
|
3796
|
+
}
|
|
3797
|
+
|
|
3798
|
+
showLogBar();
|
|
3799
|
+
state.buildRunning = true;
|
|
3800
|
+
updateActionButtons();
|
|
3801
|
+
elements.resultPanel.classList.add("hidden");
|
|
3802
|
+
setView("build");
|
|
3803
|
+
setStatus("busy", text("functionLabRunning"));
|
|
3804
|
+
setStep("folder", "done", text("functionLabProject"));
|
|
3805
|
+
setStep("settings", "done", text("functionLabSettings"));
|
|
3806
|
+
setStep("doctor", "active", text("functionLabUsbCheck"));
|
|
3807
|
+
setStep("build", "active", text("functionLabRunning"));
|
|
3808
|
+
setProgress(20, text("functionLabUsbCheck"), "active");
|
|
3809
|
+
startAnimatedLogs();
|
|
3810
|
+
|
|
3811
|
+
try {
|
|
3812
|
+
const response = await api.runNativeFunctionLab();
|
|
3813
|
+
stopAnimatedLogs();
|
|
3814
|
+
if (!response.ok) {
|
|
3815
|
+
setStep("doctor", "error", text("functionLabUsbCheck"));
|
|
3816
|
+
setStep("build", "error", text("functionLabFail"));
|
|
3817
|
+
setStatus("error", text("functionLabFail"));
|
|
3818
|
+
setProgress(90, text("progressError"), "error");
|
|
3819
|
+
appendLog(response.message || text("functionLabFail"), "error");
|
|
3820
|
+
if (response.projectRoot) {
|
|
3821
|
+
appendLog(`${text("functionLabProject")}: ${response.projectRoot}`, "system");
|
|
3822
|
+
}
|
|
3823
|
+
if (response.buildDir) {
|
|
3824
|
+
appendLog(`Build directory kept: ${response.buildDir}`, "system");
|
|
3825
|
+
}
|
|
3826
|
+
return;
|
|
3827
|
+
}
|
|
3828
|
+
|
|
3829
|
+
const result = response.result;
|
|
3830
|
+
state.lastApkPath = result.apkPath;
|
|
3831
|
+
state.lastDistPath = result.distPath || "";
|
|
3832
|
+
elements.apkPath.textContent = result.apkPath;
|
|
3833
|
+
elements.successTitle.textContent = text("functionLabSuccessTitle");
|
|
3834
|
+
elements.successText.textContent = text("functionLabSuccessText");
|
|
3835
|
+
elements.successApkPath.textContent = result.apkPath;
|
|
3836
|
+
elements.resultPanel.classList.remove("hidden");
|
|
3837
|
+
setStep("doctor", "done", text("functionLabUsbCheck"));
|
|
3838
|
+
setStep("build", "done", text("functionLabOk"));
|
|
3839
|
+
setStatus("ready", text("functionLabOk"));
|
|
3840
|
+
setProgress(100, text("progressDone"));
|
|
3841
|
+
appendLog(`${text("functionLabOk")}: ${result.device && result.device.id ? result.device.id : "Android USB"}`, "success");
|
|
3842
|
+
appendLog(`${text("buildOk")}: ${result.apkPath}`, "success");
|
|
3843
|
+
setView("success");
|
|
3844
|
+
} catch (error) {
|
|
3845
|
+
stopAnimatedLogs();
|
|
3846
|
+
setStep("build", "error", text("functionLabFail"));
|
|
3847
|
+
setStatus("error", error.message);
|
|
3848
|
+
setProgress(90, text("progressError"), "error");
|
|
3849
|
+
appendLog(error.message, "error");
|
|
3850
|
+
} finally {
|
|
3851
|
+
state.buildRunning = false;
|
|
3852
|
+
updateActionButtons();
|
|
3853
|
+
}
|
|
3854
|
+
}
|
|
3855
|
+
|
|
2754
3856
|
function toggleTheme() {
|
|
2755
3857
|
state.theme = state.theme === "dark" ? "light" : "dark";
|
|
2756
3858
|
applyTheme();
|
|
@@ -2804,6 +3906,7 @@ function bindEvents() {
|
|
|
2804
3906
|
elements.doctorButton.addEventListener("click", runDoctorOnly);
|
|
2805
3907
|
elements.buildButton.addEventListener("click", runBuildFlow);
|
|
2806
3908
|
elements.usbDebugButton.addEventListener("click", runUsbDebugFlow);
|
|
3909
|
+
elements.nativeFunctionLabButton.addEventListener("click", runNativeFunctionLabFlow);
|
|
2807
3910
|
elements.newFileButton.addEventListener("click", createNewProjectFile);
|
|
2808
3911
|
elements.saveFileButton.addEventListener("click", saveCurrentFile);
|
|
2809
3912
|
elements.fileTree.addEventListener("click", (event) => {
|
|
@@ -2816,6 +3919,7 @@ function bindEvents() {
|
|
|
2816
3919
|
setCurrentFileDirty(true);
|
|
2817
3920
|
updateFilePreview();
|
|
2818
3921
|
});
|
|
3922
|
+
elements.fileEditorInput.addEventListener("scroll", syncFileEditorHighlightScroll);
|
|
2819
3923
|
elements.clearLogsButton.addEventListener("click", clearLogs);
|
|
2820
3924
|
elements.toggleLogsButton.addEventListener("click", toggleLogBar);
|
|
2821
3925
|
elements.bottomToggleLogsButton.addEventListener("click", toggleLogBar);
|
|
@@ -2866,6 +3970,7 @@ function bindEvents() {
|
|
|
2866
3970
|
});
|
|
2867
3971
|
elements.permissionGrid.addEventListener("change", validateSettings);
|
|
2868
3972
|
elements.nativeCodeGrid.addEventListener("click", handleNativeCodeCopy);
|
|
3973
|
+
elements.nativeCodeCategories.addEventListener("click", handleNativeCodeCopy);
|
|
2869
3974
|
[
|
|
2870
3975
|
elements.appNameInput,
|
|
2871
3976
|
elements.packageIdInput,
|
|
@@ -2980,7 +4085,7 @@ async function init() {
|
|
|
2980
4085
|
elements.iconPreview.src = iconPreviewPath(state.defaultIconPath);
|
|
2981
4086
|
}
|
|
2982
4087
|
} catch {
|
|
2983
|
-
elements.appVersion.textContent = "v0.
|
|
4088
|
+
elements.appVersion.textContent = "v0.11.0";
|
|
2984
4089
|
}
|
|
2985
4090
|
|
|
2986
4091
|
setTimeout(finishBoot, 1800);
|