html2apk 0.8.0 → 0.9.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')" },
@@ -698,12 +774,171 @@ const nativeCodeEntries = [
698
774
  {
699
775
  syntax: { pt: "iniciarIconeFlutuante() / pararIconeFlutuante()", en: "startFloatingIcon() / stopFloatingIcon()" },
700
776
  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." },
777
+ description: { pt: "Controla o icone flutuante quando a sobreposicao estiver liberada no Android.", en: "Controls the floating icon when draw-over-apps is allowed on Android." },
702
778
  returns: { pt: "Promise<void>.", en: "Promise<void>." },
703
779
  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." }
780
+ },
781
+ {
782
+ syntax: { pt: "tirarFoto() / capturarVideo()", en: "takePhoto() / captureVideo()" },
783
+ java: "ACTION_IMAGE_CAPTURE / ACTION_VIDEO_CAPTURE",
784
+ 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." },
785
+ 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." },
786
+ 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." }
787
+ },
788
+ {
789
+ syntax: { pt: "escanearQRCode()", en: "scanQRCode()" },
790
+ java: "BarcodeDetector + camera",
791
+ 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." },
792
+ 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." },
793
+ 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." }
794
+ },
795
+ {
796
+ syntax: { pt: "salvarArquivo('nome.json', minhaVariavel) / lerArquivo('nome.json')", en: "saveFile('name.json', myValue) / readFile('name.json')" },
797
+ java: "app-scoped external files",
798
+ 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." },
799
+ 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." },
800
+ 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." }
801
+ },
802
+ {
803
+ syntax: { pt: "baixarArquivo(url, 'arquivo.pdf') / abrirArquivo('arquivo.pdf')", en: "downloadFile(url, 'file.pdf') / openFile('file.pdf')" },
804
+ java: "HttpURLConnection + NotificationCompat",
805
+ description: { pt: "Baixa por URL para o armazenamento persistente e mostra notificacao Android com barra de progresso.", en: "Downloads from a URL to persistent storage and shows an Android progress notification." },
806
+ returns: { pt: "`baixarArquivo` retorna metadados, tamanho, origem e se a notificacao foi mostrada.", en: "`downloadFile` returns metadata, size, source and whether the notification was shown." },
807
+ handling: { pt: "Valide URL e tamanho. No Android 13+, se a permissao de notificacao for negada, o download continua e retorna `notificationShown:false`.", en: "Validate URL and size. On Android 13+, if notification permission is denied, the download continues and returns `notificationShown:false`." }
808
+ },
809
+ {
810
+ syntax: { pt: "baixarBase64('foto.png', base64) / baixarArquivoLocal(arquivo, 'copia.pdf')", en: "downloadBase64('photo.png', base64) / downloadLocalFile(file, 'copy.pdf')" },
811
+ java: "InputStream + NotificationCompat",
812
+ 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()`." },
813
+ returns: { pt: "Metadados do arquivo salvo, `sourceType`, `notificationShown` e permissao de notificacao.", en: "Saved file metadata, `sourceType`, `notificationShown` and notification permission." },
814
+ handling: { pt: "Use `baixarBase64` para conteudo em memoria e `baixarArquivoLocal` para copiar arquivo normal com progresso. Para esconder a notificacao, passe `{ notificacao: false }`.", en: "Use `downloadBase64` for in-memory content and `downloadLocalFile` to copy a normal file with progress. To hide the notification, pass `{ notification: false }`." }
815
+ },
816
+ {
817
+ syntax: { pt: "obterLocalizacao() / acompanharLocalizacao()", en: "getLocation() / watchLocation()" },
818
+ java: "LocationManager",
819
+ 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." },
820
+ returns: { pt: "{ latitude, longitude, accuracy, provider } ou { watchId }.", en: "{ latitude, longitude, accuracy, provider } or { watchId }." },
821
+ 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." }
822
+ },
823
+ {
824
+ syntax: { pt: "autenticarBiometria({ titulo })", en: "authenticateBiometric({ title })" },
825
+ java: "BiometricPrompt",
826
+ description: { pt: "Abre a biometria/tela segura do Android para confirmar identidade.", en: "Opens Android biometric/secure prompt to confirm identity." },
827
+ returns: { pt: "{ supported, authenticated, canceled, message }.", en: "{ supported, authenticated, canceled, message }." },
828
+ 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." }
829
+ },
830
+ {
831
+ syntax: { pt: "salvarSeguro('token', valor) / lerSeguro('token')", en: "saveSecure('token', value) / readSecure('token')" },
832
+ java: "Android Keystore",
833
+ description: { pt: "Guarda pequenos segredos cifrados com chave do Android Keystore.", en: "Stores small secrets encrypted with an Android Keystore key." },
834
+ returns: { pt: "`lerSeguro` retorna o valor salvo; `listarSeguro` retorna chaves; `removerSeguro` apaga.", en: "`readSecure` returns the saved value; `listSecureKeys` returns keys; `deleteSecure` removes." },
835
+ 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." }
836
+ },
837
+ {
838
+ syntax: { pt: "definirPapelParede('foto.jpg', { alvo: 'inicio' })", en: "setWallpaper('photo.jpg', { target: 'home' })" },
839
+ java: "WallpaperManager",
840
+ 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." },
841
+ returns: { pt: "{ applied, systemApplied, lockApplied, lockSupported, mimeType }.", en: "{ applied, systemApplied, lockApplied, lockSupported, mimeType }." },
842
+ 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." },
843
+ recipe: {
844
+ 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." },
845
+ example: {
846
+ pt: `const resultado = await definirPapelParede("foto.jpg", {
847
+ alvo: "inicio"
848
+ });
849
+
850
+ if (!resultado.applied) {
851
+ console.log(resultado);
852
+ }`,
853
+ en: `const result = await setWallpaper("photo.jpg", {
854
+ target: "home"
855
+ });
856
+
857
+ if (!result.applied) {
858
+ console.log(result);
859
+ }`
860
+ }
861
+ }
862
+ },
863
+ {
864
+ syntax: { pt: "infoPapelParede() / abrirConfiguracaoPapelParede()", en: "wallpaperInfo() / openWallpaperSettings()" },
865
+ java: "android.settings.WALLPAPER_SETTINGS",
866
+ 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." },
867
+ 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." },
868
+ 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." },
869
+ recipe: {
870
+ 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." },
871
+ example: {
872
+ pt: `const info = await infoPapelParede();
873
+
874
+ if (!info.videoSupported) {
875
+ await abrirConfiguracaoPapelParede();
876
+ }`,
877
+ en: `const info = await wallpaperInfo();
878
+
879
+ if (!info.videoSupported) {
880
+ await openWallpaperSettings();
881
+ }`
882
+ }
883
+ }
704
884
  }
705
885
  ];
706
886
 
887
+ [
888
+ "feedback",
889
+ "feedback",
890
+ "notifications",
891
+ "notifications",
892
+ "notifications",
893
+ "notifications",
894
+ "notifications",
895
+ "permissions",
896
+ "permissions",
897
+ "permissions",
898
+ "permissions",
899
+ "permissions",
900
+ "media",
901
+ "media",
902
+ "media",
903
+ "media",
904
+ "media",
905
+ "files",
906
+ "files",
907
+ "media",
908
+ "files",
909
+ "files",
910
+ "share",
911
+ "share",
912
+ "device",
913
+ "device",
914
+ "device",
915
+ "share",
916
+ "share",
917
+ "share",
918
+ "share",
919
+ "device",
920
+ "device",
921
+ "device",
922
+ "device",
923
+ "device",
924
+ "device",
925
+ "device",
926
+ "media",
927
+ "media",
928
+ "files",
929
+ "files",
930
+ "files",
931
+ "security",
932
+ "security",
933
+ "security",
934
+ "wallpaper",
935
+ "wallpaper"
936
+ ].forEach((category, index) => {
937
+ if (nativeCodeEntries[index]) {
938
+ nativeCodeEntries[index].category = category;
939
+ }
940
+ });
941
+
707
942
  const nativeCodeRecipes = [
708
943
  {
709
944
  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." },
@@ -1384,7 +1619,7 @@ for (const app of result.apps) {
1384
1619
  }
1385
1620
  },
1386
1621
  {
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." },
1622
+ when: { pt: "Para apps que precisam mostrar/esconder o icone flutuante.", en: "For apps that need to show/hide the floating icon." },
1388
1623
  example: {
1389
1624
  pt: `const status = await iniciarIconeFlutuante();
1390
1625
 
@@ -1403,6 +1638,180 @@ if (status.requiresSettings) {
1403
1638
  // To turn it off:
1404
1639
  // await stopFloatingIcon();`
1405
1640
  }
1641
+ },
1642
+ {
1643
+ 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." },
1644
+ example: {
1645
+ pt: `const foto = await tirarFoto({ base64: true });
1646
+
1647
+ if (foto.base64) {
1648
+ document.querySelector("img.preview").src =
1649
+ "data:" + foto.mimeType + ";base64," + foto.base64;
1650
+ }`,
1651
+ en: `const photo = await takePhoto({ base64: true });
1652
+
1653
+ if (photo.base64) {
1654
+ document.querySelector("img.preview").src =
1655
+ "data:" + photo.mimeType + ";base64," + photo.base64;
1656
+ }`
1657
+ }
1658
+ },
1659
+ {
1660
+ 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." },
1661
+ example: {
1662
+ pt: `try {
1663
+ const qr = await escanearQRCode();
1664
+ if (qr) {
1665
+ console.log("QR:", qr.text);
1666
+ }
1667
+ } catch (erro) {
1668
+ await toast("Digite ou cole o codigo");
1669
+ }`,
1670
+ en: `try {
1671
+ const qr = await scanQRCode();
1672
+ if (qr) {
1673
+ console.log("QR:", qr.text);
1674
+ }
1675
+ } catch (error) {
1676
+ await toast("Type or paste the code");
1677
+ }`
1678
+ }
1679
+ },
1680
+ {
1681
+ 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." },
1682
+ example: {
1683
+ pt: `await salvarArquivo("perfil.json", {
1684
+ nome: "Ana",
1685
+ plano: "premium"
1686
+ });
1687
+
1688
+ const perfil = await lerArquivo("perfil.json");
1689
+ console.log(perfil.nome);
1690
+
1691
+ const arquivos = await listarArquivos();`,
1692
+ en: `await saveFile("profile.json", {
1693
+ name: "Ana",
1694
+ plan: "premium"
1695
+ });
1696
+
1697
+ const profile = await readFile("profile.json");
1698
+ console.log(profile.name);
1699
+
1700
+ const files = await listFiles();`
1701
+ }
1702
+ },
1703
+ {
1704
+ when: { pt: "Para baixar um PDF ou imagem e abrir/compartilhar depois.", en: "To download a PDF or image and open/share it later." },
1705
+ example: {
1706
+ pt: `await baixarArquivo(
1707
+ "https://exemplo.com/relatorio.pdf",
1708
+ "relatorio.pdf"
1709
+ );
1710
+
1711
+ await abrirArquivo("relatorio.pdf");
1712
+ // await compartilharArquivo("relatorio.pdf");`,
1713
+ en: `await downloadFile(
1714
+ "https://example.com/report.pdf",
1715
+ "report.pdf"
1716
+ );
1717
+
1718
+ await openFile("report.pdf");
1719
+ // await shareFile("report.pdf");`
1720
+ }
1721
+ },
1722
+ {
1723
+ 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." },
1724
+ example: {
1725
+ pt: `await baixarBase64("pixel.png", base64, {
1726
+ mimeType: "image/png"
1727
+ });
1728
+
1729
+ const arquivo = await escolherArquivo();
1730
+ if (arquivo) {
1731
+ await baixarArquivoLocal(arquivo, "copia-" + arquivo.name);
1732
+ }`,
1733
+ en: `await downloadBase64("pixel.png", base64, {
1734
+ mimeType: "image/png"
1735
+ });
1736
+
1737
+ const file = await pickFile();
1738
+ if (file) {
1739
+ await downloadLocalFile(file, "copy-" + file.name);
1740
+ }`
1741
+ }
1742
+ },
1743
+ {
1744
+ when: { pt: "Para preencher mapa, check-in ou entrega usando localizacao atual.", en: "To fill maps, check-ins or delivery flows using current location." },
1745
+ example: {
1746
+ pt: `const local = await obterLocalizacao({
1747
+ altaPrecisao: true,
1748
+ timeoutMs: 10000
1749
+ });
1750
+
1751
+ if (local.latitude) {
1752
+ console.log(local.latitude, local.longitude);
1753
+ }
1754
+
1755
+ const watch = await acompanharLocalizacao();
1756
+ const parar = aoMudarLocalizacao(console.log);
1757
+
1758
+ // Ao sair da tela:
1759
+ await pararLocalizacao(watch.watchId);
1760
+ parar();`,
1761
+ en: `const location = await getLocation({
1762
+ highAccuracy: true,
1763
+ timeoutMs: 10000
1764
+ });
1765
+
1766
+ if (location.latitude) {
1767
+ console.log(location.latitude, location.longitude);
1768
+ }
1769
+
1770
+ const watch = await watchLocation();
1771
+ const stopEvent = onLocationChange(console.log);
1772
+
1773
+ // When leaving the screen:
1774
+ await stopLocationWatch(watch.watchId);
1775
+ stopEvent();`
1776
+ }
1777
+ },
1778
+ {
1779
+ 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." },
1780
+ example: {
1781
+ pt: `const bio = await autenticarBiometria({
1782
+ titulo: "Confirmar acesso",
1783
+ descricao: "Use a biometria do aparelho"
1784
+ });
1785
+
1786
+ if (bio.authenticated) {
1787
+ abrirNoApp("#/seguro");
1788
+ }`,
1789
+ en: `const bio = await authenticateBiometric({
1790
+ title: "Confirm access",
1791
+ description: "Use this device biometrics"
1792
+ });
1793
+
1794
+ if (bio.authenticated) {
1795
+ openInApp("#/secure");
1796
+ }`
1797
+ }
1798
+ },
1799
+ {
1800
+ when: { pt: "Para guardar tokens ou preferencias sensiveis cifradas pelo Android Keystore.", en: "To store tokens or sensitive preferences encrypted by Android Keystore." },
1801
+ example: {
1802
+ pt: `await salvarSeguro("token", "abc123");
1803
+
1804
+ const token = await lerSeguro("token");
1805
+ console.log(token);
1806
+
1807
+ await removerSeguro("token");`,
1808
+ en: `await saveSecure("token", "abc123");
1809
+
1810
+ const token = await readSecure("token");
1811
+ console.log(token);
1812
+
1813
+ await deleteSecure("token");`
1814
+ }
1406
1815
  }
1407
1816
  ];
1408
1817
 
@@ -1425,6 +1834,7 @@ const state = {
1425
1834
  currentFileDirty: false,
1426
1835
  animationTimer: null,
1427
1836
  progress: 0,
1837
+ nativeCodeCategory: localStorage.getItem("html2apk.nativeCodeCategory") || "all",
1428
1838
  logsVisible: localStorage.getItem("html2apk.logsVisible") === "true"
1429
1839
  };
1430
1840
 
@@ -1493,7 +1903,10 @@ function collectElements() {
1493
1903
  "progressBar",
1494
1904
  "progressPercent",
1495
1905
  "reviewGrid",
1906
+ "nativeCodeCategories",
1907
+ "nativeCodeSummary",
1496
1908
  "nativeCodeGrid",
1909
+ "nativeFunctionLabButton",
1497
1910
  "resultPanel",
1498
1911
  "apkPath",
1499
1912
  "openDistButton",
@@ -1692,6 +2105,7 @@ function updateActionButtons() {
1692
2105
  elements.doctorButton.disabled = !hasProject || isBusy;
1693
2106
  elements.settingsNextButton.disabled = !hasProject || !state.settingsValid || !state.environmentOk || isBusy;
1694
2107
  elements.newFileButton.disabled = !hasProject;
2108
+ elements.nativeFunctionLabButton.disabled = isBusy;
1695
2109
  setBuildButtons(hasProject && state.settingsValid && state.environmentOk && !isBusy);
1696
2110
  }
1697
2111
 
@@ -1762,51 +2176,134 @@ function escapeHtml(value) {
1762
2176
  .replace(/"/g, "&quot;");
1763
2177
  }
1764
2178
 
2179
+ function syntaxToken(type, value) {
2180
+ return `<span class="syntax-token-${type}">${escapeHtml(value)}</span>`;
2181
+ }
2182
+
2183
+ function highlightByRegex(value, regex, classify) {
2184
+ const source = String(value || "");
2185
+ let html = "";
2186
+ let cursor = 0;
2187
+
2188
+ source.replace(regex, (match, ...args) => {
2189
+ const index = args[args.length - 2];
2190
+ const tokenType = classify(match, index, source);
2191
+ html += escapeHtml(source.slice(cursor, index));
2192
+ html += tokenType ? syntaxToken(tokenType, match) : escapeHtml(match);
2193
+ cursor = index + match.length;
2194
+ return match;
2195
+ });
2196
+
2197
+ html += escapeHtml(source.slice(cursor));
2198
+ return html;
2199
+ }
2200
+
2201
+ function highlightJavaScript(value) {
2202
+ const keywordList = [
2203
+ "await", "async", "break", "case", "catch", "class", "const", "continue", "default", "delete",
2204
+ "do", "else", "export", "extends", "false", "finally", "for", "from", "function", "if",
2205
+ "import", "in", "instanceof", "let", "new", "null", "return", "super", "switch", "this",
2206
+ "throw", "true", "try", "typeof", "undefined", "var", "void", "while", "yield"
2207
+ ];
2208
+ const keywords = new Set(keywordList);
2209
+ const keywordPattern = keywordList.join("|");
2210
+ const regex = new RegExp("\\/\\*[\\s\\S]*?\\*\\/|\\/\\/[^\\n\\r]*|\"(?:\\\\.|[^\"\\\\])*\"|'(?:\\\\.|[^'\\\\])*'|`(?:\\\\.|[^`\\\\])*`|\\b\\d+(?:\\.\\d+)?\\b|\\b(?:" + keywordPattern + ")\\b|\\b[A-Za-z_$][\\w$]*(?=\\s*\\()", "g");
2211
+
2212
+ return highlightByRegex(value, regex, (match) => {
2213
+ if (match.startsWith("//") || match.startsWith("/*")) {
2214
+ return "comment";
2215
+ }
2216
+ if (match.startsWith("\"") || match.startsWith("'") || match.startsWith("`")) {
2217
+ return "string";
2218
+ }
2219
+ if (/^\d/.test(match)) {
2220
+ return "number";
2221
+ }
2222
+ if (keywords.has(match)) {
2223
+ return "keyword";
2224
+ }
2225
+ return "function";
2226
+ });
2227
+ }
2228
+
2229
+ function highlightHtmlLike(value) {
2230
+ return highlightByRegex(value, /<!--[\s\S]*?-->|<!doctype[^>]*>|<\/?[a-zA-Z][^>]*?>/gi, (match) => {
2231
+ if (match.startsWith("<!--")) {
2232
+ return "comment";
2233
+ }
2234
+ return "tag";
2235
+ });
2236
+ }
2237
+
2238
+ function highlightCss(value) {
2239
+ 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;
2240
+ return highlightByRegex(value, regex, (match) => {
2241
+ if (match.startsWith("/*")) {
2242
+ return "comment";
2243
+ }
2244
+ if (match.startsWith("\"") || match.startsWith("'")) {
2245
+ return "string";
2246
+ }
2247
+ if (match.startsWith("#") || /^\d/.test(match)) {
2248
+ return "number";
2249
+ }
2250
+ if (/^[a-zA-Z-]+$/.test(match)) {
2251
+ return "keyword";
2252
+ }
2253
+ return "tag";
2254
+ });
2255
+ }
2256
+
2257
+ function highlightJson(value) {
2258
+ const regex = /"(?:\\.|[^"\\])*"(?=\s*:)|"(?:\\.|[^"\\])*"|-?\b\d+(?:\.\d+)?(?:e[+-]?\d+)?\b|\b(?:true|false|null)\b/gi;
2259
+ return highlightByRegex(value, regex, (match, index, source) => {
2260
+ if (match.startsWith("\"")) {
2261
+ return /^\s*:/.test(source.slice(index + match.length)) ? "keyword" : "string";
2262
+ }
2263
+ if (/^(true|false|null)$/i.test(match)) {
2264
+ return "keyword";
2265
+ }
2266
+ return "number";
2267
+ });
2268
+ }
2269
+
1765
2270
  function highlightSource(value, language) {
1766
- let html = escapeHtml(value);
1767
2271
  const lang = String(language || "").toLowerCase();
1768
2272
 
1769
2273
  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;
2274
+ return highlightHtmlLike(value);
1774
2275
  }
1775
-
1776
2276
  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;
2277
+ return highlightJavaScript(value);
1783
2278
  }
1784
-
1785
2279
  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;
2280
+ return highlightCss(value);
1791
2281
  }
1792
-
1793
2282
  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>");
2283
+ return highlightJson(value);
1799
2284
  }
1800
2285
 
1801
- return html;
2286
+ return escapeHtml(value);
1802
2287
  }
1803
2288
 
1804
2289
  function updateFilePreview() {
1805
- if (!elements.fileHighlight) {
2290
+ if (!elements.fileHighlight || !elements.fileEditorInput) {
2291
+ return;
2292
+ }
2293
+
2294
+ const value = elements.fileEditorInput.value || "";
2295
+ const highlighted = highlightSource(value.length ? value : " ", state.currentFileLanguage);
2296
+ elements.fileHighlight.innerHTML = `<code>${highlighted}</code>`;
2297
+ syncFileEditorHighlightScroll();
2298
+ }
2299
+
2300
+ function syncFileEditorHighlightScroll() {
2301
+ if (!elements.fileHighlight || !elements.fileEditorInput) {
1806
2302
  return;
1807
2303
  }
1808
2304
 
1809
- elements.fileHighlight.innerHTML = `<code>${highlightSource(elements.fileEditorInput.value, state.currentFileLanguage)}</code>`;
2305
+ elements.fileHighlight.scrollTop = elements.fileEditorInput.scrollTop;
2306
+ elements.fileHighlight.scrollLeft = elements.fileEditorInput.scrollLeft;
1810
2307
  }
1811
2308
 
1812
2309
  function setCurrentFileDirty(value) {
@@ -1891,6 +2388,8 @@ async function openProjectFile(relativePath) {
1891
2388
  elements.fileLanguageBadge.textContent = state.currentFileLanguage;
1892
2389
  elements.fileEditorInput.disabled = false;
1893
2390
  elements.fileEditorInput.value = file.content || "";
2391
+ elements.fileEditorInput.scrollTop = 0;
2392
+ elements.fileEditorInput.scrollLeft = 0;
1894
2393
  setCurrentFileDirty(false);
1895
2394
  updateFilePreview();
1896
2395
  renderFileTree();
@@ -1940,7 +2439,8 @@ async function createNewProjectFile() {
1940
2439
 
1941
2440
  function recipeForCode(index) {
1942
2441
  const language = currentLanguage();
1943
- const recipe = nativeCodeRecipes[index] || {};
2442
+ const entry = nativeCodeEntries[index] || {};
2443
+ const recipe = entry.recipe || nativeCodeRecipes[index] || {};
1944
2444
  return {
1945
2445
  when: recipe.when ? recipe.when[language] || recipe.when.pt : "",
1946
2446
  example: recipe.example ? recipe.example[language] || recipe.example.pt : ""
@@ -1966,6 +2466,14 @@ async function copyToClipboard(value) {
1966
2466
  }
1967
2467
 
1968
2468
  async function handleNativeCodeCopy(event) {
2469
+ const categoryButton = event.target.closest("[data-code-category]");
2470
+ if (categoryButton) {
2471
+ state.nativeCodeCategory = categoryButton.dataset.codeCategory || "all";
2472
+ localStorage.setItem("html2apk.nativeCodeCategory", state.nativeCodeCategory);
2473
+ renderNativeCodeGrid();
2474
+ return;
2475
+ }
2476
+
1969
2477
  const button = event.target.closest("[data-copy-code]");
1970
2478
  if (!button) {
1971
2479
  return;
@@ -2125,6 +2633,20 @@ function renderNativeCodeGrid() {
2125
2633
  }
2126
2634
 
2127
2635
  const language = currentLanguage();
2636
+ const activeCategory = nativeCodeCategories.some((category) => category.id === state.nativeCodeCategory)
2637
+ ? state.nativeCodeCategory
2638
+ : "all";
2639
+ state.nativeCodeCategory = activeCategory;
2640
+ const categoryCounts = nativeCodeEntries.reduce((counts, entry) => {
2641
+ const category = entry.category || "device";
2642
+ counts[category] = (counts[category] || 0) + 1;
2643
+ counts.all = (counts.all || 0) + 1;
2644
+ return counts;
2645
+ }, {});
2646
+ const activeCategoryMeta = nativeCodeCategories.find((category) => category.id === activeCategory) || nativeCodeCategories[0];
2647
+ const visibleEntries = nativeCodeEntries
2648
+ .map((entry, index) => ({ entry, index }))
2649
+ .filter((item) => activeCategory === "all" || item.entry.category === activeCategory);
2128
2650
  const javaLabel = text("javaLabel");
2129
2651
  const doesLabel = text("doesLabel");
2130
2652
  const whenUseLabel = text("whenUseLabel");
@@ -2132,17 +2654,45 @@ function renderNativeCodeGrid() {
2132
2654
  const handlingLabel = text("handlingLabel");
2133
2655
  const exampleLabel = text("exampleLabel");
2134
2656
  const copyCodeLabel = text("copyCode");
2135
- elements.nativeCodeGrid.innerHTML = nativeCodeEntries.map((entry, index) => {
2657
+
2658
+ if (elements.nativeCodeCategories) {
2659
+ elements.nativeCodeCategories.innerHTML = nativeCodeCategories.map((category) => {
2660
+ const title = category.title[language] || category.title.pt;
2661
+ const description = category.description[language] || category.description.pt;
2662
+ const count = categoryCounts[category.id] || 0;
2663
+ const active = category.id === activeCategory ? " active" : "";
2664
+ return `
2665
+ <button type="button" class="code-category-button${active}" data-code-category="${escapeHtml(category.id)}" aria-pressed="${category.id === activeCategory}">
2666
+ <strong>${escapeHtml(title)}</strong>
2667
+ <span class="code-category-count">${count}</span>
2668
+ <small>${escapeHtml(description)}</small>
2669
+ </button>
2670
+ `;
2671
+ }).join("");
2672
+ }
2673
+
2674
+ if (elements.nativeCodeSummary) {
2675
+ const title = activeCategoryMeta.title[language] || activeCategoryMeta.title.pt;
2676
+ const description = activeCategoryMeta.description[language] || activeCategoryMeta.description.pt;
2677
+ elements.nativeCodeSummary.innerHTML = `
2678
+ <strong>${escapeHtml(title)} · ${escapeHtml(text("codesShowing"))} ${visibleEntries.length} ${escapeHtml(text("codesItems"))}</strong>
2679
+ <p>${escapeHtml(description)}</p>
2680
+ `;
2681
+ }
2682
+
2683
+ elements.nativeCodeGrid.innerHTML = visibleEntries.map(({ entry, index }) => {
2136
2684
  const syntax = entry.syntax ? entry.syntax[language] || entry.syntax.pt : entry.js;
2137
2685
  const description = entry.description[language] || entry.description.pt;
2138
2686
  const returns = entry.returns[language] || entry.returns.pt;
2139
2687
  const handling = entry.handling[language] || entry.handling.pt;
2140
2688
  const recipe = recipeForCode(index);
2689
+ const highlightedSyntax = highlightSource(syntax, "js");
2690
+ const highlightedExample = recipe.example ? highlightSource(recipe.example, "js") : "";
2141
2691
 
2142
2692
  return `
2143
2693
  <article class="code-card">
2144
2694
  <div class="code-card-top">
2145
- <code>${escapeHtml(syntax)}</code>
2695
+ <code class="syntax-inline">${highlightedSyntax}</code>
2146
2696
  <span>${escapeHtml(javaLabel)}: ${escapeHtml(entry.java)}</span>
2147
2697
  </div>
2148
2698
  <p><strong>${escapeHtml(doesLabel)}:</strong> ${escapeHtml(description)}</p>
@@ -2154,7 +2704,7 @@ function renderNativeCodeGrid() {
2154
2704
  <strong>${escapeHtml(exampleLabel)}</strong>
2155
2705
  <button type="button" class="copy-code-button" data-copy-code="${index}">${escapeHtml(copyCodeLabel)}</button>
2156
2706
  </div>
2157
- <pre><code>${escapeHtml(recipe.example)}</code></pre>
2707
+ <pre><code>${highlightedExample}</code></pre>
2158
2708
  </div>
2159
2709
  </article>
2160
2710
  `;
@@ -2521,7 +3071,9 @@ async function summarizeProject(project) {
2521
3071
  elements.fileLanguageBadge.textContent = "text";
2522
3072
  elements.fileEditorInput.value = "";
2523
3073
  elements.fileEditorInput.disabled = true;
2524
- elements.fileHighlight.innerHTML = "<code></code>";
3074
+ elements.fileEditorInput.scrollTop = 0;
3075
+ elements.fileEditorInput.scrollLeft = 0;
3076
+ updateFilePreview();
2525
3077
  elements.saveFileButton.disabled = true;
2526
3078
  populateSettings(project.config || {}, project);
2527
3079
  setStep("folder", project.hasEntryFile ? "done" : "active", project.hasEntryFile ? text("folderReady") : text("missing"));
@@ -2751,6 +3303,74 @@ async function runUsbDebugFlow() {
2751
3303
  }
2752
3304
  }
2753
3305
 
3306
+ async function runNativeFunctionLabFlow() {
3307
+ if (state.buildRunning) {
3308
+ setStatus("error", text("functionLabRunning"));
3309
+ return;
3310
+ }
3311
+ if (!api.runNativeFunctionLab) {
3312
+ setStatus("error", text("functionLabFail"));
3313
+ return;
3314
+ }
3315
+
3316
+ showLogBar();
3317
+ state.buildRunning = true;
3318
+ updateActionButtons();
3319
+ elements.resultPanel.classList.add("hidden");
3320
+ setView("build");
3321
+ setStatus("busy", text("functionLabRunning"));
3322
+ setStep("folder", "done", text("functionLabProject"));
3323
+ setStep("settings", "done", text("functionLabSettings"));
3324
+ setStep("doctor", "active", text("functionLabUsbCheck"));
3325
+ setStep("build", "active", text("functionLabRunning"));
3326
+ setProgress(20, text("functionLabUsbCheck"), "active");
3327
+ startAnimatedLogs();
3328
+
3329
+ try {
3330
+ const response = await api.runNativeFunctionLab();
3331
+ stopAnimatedLogs();
3332
+ if (!response.ok) {
3333
+ setStep("doctor", "error", text("functionLabUsbCheck"));
3334
+ setStep("build", "error", text("functionLabFail"));
3335
+ setStatus("error", text("functionLabFail"));
3336
+ setProgress(90, text("progressError"), "error");
3337
+ appendLog(response.message || text("functionLabFail"), "error");
3338
+ if (response.projectRoot) {
3339
+ appendLog(`${text("functionLabProject")}: ${response.projectRoot}`, "system");
3340
+ }
3341
+ if (response.buildDir) {
3342
+ appendLog(`Build directory kept: ${response.buildDir}`, "system");
3343
+ }
3344
+ return;
3345
+ }
3346
+
3347
+ const result = response.result;
3348
+ state.lastApkPath = result.apkPath;
3349
+ state.lastDistPath = result.distPath || "";
3350
+ elements.apkPath.textContent = result.apkPath;
3351
+ elements.successTitle.textContent = text("functionLabSuccessTitle");
3352
+ elements.successText.textContent = text("functionLabSuccessText");
3353
+ elements.successApkPath.textContent = result.apkPath;
3354
+ elements.resultPanel.classList.remove("hidden");
3355
+ setStep("doctor", "done", text("functionLabUsbCheck"));
3356
+ setStep("build", "done", text("functionLabOk"));
3357
+ setStatus("ready", text("functionLabOk"));
3358
+ setProgress(100, text("progressDone"));
3359
+ appendLog(`${text("functionLabOk")}: ${result.device && result.device.id ? result.device.id : "Android USB"}`, "success");
3360
+ appendLog(`${text("buildOk")}: ${result.apkPath}`, "success");
3361
+ setView("success");
3362
+ } catch (error) {
3363
+ stopAnimatedLogs();
3364
+ setStep("build", "error", text("functionLabFail"));
3365
+ setStatus("error", error.message);
3366
+ setProgress(90, text("progressError"), "error");
3367
+ appendLog(error.message, "error");
3368
+ } finally {
3369
+ state.buildRunning = false;
3370
+ updateActionButtons();
3371
+ }
3372
+ }
3373
+
2754
3374
  function toggleTheme() {
2755
3375
  state.theme = state.theme === "dark" ? "light" : "dark";
2756
3376
  applyTheme();
@@ -2804,6 +3424,7 @@ function bindEvents() {
2804
3424
  elements.doctorButton.addEventListener("click", runDoctorOnly);
2805
3425
  elements.buildButton.addEventListener("click", runBuildFlow);
2806
3426
  elements.usbDebugButton.addEventListener("click", runUsbDebugFlow);
3427
+ elements.nativeFunctionLabButton.addEventListener("click", runNativeFunctionLabFlow);
2807
3428
  elements.newFileButton.addEventListener("click", createNewProjectFile);
2808
3429
  elements.saveFileButton.addEventListener("click", saveCurrentFile);
2809
3430
  elements.fileTree.addEventListener("click", (event) => {
@@ -2816,6 +3437,7 @@ function bindEvents() {
2816
3437
  setCurrentFileDirty(true);
2817
3438
  updateFilePreview();
2818
3439
  });
3440
+ elements.fileEditorInput.addEventListener("scroll", syncFileEditorHighlightScroll);
2819
3441
  elements.clearLogsButton.addEventListener("click", clearLogs);
2820
3442
  elements.toggleLogsButton.addEventListener("click", toggleLogBar);
2821
3443
  elements.bottomToggleLogsButton.addEventListener("click", toggleLogBar);
@@ -2866,6 +3488,7 @@ function bindEvents() {
2866
3488
  });
2867
3489
  elements.permissionGrid.addEventListener("change", validateSettings);
2868
3490
  elements.nativeCodeGrid.addEventListener("click", handleNativeCodeCopy);
3491
+ elements.nativeCodeCategories.addEventListener("click", handleNativeCodeCopy);
2869
3492
  [
2870
3493
  elements.appNameInput,
2871
3494
  elements.packageIdInput,