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.
@@ -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 no modo flutuante", en: "Required for floating mode" }
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: "pickFile",
547
- description: { pt: "Abre o seletor nativo para o usuario escolher uma imagem.", en: "Opens the native picker for one image." },
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: "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." }
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({ multiplo: true })", en: "pickImages({ multiple: true })" },
553
- java: "pickFile",
554
- description: { pt: "Abre galeria/seletor para varias imagens.", en: "Opens gallery/picker for multiple images." },
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 de compartilhamento.", en: "Opens the native share sheet." },
597
- returns: { pt: "Promise<void>.", en: "Promise<void>." },
598
- 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." }
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 conexao atual e bateria.", en: "Reads current connection and battery." },
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 icone flutuante quando o app foi gerado em modo floating.", en: "Controls the floating icon when the app was generated in floating mode." },
702
- returns: { pt: "Promise<void>.", en: "Promise<void>." },
703
- handling: { pt: "`iniciarIconeFlutuante()` abre a tela de permissao automaticamente se faltar sobreposicao. Quando o usuario voltar, chame novamente para iniciar.", en: "`startFloatingIcon()` opens the permission screen automatically if draw-over-apps is missing. When the user comes back, call it again to start." }
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({ multiplo: true });
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/ou link.", en: "To open Android native sharing with text and/or link." },
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 compartilhar({
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 share({
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 apps em modo floating que precisam mostrar/esconder o icone flutuante.", en: "For floating-mode apps that need to show/hide the floating icon." },
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 status = await iniciarIconeFlutuante();
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, "&quot;");
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
- html = html
1771
- .replace(/(&lt;!--[\s\S]*?--&gt;)/g, "<span class=\"syntax-token-comment\">$1</span>")
1772
- .replace(/(&lt;\/?[a-zA-Z][^&]*?&gt;)/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
- html = html
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(/(&quot;.*?&quot;|'.*?'|`[\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
- html = html
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
- html = html
1795
- .replace(/(&quot;[^&]+&quot;)(\s*:)/g, "<span class=\"syntax-token-keyword\">$1</span>$2")
1796
- .replace(/(:\s*)(&quot;.*?&quot;)/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 html;
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
- elements.fileHighlight.innerHTML = `<code>${highlightSource(elements.fileEditorInput.value, state.currentFileLanguage)}</code>`;
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 recipe = nativeCodeRecipes[index] || {};
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
- elements.nativeCodeGrid.innerHTML = nativeCodeEntries.map((entry, index) => {
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>${escapeHtml(syntax)}</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>${escapeHtml(recipe.example)}</code></pre>
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.fileHighlight.innerHTML = "<code></code>";
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.7.0";
4088
+ elements.appVersion.textContent = "v0.11.0";
2984
4089
  }
2985
4090
 
2986
4091
  setTimeout(finishBoot, 1800);