html2apk 0.5.0 → 0.7.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.
@@ -10,6 +10,7 @@ const i18n = {
10
10
  navSettings: "Configuracoes",
11
11
  navAppearance: "Aparencia",
12
12
  navBuild: "Build",
13
+ navFiles: "Arquivos",
13
14
  navCodes: "Codigos",
14
15
  navLogs: "Logs",
15
16
  navHelp: "Ajuda",
@@ -32,6 +33,10 @@ const i18n = {
32
33
  appName: "Nome do app",
33
34
  packageId: "Package ID",
34
35
  appVersion: "Versao do app",
36
+ buildFormat: "Formato",
37
+ formatApk: "APK",
38
+ formatAab: "AAB",
39
+ buildFormatHint: "APK para instalar direto; AAB para loja.",
35
40
  mode: "Modo",
36
41
  chooseMode: "Escolha o modo",
37
42
  modeFullscreen: "Tela cheia",
@@ -51,13 +56,35 @@ const i18n = {
51
56
  oneSignalAppIdHint: "Opcional. Use o App ID do OneSignal, nao a REST API Key.",
52
57
  androidPermissions: "Permissoes Android",
53
58
  chooseIcon: "Trocar icone PNG",
59
+ keystoreTitle: "Assinatura / keystore",
60
+ keystoreFile: "Arquivo keystore",
61
+ chooseKeystore: "Escolher",
62
+ keystoreAlias: "Alias",
63
+ keystoreStorePassword: "Senha da store",
64
+ keystoreKeyPassword: "Senha da key",
65
+ keystoreHint: "Opcional para APK debug. Para AAB/release, preencha para assinar o arquivo.",
54
66
  reviewBuild: "Revisar build",
55
67
  debugBuild: "Debug tecnico",
56
68
  debugBuildText: "Mantem a pasta Cordova temporaria para inspecao.",
69
+ runtimeLogsBuild: "Console no APK",
70
+ runtimeLogsBuildText: "Mostra um modal Console no app gerado para depurar erros e funcoes interpretadas.",
57
71
  releaseBuild: "Release",
58
72
  releaseBuildText: "Usa configuracao de assinatura se houver keystore.",
59
73
  appearanceEyebrow: "Preferencias",
60
74
  appearanceTitle: "Idioma e tema",
75
+ filesEyebrow: "Editor visual",
76
+ filesTitle: "Arquivos do projeto",
77
+ fileTreeTitle: "Pastas e arquivos",
78
+ newFile: "Novo arquivo",
79
+ saveFile: "Salvar",
80
+ noFileSelected: "Nenhum arquivo selecionado",
81
+ syntaxPreview: "Previa com sintaxe",
82
+ newFilePrompt: "Digite o caminho do novo arquivo dentro do projeto:",
83
+ fileSaved: "Arquivo salvo",
84
+ fileCreated: "Arquivo criado",
85
+ fileOpenFail: "Nao foi possivel abrir o arquivo",
86
+ fileSaveFail: "Nao foi possivel salvar o arquivo",
87
+ unsavedFileConfirm: "Ha alteracoes nao salvas. Deseja trocar de arquivo mesmo assim?",
61
88
  language: "Idioma",
62
89
  languageText: "Escolha como os feedbacks aparecem durante o build.",
63
90
  themeText: "O botao tambem fica na barra lateral para acesso rapido.",
@@ -133,7 +160,10 @@ const i18n = {
133
160
  invalidThemeColor: "Use uma cor hexadecimal valida, exemplo: #126fff.",
134
161
  invalidOneSignalAppId: "Use um OneSignal App ID valido ou deixe vazio.",
135
162
  invalidMinSdkVersion: "Escolha uma versao minima do Android entre API 24 e API 36.",
163
+ missingKeystoreForAab: "Para gerar AAB, informe o arquivo keystore, alias e senhas.",
164
+ incompleteKeystore: "Complete arquivo keystore, alias, senha da store e senha da key.",
136
165
  iconSelected: "Icone selecionado",
166
+ keystoreSelected: "Keystore selecionado",
137
167
  progressLabel: "Progresso",
138
168
  progressIdle: "Aguardando pasta",
139
169
  progressFolder: "Pasta recebida",
@@ -166,6 +196,7 @@ const i18n = {
166
196
  navSettings: "Settings",
167
197
  navAppearance: "Appearance",
168
198
  navBuild: "Build",
199
+ navFiles: "Files",
169
200
  navCodes: "Code",
170
201
  navLogs: "Logs",
171
202
  navHelp: "Help",
@@ -188,6 +219,10 @@ const i18n = {
188
219
  appName: "App name",
189
220
  packageId: "Package ID",
190
221
  appVersion: "App version",
222
+ buildFormat: "Format",
223
+ formatApk: "APK",
224
+ formatAab: "AAB",
225
+ buildFormatHint: "APK to install directly; AAB for stores.",
191
226
  mode: "Mode",
192
227
  chooseMode: "Choose mode",
193
228
  modeFullscreen: "Fullscreen",
@@ -207,13 +242,35 @@ const i18n = {
207
242
  oneSignalAppIdHint: "Optional. Use the OneSignal App ID, not the REST API Key.",
208
243
  androidPermissions: "Android permissions",
209
244
  chooseIcon: "Change PNG icon",
245
+ keystoreTitle: "Signing / keystore",
246
+ keystoreFile: "Keystore file",
247
+ chooseKeystore: "Choose",
248
+ keystoreAlias: "Alias",
249
+ keystoreStorePassword: "Store password",
250
+ keystoreKeyPassword: "Key password",
251
+ keystoreHint: "Optional for debug APK. For AAB/release, fill this to sign the file.",
210
252
  reviewBuild: "Review build",
211
253
  debugBuild: "Technical debug",
212
254
  debugBuildText: "Keeps the temporary Cordova folder for inspection.",
255
+ runtimeLogsBuild: "APK console",
256
+ runtimeLogsBuildText: "Shows a Console modal in the generated app to debug errors and interpreted functions.",
213
257
  releaseBuild: "Release",
214
258
  releaseBuildText: "Uses signing configuration when a keystore exists.",
215
259
  appearanceEyebrow: "Preferences",
216
260
  appearanceTitle: "Language and theme",
261
+ filesEyebrow: "Visual editor",
262
+ filesTitle: "Project files",
263
+ fileTreeTitle: "Folders and files",
264
+ newFile: "New file",
265
+ saveFile: "Save",
266
+ noFileSelected: "No file selected",
267
+ syntaxPreview: "Syntax preview",
268
+ newFilePrompt: "Enter the new file path inside the project:",
269
+ fileSaved: "File saved",
270
+ fileCreated: "File created",
271
+ fileOpenFail: "Could not open the file",
272
+ fileSaveFail: "Could not save the file",
273
+ unsavedFileConfirm: "There are unsaved changes. Switch files anyway?",
217
274
  language: "Language",
218
275
  languageText: "Choose how feedback appears during the build.",
219
276
  themeText: "The button also stays in the sidebar for quick access.",
@@ -289,7 +346,10 @@ const i18n = {
289
346
  invalidThemeColor: "Use a valid hex color, example: #126fff.",
290
347
  invalidOneSignalAppId: "Use a valid OneSignal App ID or leave it empty.",
291
348
  invalidMinSdkVersion: "Choose a minimum Android version between API 24 and API 36.",
349
+ missingKeystoreForAab: "To build AAB, enter the keystore file, alias and passwords.",
350
+ incompleteKeystore: "Complete keystore file, alias, store password and key password.",
292
351
  iconSelected: "Icon selected",
352
+ keystoreSelected: "Keystore selected",
293
353
  progressLabel: "Progress",
294
354
  progressIdle: "Waiting for folder",
295
355
  progressFolder: "Folder received",
@@ -1359,6 +1419,10 @@ const state = {
1359
1419
  lastApkPath: null,
1360
1420
  lastDistPath: null,
1361
1421
  defaultIconPath: "",
1422
+ fileTree: [],
1423
+ currentFilePath: "",
1424
+ currentFileLanguage: "text",
1425
+ currentFileDirty: false,
1362
1426
  animationTimer: null,
1363
1427
  progress: 0,
1364
1428
  logsVisible: localStorage.getItem("html2apk.logsVisible") === "true"
@@ -1398,6 +1462,7 @@ function collectElements() {
1398
1462
  "appNameInput",
1399
1463
  "packageIdInput",
1400
1464
  "versionInput",
1465
+ "buildFormatInput",
1401
1466
  "modeInput",
1402
1467
  "orientationInput",
1403
1468
  "minSdkVersionInput",
@@ -1410,9 +1475,15 @@ function collectElements() {
1410
1475
  "iconPathInput",
1411
1476
  "iconPreview",
1412
1477
  "selectIconButton",
1478
+ "keystorePathInput",
1479
+ "selectKeystoreButton",
1480
+ "keystoreAliasInput",
1481
+ "keystoreStorePasswordInput",
1482
+ "keystoreKeyPasswordInput",
1413
1483
  "settingsValidation",
1414
1484
  "settingsNextButton",
1415
1485
  "debugInput",
1486
+ "runtimeLogsInput",
1416
1487
  "releaseInput",
1417
1488
  "stepFolderText",
1418
1489
  "stepSettingsText",
@@ -1433,6 +1504,13 @@ function collectElements() {
1433
1504
  "successOpenDistButton",
1434
1505
  "successShowApkButton",
1435
1506
  "newBuildButton",
1507
+ "newFileButton",
1508
+ "saveFileButton",
1509
+ "fileTree",
1510
+ "currentFileName",
1511
+ "fileLanguageBadge",
1512
+ "fileEditorInput",
1513
+ "fileHighlight",
1436
1514
  "logConsole",
1437
1515
  "bottomLogConsole",
1438
1516
  "clearLogsButton",
@@ -1469,6 +1547,12 @@ function applyLanguage() {
1469
1547
  applyLogBarVisibility();
1470
1548
  renderPermissionOptions(selectedPermissions());
1471
1549
  renderNativeCodeGrid();
1550
+ renderFileTree();
1551
+ if (state.currentFilePath && elements.currentFileName) {
1552
+ elements.currentFileName.textContent = `${state.currentFilePath}${state.currentFileDirty ? " *" : ""}`;
1553
+ } else if (elements.currentFileName) {
1554
+ elements.currentFileName.textContent = text("noFileSelected");
1555
+ }
1472
1556
  if (state.project) {
1473
1557
  validateSettings();
1474
1558
  renderReview();
@@ -1607,6 +1691,7 @@ function updateActionButtons() {
1607
1691
  elements.nextSettingsButton.disabled = !hasProject || isBusy || !state.environmentOk;
1608
1692
  elements.doctorButton.disabled = !hasProject || isBusy;
1609
1693
  elements.settingsNextButton.disabled = !hasProject || !state.settingsValid || !state.environmentOk || isBusy;
1694
+ elements.newFileButton.disabled = !hasProject;
1610
1695
  setBuildButtons(hasProject && state.settingsValid && state.environmentOk && !isBusy);
1611
1696
  }
1612
1697
 
@@ -1677,6 +1762,182 @@ function escapeHtml(value) {
1677
1762
  .replace(/"/g, """);
1678
1763
  }
1679
1764
 
1765
+ function highlightSource(value, language) {
1766
+ let html = escapeHtml(value);
1767
+ const lang = String(language || "").toLowerCase();
1768
+
1769
+ 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;
1774
+ }
1775
+
1776
+ 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;
1783
+ }
1784
+
1785
+ 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;
1791
+ }
1792
+
1793
+ 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>");
1799
+ }
1800
+
1801
+ return html;
1802
+ }
1803
+
1804
+ function updateFilePreview() {
1805
+ if (!elements.fileHighlight) {
1806
+ return;
1807
+ }
1808
+
1809
+ elements.fileHighlight.innerHTML = `<code>${highlightSource(elements.fileEditorInput.value, state.currentFileLanguage)}</code>`;
1810
+ }
1811
+
1812
+ function setCurrentFileDirty(value) {
1813
+ state.currentFileDirty = Boolean(value);
1814
+ elements.saveFileButton.disabled = !state.currentFilePath || !state.currentFileDirty;
1815
+ if (state.currentFilePath) {
1816
+ elements.currentFileName.textContent = `${state.currentFilePath}${state.currentFileDirty ? " *" : ""}`;
1817
+ }
1818
+ }
1819
+
1820
+ function renderFileNodes(nodes, depth = 0) {
1821
+ return (nodes || []).map((node) => {
1822
+ const padding = 8 + (depth * 16);
1823
+ if (node.type === "directory") {
1824
+ return `
1825
+ <div class="folder-row" style="padding-left:${padding}px">${escapeHtml(node.name)}</div>
1826
+ ${renderFileNodes(node.children, depth + 1)}
1827
+ `;
1828
+ }
1829
+
1830
+ const active = node.path === state.currentFilePath ? " active" : "";
1831
+ const disabled = node.editable ? "" : " disabled";
1832
+ const marker = node.editable ? "file" : "bin";
1833
+ return `
1834
+ <button class="file-row${active}" type="button" style="padding-left:${padding}px" data-file-path="${escapeHtml(node.path)}"${disabled}>
1835
+ <span>${marker}</span>
1836
+ <strong>${escapeHtml(node.name)}</strong>
1837
+ </button>
1838
+ `;
1839
+ }).join("");
1840
+ }
1841
+
1842
+ function renderFileTree() {
1843
+ if (!elements.fileTree) {
1844
+ return;
1845
+ }
1846
+
1847
+ if (!state.project) {
1848
+ elements.fileTree.className = "file-tree-empty";
1849
+ elements.fileTree.textContent = text("chooseProjectFirst");
1850
+ return;
1851
+ }
1852
+
1853
+ if (!state.fileTree.length) {
1854
+ elements.fileTree.className = "file-tree-empty";
1855
+ elements.fileTree.textContent = text("missing");
1856
+ return;
1857
+ }
1858
+
1859
+ elements.fileTree.className = "file-tree";
1860
+ elements.fileTree.innerHTML = renderFileNodes(state.fileTree);
1861
+ }
1862
+
1863
+ async function refreshProjectFiles() {
1864
+ if (!state.project || !api.listProjectFiles) {
1865
+ renderFileTree();
1866
+ return;
1867
+ }
1868
+
1869
+ try {
1870
+ state.fileTree = await api.listProjectFiles(state.project.projectRoot);
1871
+ renderFileTree();
1872
+ } catch (error) {
1873
+ appendLog(`${text("projectWatcherFail")}: ${error.message}`, "error");
1874
+ }
1875
+ }
1876
+
1877
+ async function openProjectFile(relativePath) {
1878
+ if (!state.project || !relativePath) {
1879
+ return;
1880
+ }
1881
+
1882
+ if (state.currentFileDirty && !window.confirm(text("unsavedFileConfirm"))) {
1883
+ return;
1884
+ }
1885
+
1886
+ try {
1887
+ const file = await api.readProjectFile(state.project.projectRoot, relativePath);
1888
+ state.currentFilePath = file.path;
1889
+ state.currentFileLanguage = file.language || "text";
1890
+ elements.currentFileName.textContent = file.path;
1891
+ elements.fileLanguageBadge.textContent = state.currentFileLanguage;
1892
+ elements.fileEditorInput.disabled = false;
1893
+ elements.fileEditorInput.value = file.content || "";
1894
+ setCurrentFileDirty(false);
1895
+ updateFilePreview();
1896
+ renderFileTree();
1897
+ } catch (error) {
1898
+ appendLog(`${text("fileOpenFail")}: ${error.message}`, "error");
1899
+ setStatus("error", text("fileOpenFail"));
1900
+ }
1901
+ }
1902
+
1903
+ async function saveCurrentFile() {
1904
+ if (!state.project || !state.currentFilePath) {
1905
+ return;
1906
+ }
1907
+
1908
+ try {
1909
+ await api.writeProjectFile(state.project.projectRoot, state.currentFilePath, elements.fileEditorInput.value);
1910
+ setCurrentFileDirty(false);
1911
+ appendLog(`${text("fileSaved")}: ${state.currentFilePath}`, "success");
1912
+ await refreshProjectFiles();
1913
+ } catch (error) {
1914
+ appendLog(`${text("fileSaveFail")}: ${error.message}`, "error");
1915
+ setStatus("error", text("fileSaveFail"));
1916
+ }
1917
+ }
1918
+
1919
+ async function createNewProjectFile() {
1920
+ if (!state.project) {
1921
+ setStatus("error", text("chooseProjectFirst"));
1922
+ return;
1923
+ }
1924
+
1925
+ const relativePath = window.prompt(text("newFilePrompt"), "js/app.js");
1926
+ if (!relativePath) {
1927
+ return;
1928
+ }
1929
+
1930
+ try {
1931
+ const file = await api.createProjectFile(state.project.projectRoot, relativePath);
1932
+ appendLog(`${text("fileCreated")}: ${file.path}`, "success");
1933
+ await refreshProjectFiles();
1934
+ await openProjectFile(file.path);
1935
+ } catch (error) {
1936
+ appendLog(`${text("fileSaveFail")}: ${error.message}`, "error");
1937
+ setStatus("error", text("fileSaveFail"));
1938
+ }
1939
+ }
1940
+
1680
1941
  function recipeForCode(index) {
1681
1942
  const language = currentLanguage();
1682
1943
  const recipe = nativeCodeRecipes[index] || {};
@@ -1808,10 +2069,41 @@ function normalizeThemeMode(value) {
1808
2069
  return String(value || "").trim().toLowerCase() === "auto" ? "auto" : "fixed";
1809
2070
  }
1810
2071
 
2072
+ function normalizeBuildFormat(value) {
2073
+ return String(value || "").trim().toLowerCase() === "aab" ? "aab" : "apk";
2074
+ }
2075
+
1811
2076
  function normalizeOneSignalAppId(value) {
1812
2077
  return String(value || "").trim();
1813
2078
  }
1814
2079
 
2080
+ function keystoreFromConfig(config = {}) {
2081
+ const keystore = config.keystore && typeof config.keystore === "object" ? config.keystore : {};
2082
+ return {
2083
+ path: String(keystore.path || "").trim(),
2084
+ alias: String(keystore.alias || "").trim(),
2085
+ storePassword: String(keystore.storePassword || keystore.password || "").trim(),
2086
+ keyPassword: String(keystore.keyPassword || keystore.password || "").trim()
2087
+ };
2088
+ }
2089
+
2090
+ function keystoreFromInputs() {
2091
+ return {
2092
+ path: elements.keystorePathInput.value.trim(),
2093
+ alias: elements.keystoreAliasInput.value.trim(),
2094
+ storePassword: elements.keystoreStorePasswordInput.value,
2095
+ keyPassword: elements.keystoreKeyPasswordInput.value
2096
+ };
2097
+ }
2098
+
2099
+ function hasAnyKeystoreField(keystore) {
2100
+ return Boolean(keystore.path || keystore.alias || keystore.storePassword || keystore.keyPassword);
2101
+ }
2102
+
2103
+ function hasCompleteKeystore(keystore) {
2104
+ return Boolean(keystore.path && keystore.alias && keystore.storePassword && keystore.keyPassword);
2105
+ }
2106
+
1815
2107
  function oneSignalAppIdFromConfig(config = {}) {
1816
2108
  return normalizeOneSignalAppId(
1817
2109
  config.oneSignalAppId
@@ -1874,6 +2166,7 @@ function populateSettings(config = {}, project = state.project) {
1874
2166
  elements.appNameInput.value = config.appName || projectName || "";
1875
2167
  elements.packageIdInput.value = config.packageId || `com.html2apk.${packageSegment(projectName)}`;
1876
2168
  elements.versionInput.value = config.version || "1.0.0";
2169
+ elements.buildFormatInput.value = normalizeBuildFormat(config.buildFormat || config.outputFormat || config.artifactType || config.packageType);
1877
2170
  elements.modeInput.value = config.mode || "fullscreen";
1878
2171
  elements.orientationInput.value = normalizeOrientationInputValue(config.orientation);
1879
2172
  elements.minSdkVersionInput.value = String(normalizeMinSdkVersion(config.minSdkVersion || config.androidMinSdkVersion));
@@ -1887,14 +2180,22 @@ function populateSettings(config = {}, project = state.project) {
1887
2180
  const iconPath = String(config.icon || "").trim() || defaultIconPath();
1888
2181
  elements.iconPathInput.value = iconPath;
1889
2182
  elements.iconPreview.src = iconPreviewPath(iconPath);
2183
+ const keystore = keystoreFromConfig(config);
2184
+ elements.keystorePathInput.value = keystore.path;
2185
+ elements.keystoreAliasInput.value = keystore.alias;
2186
+ elements.keystoreStorePasswordInput.value = keystore.storePassword;
2187
+ elements.keystoreKeyPasswordInput.value = keystore.keyPassword;
1890
2188
  elements.debugInput.checked = Boolean(config.debug);
1891
- elements.releaseInput.checked = Boolean(config.release);
2189
+ elements.runtimeLogsInput.checked = Boolean(config.showRuntimeLogs || config.mostrarLogs || config.runtimeLogs || config.debugConsole || config.console);
2190
+ elements.releaseInput.checked = Boolean(config.release || elements.buildFormatInput.value === "aab");
1892
2191
  }
1893
2192
 
1894
2193
  function validateSettings() {
1895
2194
  const errors = [];
1896
2195
  const packagePattern = /^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)+$/;
1897
2196
  const versionPattern = /^\d+\.\d+\.\d+([-.+][0-9A-Za-z.-]+)?$/;
2197
+ const buildFormat = normalizeBuildFormat(elements.buildFormatInput.value);
2198
+ const keystore = keystoreFromInputs();
1898
2199
 
1899
2200
  if (!state.project) {
1900
2201
  errors.push(text("missingProject"));
@@ -1920,6 +2221,11 @@ function validateSettings() {
1920
2221
  if (!isValidOptionalOneSignalAppId(elements.oneSignalAppIdInput.value)) {
1921
2222
  errors.push(text("invalidOneSignalAppId"));
1922
2223
  }
2224
+ if (buildFormat === "aab" && !hasCompleteKeystore(keystore)) {
2225
+ errors.push(text("missingKeystoreForAab"));
2226
+ } else if (hasAnyKeystoreField(keystore) && !hasCompleteKeystore(keystore)) {
2227
+ errors.push(text("incompleteKeystore"));
2228
+ }
1923
2229
  if (elements.iconPathInput.value.trim() && !/\.png$/i.test(elements.iconPathInput.value.trim())) {
1924
2230
  errors.push(text("invalidIconType"));
1925
2231
  }
@@ -1960,6 +2266,7 @@ function renderReview() {
1960
2266
  [text("appName"), elements.appNameInput.value.trim()],
1961
2267
  [text("packageId"), elements.packageIdInput.value.trim()],
1962
2268
  [text("appVersion"), elements.versionInput.value.trim()],
2269
+ [text("buildFormat"), selectedOptionText(elements.buildFormatInput)],
1963
2270
  [text("mode"), selectedOptionText(elements.modeInput)],
1964
2271
  [text("orientation"), selectedOptionText(elements.orientationInput)],
1965
2272
  [text("minSdkVersion"), selectedOptionText(elements.minSdkVersionInput)],
@@ -1967,7 +2274,10 @@ function renderReview() {
1967
2274
  [text("appThemeColor"), elements.themeColorTextInput.value.trim()],
1968
2275
  [text("oneSignalAppId"), elements.oneSignalAppIdInput.value.trim() || "-"],
1969
2276
  [text("androidPermissions"), selectedPermissions().join(", ")],
1970
- [text("appIcon"), displayIconValue(elements.iconPathInput.value.trim())]
2277
+ [text("appIcon"), displayIconValue(elements.iconPathInput.value.trim())],
2278
+ [text("keystoreTitle"), elements.keystorePathInput.value.trim() ? elements.keystorePathInput.value.trim() : "-"],
2279
+ [text("runtimeLogsBuild"), elements.runtimeLogsInput.checked ? text("selected") : "-"],
2280
+ [text("releaseBuild"), elements.releaseInput.checked || normalizeBuildFormat(elements.buildFormatInput.value) === "aab" ? text("selected") : "-"]
1971
2281
  ];
1972
2282
 
1973
2283
  elements.reviewGrid.innerHTML = items.map(([label, value]) => `
@@ -2166,6 +2476,7 @@ function applyProjectChange(payload) {
2166
2476
  }
2167
2477
 
2168
2478
  renderProjectSnapshot(payload.project);
2479
+ refreshProjectFiles();
2169
2480
 
2170
2481
  const reloadSettings = isConfigFilePath(payload.changedPath);
2171
2482
  if (reloadSettings && !state.buildRunning) {
@@ -2180,6 +2491,14 @@ function applyProjectChange(payload) {
2180
2491
  if (document.querySelector(".nav-item.active")?.dataset.view === "build") {
2181
2492
  renderReview();
2182
2493
  }
2494
+ if (
2495
+ state.currentFilePath &&
2496
+ payload.changedPath &&
2497
+ String(payload.changedPath).replace(/\\/g, "/").endsWith(`/${state.currentFilePath}`) &&
2498
+ !state.currentFileDirty
2499
+ ) {
2500
+ openProjectFile(state.currentFilePath);
2501
+ }
2183
2502
  }
2184
2503
 
2185
2504
  async function summarizeProject(project) {
@@ -2194,6 +2513,16 @@ async function summarizeProject(project) {
2194
2513
  elements.nextSettingsButton.disabled = false;
2195
2514
  elements.doctorButton.disabled = true;
2196
2515
  setBuildButtons(false);
2516
+ state.fileTree = [];
2517
+ state.currentFilePath = "";
2518
+ state.currentFileLanguage = "text";
2519
+ state.currentFileDirty = false;
2520
+ elements.currentFileName.textContent = text("noFileSelected");
2521
+ elements.fileLanguageBadge.textContent = "text";
2522
+ elements.fileEditorInput.value = "";
2523
+ elements.fileEditorInput.disabled = true;
2524
+ elements.fileHighlight.innerHTML = "<code></code>";
2525
+ elements.saveFileButton.disabled = true;
2197
2526
  populateSettings(project.config || {}, project);
2198
2527
  setStep("folder", project.hasEntryFile ? "done" : "active", project.hasEntryFile ? text("folderReady") : text("missing"));
2199
2528
  setStep("settings", "active", text("settingsMissing"));
@@ -2203,6 +2532,7 @@ async function summarizeProject(project) {
2203
2532
  setStatus("ready", text("projectLoaded"));
2204
2533
  appendLog(`${text("droppedFolder")}: ${project.projectRoot}`, "system");
2205
2534
  validateSettings();
2535
+ await refreshProjectFiles();
2206
2536
  setProgress(project.hasEntryFile ? 25 : 15, project.hasEntryFile ? text("progressFolder") : text("missing"), project.hasEntryFile ? "" : "error");
2207
2537
  await watchCurrentProject();
2208
2538
  await ensureEnvironmentBeforeSettings();
@@ -2240,11 +2570,14 @@ async function runDoctorOnly() {
2240
2570
  }
2241
2571
 
2242
2572
  function buildOptions() {
2243
- return {
2573
+ const buildFormat = normalizeBuildFormat(elements.buildFormatInput.value);
2574
+ const keystore = keystoreFromInputs();
2575
+ const options = {
2244
2576
  projectRoot: state.project.projectRoot,
2245
2577
  appName: elements.appNameInput.value.trim(),
2246
2578
  packageId: elements.packageIdInput.value.trim(),
2247
2579
  version: elements.versionInput.value.trim(),
2580
+ buildFormat,
2248
2581
  mode: elements.modeInput.value,
2249
2582
  orientation: elements.orientationInput.value,
2250
2583
  minSdkVersion: normalizeMinSdkVersion(elements.minSdkVersionInput.value),
@@ -2256,8 +2589,15 @@ function buildOptions() {
2256
2589
  icon: elements.iconPathInput.value.trim(),
2257
2590
  androidPlatform: elements.androidPlatformInput.value.trim(),
2258
2591
  debug: elements.debugInput.checked,
2259
- release: elements.releaseInput.checked
2592
+ showRuntimeLogs: elements.runtimeLogsInput.checked,
2593
+ release: elements.releaseInput.checked || buildFormat === "aab"
2260
2594
  };
2595
+
2596
+ if (hasAnyKeystoreField(keystore)) {
2597
+ options.keystore = keystore;
2598
+ }
2599
+
2600
+ return options;
2261
2601
  }
2262
2602
 
2263
2603
  function startAnimatedLogs() {
@@ -2445,6 +2785,9 @@ function bindEvents() {
2445
2785
  if (button.dataset.view === "build" && !goToReview()) {
2446
2786
  return;
2447
2787
  }
2788
+ if (button.dataset.view === "files") {
2789
+ await refreshProjectFiles();
2790
+ }
2448
2791
  setView(button.dataset.view);
2449
2792
  });
2450
2793
  });
@@ -2461,6 +2804,18 @@ function bindEvents() {
2461
2804
  elements.doctorButton.addEventListener("click", runDoctorOnly);
2462
2805
  elements.buildButton.addEventListener("click", runBuildFlow);
2463
2806
  elements.usbDebugButton.addEventListener("click", runUsbDebugFlow);
2807
+ elements.newFileButton.addEventListener("click", createNewProjectFile);
2808
+ elements.saveFileButton.addEventListener("click", saveCurrentFile);
2809
+ elements.fileTree.addEventListener("click", (event) => {
2810
+ const button = event.target.closest("[data-file-path]");
2811
+ if (button && !button.disabled) {
2812
+ openProjectFile(button.dataset.filePath);
2813
+ }
2814
+ });
2815
+ elements.fileEditorInput.addEventListener("input", () => {
2816
+ setCurrentFileDirty(true);
2817
+ updateFilePreview();
2818
+ });
2464
2819
  elements.clearLogsButton.addEventListener("click", clearLogs);
2465
2820
  elements.toggleLogsButton.addEventListener("click", toggleLogBar);
2466
2821
  elements.bottomToggleLogsButton.addEventListener("click", toggleLogBar);
@@ -2475,6 +2830,21 @@ function bindEvents() {
2475
2830
  appendLog(`${text("iconSelected")}: ${iconPath}`, "system");
2476
2831
  validateSettings();
2477
2832
  });
2833
+ elements.selectKeystoreButton.addEventListener("click", async () => {
2834
+ const keystorePath = await api.selectKeystore();
2835
+ if (!keystorePath) {
2836
+ return;
2837
+ }
2838
+ elements.keystorePathInput.value = keystorePath;
2839
+ appendLog(`${text("keystoreSelected")}: ${keystorePath}`, "system");
2840
+ validateSettings();
2841
+ });
2842
+ elements.buildFormatInput.addEventListener("change", () => {
2843
+ if (normalizeBuildFormat(elements.buildFormatInput.value) === "aab") {
2844
+ elements.releaseInput.checked = true;
2845
+ }
2846
+ validateSettings();
2847
+ });
2478
2848
  elements.themeColorInput.addEventListener("input", () => {
2479
2849
  elements.themeColorTextInput.value = elements.themeColorInput.value;
2480
2850
  validateSettings();
@@ -2500,12 +2870,18 @@ function bindEvents() {
2500
2870
  elements.appNameInput,
2501
2871
  elements.packageIdInput,
2502
2872
  elements.versionInput,
2873
+ elements.buildFormatInput,
2503
2874
  elements.orientationInput,
2504
2875
  elements.minSdkVersionInput,
2505
2876
  elements.androidPlatformInput,
2506
2877
  elements.themeModeInput,
2507
2878
  elements.oneSignalAppIdInput,
2879
+ elements.keystorePathInput,
2880
+ elements.keystoreAliasInput,
2881
+ elements.keystoreStorePasswordInput,
2882
+ elements.keystoreKeyPasswordInput,
2508
2883
  elements.debugInput,
2884
+ elements.runtimeLogsInput,
2509
2885
  elements.releaseInput
2510
2886
  ].forEach((input) => {
2511
2887
  input.addEventListener("input", validateSettings);