html2apk 0.7.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.
- package/README.md +125 -1
- package/package.json +1 -1
- package/src/desktop/main.js +376 -0
- package/src/desktop/preload.js +1 -0
- package/src/desktop/renderer/index.html +12 -6
- package/src/desktop/renderer/renderer.js +659 -36
- package/src/desktop/renderer/styles.css +186 -27
- package/src/templates/cordova-plugin-html2apk-bridge/plugin.xml +17 -0
- package/src/templates/cordova-plugin-html2apk-bridge/src/android/Html2ApkBridge.java +2055 -82
- package/src/templates/cordova-plugin-html2apk-bridge/src/android/xml/html2apk_file_paths.xml +6 -0
- package/src/templates/cordova-plugin-html2apk-bridge/www/html2apk-bridge.js +407 -2
- package/src/templates/html2apk-early-bridge.js +404 -2
- package/src/templates/html2apk-runtime-console.js +156 -4
|
@@ -105,6 +105,35 @@ const i18n = {
|
|
|
105
105
|
codesEyebrow: "Bridge nativa",
|
|
106
106
|
codesTitle: "Codigos interpretados",
|
|
107
107
|
codesIntro: "Use estes blocos no JavaScript do seu projeto. O html2apk espera o Android ficar pronto, pede permissoes quando precisar e abre a configuracao certa quando o Android bloquear o pop-up.",
|
|
108
|
+
testNativeFunctions: "Testar funcoes",
|
|
109
|
+
functionLabRunning: "Gerando app de teste e abrindo no USB",
|
|
110
|
+
functionLabOk: "App de teste aberto no celular",
|
|
111
|
+
functionLabFail: "Nao foi possivel testar as funcoes",
|
|
112
|
+
functionLabProject: "App de teste criado",
|
|
113
|
+
functionLabSettings: "Configuracao de teste pronta",
|
|
114
|
+
functionLabUsbCheck: "Conferindo celular USB",
|
|
115
|
+
functionLabSuccessTitle: "App de teste aberto no celular",
|
|
116
|
+
functionLabSuccessText: "Use o celular conectado para experimentar as funcoes interpretadas e ver os retornos no proprio app.",
|
|
117
|
+
codesAll: "Tudo",
|
|
118
|
+
codesAllText: "Todas as funcoes interpretadas disponiveis no APK.",
|
|
119
|
+
codesFeedback: "Feedback",
|
|
120
|
+
codesFeedbackText: "Mensagens rapidas, vibracao e retornos simples.",
|
|
121
|
+
codesNotifications: "Notificacoes",
|
|
122
|
+
codesNotificationsText: "Notificacoes locais, agendamentos, loops e push remoto.",
|
|
123
|
+
codesPermissionsEvents: "Permissoes e eventos",
|
|
124
|
+
codesPermissionsEventsText: "Permissoes manuais, ciclo de vida, deep links e listeners nativos.",
|
|
125
|
+
codesMedia: "Midia e hardware",
|
|
126
|
+
codesMediaText: "Camera, QR Code, microfone, lanterna, imagens e videos.",
|
|
127
|
+
codesFiles: "Arquivos e dados",
|
|
128
|
+
codesFilesText: "Seletores, salvamento, CRUD interno, downloads e abertura de arquivos.",
|
|
129
|
+
codesShareNav: "Compartilhar e abrir",
|
|
130
|
+
codesShareNavText: "Compartilhamento, clipboard, URLs, WhatsApp, discador e mapas.",
|
|
131
|
+
codesDevice: "Tela e diagnostico",
|
|
132
|
+
codesDeviceText: "Tela ligada, brilho, tema, aparelho, rede, bateria, memoria e modo flutuante.",
|
|
133
|
+
codesSecurity: "Localizacao e seguranca",
|
|
134
|
+
codesSecurityText: "GPS, biometria e dados cifrados pelo Android Keystore.",
|
|
135
|
+
codesShowing: "Mostrando",
|
|
136
|
+
codesItems: "funcoes",
|
|
108
137
|
javaLabel: "Motor nativo",
|
|
109
138
|
doesLabel: "O que faz",
|
|
110
139
|
whenUseLabel: "Quando usar",
|
|
@@ -291,6 +320,35 @@ const i18n = {
|
|
|
291
320
|
codesEyebrow: "Native bridge",
|
|
292
321
|
codesTitle: "Interpreted code",
|
|
293
322
|
codesIntro: "Use these blocks in your project JavaScript. html2apk waits for Android to be ready, asks permissions when needed and opens the right settings screen when Android blocks the prompt.",
|
|
323
|
+
testNativeFunctions: "Test functions",
|
|
324
|
+
functionLabRunning: "Generating test app and opening it over USB",
|
|
325
|
+
functionLabOk: "Test app opened on the phone",
|
|
326
|
+
functionLabFail: "Could not test the functions",
|
|
327
|
+
functionLabProject: "Test app created",
|
|
328
|
+
functionLabSettings: "Test settings ready",
|
|
329
|
+
functionLabUsbCheck: "Checking USB phone",
|
|
330
|
+
functionLabSuccessTitle: "Test app opened on the phone",
|
|
331
|
+
functionLabSuccessText: "Use the connected phone to try the interpreted functions and see the results inside the app.",
|
|
332
|
+
codesAll: "All",
|
|
333
|
+
codesAllText: "All interpreted functions available in the APK.",
|
|
334
|
+
codesFeedback: "Feedback",
|
|
335
|
+
codesFeedbackText: "Short messages, vibration and simple native feedback.",
|
|
336
|
+
codesNotifications: "Notifications",
|
|
337
|
+
codesNotificationsText: "Local notifications, schedules, loops and remote push.",
|
|
338
|
+
codesPermissionsEvents: "Permissions and events",
|
|
339
|
+
codesPermissionsEventsText: "Manual permissions, lifecycle, deep links and native listeners.",
|
|
340
|
+
codesMedia: "Media and hardware",
|
|
341
|
+
codesMediaText: "Camera, QR Code, microphone, flashlight, images and videos.",
|
|
342
|
+
codesFiles: "Files and data",
|
|
343
|
+
codesFilesText: "Pickers, saving, internal CRUD, downloads and file opening.",
|
|
344
|
+
codesShareNav: "Share and open",
|
|
345
|
+
codesShareNavText: "Sharing, clipboard, URLs, WhatsApp, dialer and maps.",
|
|
346
|
+
codesDevice: "Screen and diagnostics",
|
|
347
|
+
codesDeviceText: "Keep awake, brightness, theme, device, network, battery, memory and floating mode.",
|
|
348
|
+
codesSecurity: "Location and security",
|
|
349
|
+
codesSecurityText: "GPS, biometrics and Android Keystore encrypted data.",
|
|
350
|
+
codesShowing: "Showing",
|
|
351
|
+
codesItems: "functions",
|
|
294
352
|
javaLabel: "Native engine",
|
|
295
353
|
doesLabel: "What it does",
|
|
296
354
|
whenUseLabel: "When to use",
|
|
@@ -408,6 +466,11 @@ const permissionOptions = [
|
|
|
408
466
|
label: { pt: "Vibracao", en: "Vibration" },
|
|
409
467
|
detail: { pt: "Permite vibrar", en: "Allows vibration" }
|
|
410
468
|
},
|
|
469
|
+
{
|
|
470
|
+
value: "SET_WALLPAPER",
|
|
471
|
+
label: { pt: "Papel de parede", en: "Wallpaper" },
|
|
472
|
+
detail: { pt: "Permite definir imagem estatica", en: "Allows setting a static image" }
|
|
473
|
+
},
|
|
411
474
|
{
|
|
412
475
|
value: "CAMERA",
|
|
413
476
|
label: { pt: "Camera", en: "Camera" },
|
|
@@ -431,10 +494,23 @@ const permissionOptions = [
|
|
|
431
494
|
{
|
|
432
495
|
value: "SYSTEM_ALERT_WINDOW",
|
|
433
496
|
label: { pt: "Sobrepor apps", en: "Draw over apps" },
|
|
434
|
-
detail: { pt: "Necessaria
|
|
497
|
+
detail: { pt: "Necessaria para icone flutuante", en: "Required for floating icon" }
|
|
435
498
|
}
|
|
436
499
|
];
|
|
437
500
|
|
|
501
|
+
const nativeCodeCategories = [
|
|
502
|
+
{ id: "all", title: { pt: "Tudo", en: "All" }, description: { pt: "Todas as funcoes interpretadas disponiveis no APK.", en: "All interpreted functions available in the APK." } },
|
|
503
|
+
{ id: "feedback", title: { pt: "Feedback", en: "Feedback" }, description: { pt: "Mensagens rapidas, vibracao e retornos simples.", en: "Short messages, vibration and simple native feedback." } },
|
|
504
|
+
{ id: "notifications", title: { pt: "Notificacoes", en: "Notifications" }, description: { pt: "Notificacoes locais, agendamentos, loops e push remoto.", en: "Local notifications, schedules, loops and remote push." } },
|
|
505
|
+
{ id: "permissions", title: { pt: "Permissoes e eventos", en: "Permissions and events" }, description: { pt: "Permissoes manuais, ciclo de vida, deep links e listeners nativos.", en: "Manual permissions, lifecycle, deep links and native listeners." } },
|
|
506
|
+
{ id: "media", title: { pt: "Midia e hardware", en: "Media and hardware" }, description: { pt: "Camera, QR Code, microfone, lanterna, imagens e videos.", en: "Camera, QR Code, microphone, flashlight, images and videos." } },
|
|
507
|
+
{ id: "wallpaper", title: { pt: "Papel de parede", en: "Wallpaper" }, description: { pt: "Imagem estatica na tela inicial/bloqueio e atalho para ajustes nativos.", en: "Static images for home/lock screen and shortcut to native settings." } },
|
|
508
|
+
{ id: "files", title: { pt: "Arquivos e dados", en: "Files and data" }, description: { pt: "Seletores, salvamento, CRUD interno, downloads e abertura de arquivos.", en: "Pickers, saving, internal CRUD, downloads and file opening." } },
|
|
509
|
+
{ id: "share", title: { pt: "Compartilhar e abrir", en: "Share and open" }, description: { pt: "Compartilhamento, clipboard, URLs, WhatsApp, discador e mapas.", en: "Sharing, clipboard, URLs, WhatsApp, dialer and maps." } },
|
|
510
|
+
{ id: "device", title: { pt: "Tela e diagnostico", en: "Screen and diagnostics" }, description: { pt: "Tela ligada, brilho, tema, aparelho, rede, bateria, memoria e modo flutuante.", en: "Keep awake, brightness, theme, device, network, battery, memory and floating mode." } },
|
|
511
|
+
{ id: "security", title: { pt: "Localizacao e seguranca", en: "Location and security" }, description: { pt: "GPS, biometria e dados cifrados pelo Android Keystore.", en: "GPS, biometrics and Android Keystore encrypted data." } }
|
|
512
|
+
];
|
|
513
|
+
|
|
438
514
|
const nativeCodeEntries = [
|
|
439
515
|
{
|
|
440
516
|
syntax: { pt: "toast('Mensagem')", en: "toast('Message')" },
|
|
@@ -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
|
|
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
|
|
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, """);
|
|
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
|
-
|
|
1771
|
-
.replace(/(<!--[\s\S]*?-->)/g, "<span class=\"syntax-token-comment\">$1</span>")
|
|
1772
|
-
.replace(/(<\/?[a-zA-Z][^&]*?>)/g, "<span class=\"syntax-token-tag\">$1</span>");
|
|
1773
|
-
return html;
|
|
2274
|
+
return highlightHtmlLike(value);
|
|
1774
2275
|
}
|
|
1775
|
-
|
|
1776
2276
|
if (["js", "mjs", "cjs", "ts", "tsx", "jsx"].includes(lang)) {
|
|
1777
|
-
|
|
1778
|
-
.replace(/\b(await|async|const|let|var|function|return|if|else|for|while|try|catch|throw|new|class|import|export|from|true|false|null|undefined)\b/g, "<span class=\"syntax-token-keyword\">$1</span>")
|
|
1779
|
-
.replace(/\b(\d+(?:\.\d+)?)\b/g, "<span class=\"syntax-token-number\">$1</span>")
|
|
1780
|
-
.replace(/(\/\*[\s\S]*?\*\/|\/\/[^\n\r]*)/g, "<span class=\"syntax-token-comment\">$1</span>")
|
|
1781
|
-
.replace(/(".*?"|'.*?'|`[\s\S]*?`)/g, "<span class=\"syntax-token-string\">$1</span>");
|
|
1782
|
-
return html;
|
|
2277
|
+
return highlightJavaScript(value);
|
|
1783
2278
|
}
|
|
1784
|
-
|
|
1785
2279
|
if (lang === "css") {
|
|
1786
|
-
|
|
1787
|
-
.replace(/(\/\*[\s\S]*?\*\/)/g, "<span class=\"syntax-token-comment\">$1</span>")
|
|
1788
|
-
.replace(/([.#]?[a-zA-Z0-9_-]+)(\s*\{)/g, "<span class=\"syntax-token-tag\">$1</span>$2")
|
|
1789
|
-
.replace(/(:\s*)([^;\n]+)(;?)/g, "$1<span class=\"syntax-token-string\">$2</span>$3");
|
|
1790
|
-
return html;
|
|
2280
|
+
return highlightCss(value);
|
|
1791
2281
|
}
|
|
1792
|
-
|
|
1793
2282
|
if (lang === "json") {
|
|
1794
|
-
|
|
1795
|
-
.replace(/("[^&]+")(\s*:)/g, "<span class=\"syntax-token-keyword\">$1</span>$2")
|
|
1796
|
-
.replace(/(:\s*)(".*?")/g, "$1<span class=\"syntax-token-string\">$2</span>")
|
|
1797
|
-
.replace(/\b(true|false|null)\b/g, "<span class=\"syntax-token-keyword\">$1</span>")
|
|
1798
|
-
.replace(/\b(\d+(?:\.\d+)?)\b/g, "<span class=\"syntax-token-number\">$1</span>");
|
|
2283
|
+
return highlightJson(value);
|
|
1799
2284
|
}
|
|
1800
2285
|
|
|
1801
|
-
return
|
|
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.
|
|
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
|
|
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
|
-
|
|
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>${
|
|
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>${
|
|
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.
|
|
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,
|
|
@@ -2980,7 +3603,7 @@ async function init() {
|
|
|
2980
3603
|
elements.iconPreview.src = iconPreviewPath(state.defaultIconPath);
|
|
2981
3604
|
}
|
|
2982
3605
|
} catch {
|
|
2983
|
-
elements.appVersion.textContent = "v0.
|
|
3606
|
+
elements.appVersion.textContent = "v0.7.0";
|
|
2984
3607
|
}
|
|
2985
3608
|
|
|
2986
3609
|
setTimeout(finishBoot, 1800);
|