html2apk 0.5.0 → 0.8.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.
@@ -24,6 +24,13 @@ const WINDOW_BACKGROUNDS = {
24
24
  light: "#f7fbff",
25
25
  dark: "#10141b"
26
26
  };
27
+ const EDITOR_MAX_FILE_SIZE = 1024 * 1024;
28
+ const EDITOR_IGNORED_DIRS = new Set([".git", "node_modules", "dist", "platforms", "build"]);
29
+ const EDITOR_TEXT_EXTENSIONS = new Set([
30
+ ".html", ".htm", ".css", ".js", ".mjs", ".cjs", ".json", ".xml", ".svg",
31
+ ".txt", ".md", ".ts", ".tsx", ".jsx", ".vue", ".scss", ".sass", ".less",
32
+ ".yml", ".yaml", ".properties", ".gradle", ".java", ".kt"
33
+ ]);
27
34
 
28
35
  app.commandLine.appendSwitch("disable-crash-reporter");
29
36
  process.title = APP_NAME;
@@ -139,6 +146,142 @@ async function inspectProject(projectRoot) {
139
146
  };
140
147
  }
141
148
 
149
+ function isInsideProject(projectRoot, targetPath) {
150
+ const relative = path.relative(projectRoot, targetPath);
151
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
152
+ }
153
+
154
+ function safeProjectPath(projectRoot, relativePath = "") {
155
+ const root = path.resolve(projectRoot);
156
+ const targetPath = path.resolve(root, relativePath || ".");
157
+ if (!isInsideProject(root, targetPath)) {
158
+ throw new Error("The file must stay inside the selected project folder.");
159
+ }
160
+ return targetPath;
161
+ }
162
+
163
+ function safeNewFilePath(projectRoot, relativePath) {
164
+ const cleaned = String(relativePath || "").trim().replace(/\\/g, "/");
165
+ if (!cleaned || path.isAbsolute(cleaned) || cleaned.split("/").some((part) => part === "..")) {
166
+ throw new Error("Use a relative path inside the project, example: css/style.css.");
167
+ }
168
+ return safeProjectPath(projectRoot, cleaned);
169
+ }
170
+
171
+ function fileLanguage(filePath) {
172
+ const ext = path.extname(filePath).toLowerCase().replace(".", "");
173
+ if (["htm", "html"].includes(ext)) {
174
+ return "html";
175
+ }
176
+ if (["js", "mjs", "cjs", "ts", "tsx", "jsx"].includes(ext)) {
177
+ return "js";
178
+ }
179
+ if (["css", "scss", "sass", "less"].includes(ext)) {
180
+ return "css";
181
+ }
182
+ if (ext === "json") {
183
+ return "json";
184
+ }
185
+ return ext || "text";
186
+ }
187
+
188
+ function isEditableFile(filePath, size) {
189
+ const ext = path.extname(filePath).toLowerCase();
190
+ return size <= EDITOR_MAX_FILE_SIZE && (EDITOR_TEXT_EXTENSIONS.has(ext) || ext === "");
191
+ }
192
+
193
+ async function listProjectFileTree(projectRoot, relativeDir = "") {
194
+ const absoluteDir = safeProjectPath(projectRoot, relativeDir);
195
+ const entries = await fs.readdir(absoluteDir, { withFileTypes: true });
196
+ const nodes = [];
197
+
198
+ for (const entry of entries.sort((a, b) => {
199
+ if (a.isDirectory() !== b.isDirectory()) {
200
+ return a.isDirectory() ? -1 : 1;
201
+ }
202
+ return a.name.localeCompare(b.name);
203
+ })) {
204
+ if (entry.name.startsWith(".html2apk-doctor-")) {
205
+ continue;
206
+ }
207
+
208
+ const relativePath = path.join(relativeDir, entry.name);
209
+ const normalizedRelativePath = relativePath.replace(/\\/g, "/");
210
+ if (entry.isDirectory()) {
211
+ if (EDITOR_IGNORED_DIRS.has(entry.name)) {
212
+ continue;
213
+ }
214
+ nodes.push({
215
+ type: "directory",
216
+ name: entry.name,
217
+ path: normalizedRelativePath,
218
+ children: await listProjectFileTree(projectRoot, relativePath)
219
+ });
220
+ continue;
221
+ }
222
+
223
+ if (!entry.isFile()) {
224
+ continue;
225
+ }
226
+
227
+ const absolutePath = safeProjectPath(projectRoot, relativePath);
228
+ const stat = await fs.stat(absolutePath);
229
+ nodes.push({
230
+ type: "file",
231
+ name: entry.name,
232
+ path: normalizedRelativePath,
233
+ size: stat.size,
234
+ editable: isEditableFile(absolutePath, stat.size),
235
+ language: fileLanguage(absolutePath)
236
+ });
237
+ }
238
+
239
+ return nodes;
240
+ }
241
+
242
+ async function readProjectTextFile(projectRoot, relativePath) {
243
+ const targetPath = safeProjectPath(projectRoot, relativePath);
244
+ const stat = await fs.stat(targetPath);
245
+ if (!stat.isFile()) {
246
+ throw new Error("Choose a file, not a folder.");
247
+ }
248
+ if (!isEditableFile(targetPath, stat.size)) {
249
+ throw new Error("This file is too large or is not a supported text file.");
250
+ }
251
+
252
+ const buffer = await fs.readFile(targetPath);
253
+ if (buffer.includes(0)) {
254
+ throw new Error("Binary files cannot be edited here.");
255
+ }
256
+
257
+ return {
258
+ path: String(relativePath).replace(/\\/g, "/"),
259
+ language: fileLanguage(targetPath),
260
+ size: stat.size,
261
+ content: buffer.toString("utf8")
262
+ };
263
+ }
264
+
265
+ async function writeProjectTextFile(projectRoot, relativePath, content) {
266
+ const targetPath = safeProjectPath(projectRoot, relativePath);
267
+ await fs.writeFile(targetPath, String(content || ""), "utf8");
268
+ const stat = await fs.stat(targetPath);
269
+ return {
270
+ path: String(relativePath).replace(/\\/g, "/"),
271
+ size: stat.size
272
+ };
273
+ }
274
+
275
+ async function createProjectTextFile(projectRoot, relativePath) {
276
+ const targetPath = safeNewFilePath(projectRoot, relativePath);
277
+ if (await pathExists(targetPath)) {
278
+ throw new Error("A file already exists at this path.");
279
+ }
280
+ await fs.mkdir(path.dirname(targetPath), { recursive: true });
281
+ await fs.writeFile(targetPath, "", "utf8");
282
+ return readProjectTextFile(projectRoot, path.relative(projectRoot, targetPath));
283
+ }
284
+
142
285
  function shouldIgnoreProjectWatchPath(fileName) {
143
286
  const normalized = String(fileName || "").replace(/\\/g, "/");
144
287
  return normalized
@@ -227,15 +370,28 @@ function cleanBuildOptions(options = {}) {
227
370
  const output = {
228
371
  projectRoot: options.projectRoot,
229
372
  debug: Boolean(options.debug),
373
+ showRuntimeLogs: Boolean(options.showRuntimeLogs),
230
374
  release: Boolean(options.release)
231
375
  };
232
376
 
233
- for (const key of ["mode", "appName", "packageId", "version", "icon", "androidPlatform", "minSdkVersion", "themeColor", "themeMode", "theme", "oneSignalAppId", "orientation", "permissions", "deepLinks"]) {
377
+ for (const key of ["mode", "appName", "packageId", "version", "icon", "androidPlatform", "minSdkVersion", "themeColor", "themeMode", "theme", "oneSignalAppId", "orientation", "permissions", "deepLinks", "buildFormat"]) {
234
378
  if (Array.isArray(options[key]) || options[key]) {
235
379
  output[key] = options[key];
236
380
  }
237
381
  }
238
382
 
383
+ if (options.keystore && typeof options.keystore === "object") {
384
+ const keystore = {};
385
+ for (const key of ["path", "alias", "storePassword", "keyPassword", "password", "type"]) {
386
+ if (options.keystore[key]) {
387
+ keystore[key] = options.keystore[key];
388
+ }
389
+ }
390
+ if (Object.keys(keystore).length) {
391
+ output.keystore = keystore;
392
+ }
393
+ }
394
+
239
395
  return output;
240
396
  }
241
397
 
@@ -525,10 +681,43 @@ ipcMain.handle("dialog:select-icon", async () => {
525
681
  return result.filePaths[0];
526
682
  });
527
683
 
684
+ ipcMain.handle("dialog:select-keystore", async () => {
685
+ const result = await dialog.showOpenDialog(mainWindow, {
686
+ title: "Select Android keystore",
687
+ properties: ["openFile"],
688
+ filters: [
689
+ { name: "Android keystore", extensions: ["jks", "keystore", "p12", "pfx"] },
690
+ { name: "All files", extensions: ["*"] }
691
+ ]
692
+ });
693
+
694
+ if (result.canceled || !result.filePaths.length) {
695
+ return null;
696
+ }
697
+
698
+ return result.filePaths[0];
699
+ });
700
+
528
701
  ipcMain.handle("project:inspect", async (_event, projectRoot) => {
529
702
  return inspectProject(projectRoot);
530
703
  });
531
704
 
705
+ ipcMain.handle("project:list-files", async (_event, projectRoot) => {
706
+ return listProjectFileTree(projectRoot);
707
+ });
708
+
709
+ ipcMain.handle("project:read-file", async (_event, projectRoot, relativePath) => {
710
+ return readProjectTextFile(projectRoot, relativePath);
711
+ });
712
+
713
+ ipcMain.handle("project:write-file", async (_event, projectRoot, relativePath, content) => {
714
+ return writeProjectTextFile(projectRoot, relativePath, content);
715
+ });
716
+
717
+ ipcMain.handle("project:create-file", async (_event, projectRoot, relativePath) => {
718
+ return createProjectTextFile(projectRoot, relativePath);
719
+ });
720
+
532
721
  ipcMain.handle("project:watch", async (event, projectRoot) => {
533
722
  return startProjectWatcher(projectRoot, event.sender);
534
723
  });
@@ -599,7 +788,7 @@ ipcMain.handle("build:run", async (event, options) => {
599
788
  ...buildOptions,
600
789
  onLog: (line) => sendLog(line)
601
790
  });
602
- sendLog(`APK generated: ${result.apkPath}`, "success");
791
+ sendLog(`Android file generated: ${result.artifactPath || result.apkPath}`, "success");
603
792
  return {
604
793
  ok: true,
605
794
  result
@@ -6,7 +6,12 @@ contextBridge.exposeInMainWorld("html2apkDesktop", {
6
6
  appInfo: () => ipcRenderer.invoke("app:info"),
7
7
  selectFolder: () => ipcRenderer.invoke("dialog:select-folder"),
8
8
  selectIcon: () => ipcRenderer.invoke("dialog:select-icon"),
9
+ selectKeystore: () => ipcRenderer.invoke("dialog:select-keystore"),
9
10
  inspectProject: (projectRoot) => ipcRenderer.invoke("project:inspect", projectRoot),
11
+ listProjectFiles: (projectRoot) => ipcRenderer.invoke("project:list-files", projectRoot),
12
+ readProjectFile: (projectRoot, relativePath) => ipcRenderer.invoke("project:read-file", projectRoot, relativePath),
13
+ writeProjectFile: (projectRoot, relativePath, content) => ipcRenderer.invoke("project:write-file", projectRoot, relativePath, content),
14
+ createProjectFile: (projectRoot, relativePath) => ipcRenderer.invoke("project:create-file", projectRoot, relativePath),
10
15
  watchProject: (projectRoot) => ipcRenderer.invoke("project:watch", projectRoot),
11
16
  unwatchProject: () => ipcRenderer.invoke("project:unwatch"),
12
17
  runDoctor: (projectRoot) => ipcRenderer.invoke("doctor:run", projectRoot),
@@ -49,7 +49,7 @@
49
49
  <img src="../../../html2apk.png" alt="" class="brand-icon">
50
50
  <div>
51
51
  <strong>html<span>2apk</span></strong>
52
- <small id="appVersion">v0.1.0</small>
52
+ <small id="appVersion">v0.7.0</small>
53
53
  </div>
54
54
  </div>
55
55
 
@@ -58,6 +58,7 @@
58
58
  <button class="nav-item" data-view="settings"><span class="nav-icon">C</span><span data-i18n="navSettings">Configuracoes</span></button>
59
59
  <button class="nav-item" data-view="appearance"><span class="nav-icon">A</span><span data-i18n="navAppearance">Aparencia</span></button>
60
60
  <button class="nav-item" data-view="build"><span class="nav-icon">B</span><span data-i18n="navBuild">Build</span></button>
61
+ <button class="nav-item" data-view="files"><span class="nav-icon">F</span><span data-i18n="navFiles">Arquivos</span></button>
61
62
  <button class="nav-item" data-view="codes"><span class="nav-icon">J</span><span data-i18n="navCodes">Codigos</span></button>
62
63
  <button class="nav-item" data-view="logs"><span class="nav-icon">L</span><span data-i18n="navLogs">Logs</span></button>
63
64
  </nav>
@@ -137,6 +138,14 @@
137
138
  <span data-i18n="appVersion">Versao do app</span>
138
139
  <input id="versionInput" type="text" placeholder="1.0.0">
139
140
  </label>
141
+ <label class="field">
142
+ <span data-i18n="buildFormat">Formato</span>
143
+ <select id="buildFormatInput">
144
+ <option value="apk" data-i18n="formatApk">APK</option>
145
+ <option value="aab" data-i18n="formatAab">AAB</option>
146
+ </select>
147
+ <small data-i18n="buildFormatHint">APK para instalar direto; AAB para loja.</small>
148
+ </label>
140
149
  <label class="field">
141
150
  <span data-i18n="mode">Modo</span>
142
151
  <select id="modeInput">
@@ -205,6 +214,31 @@
205
214
  </div>
206
215
  </div>
207
216
  </div>
217
+ <div class="field keystore-field">
218
+ <span data-i18n="keystoreTitle">Assinatura / keystore</span>
219
+ <div class="keystore-grid">
220
+ <label>
221
+ <small data-i18n="keystoreFile">Arquivo keystore</small>
222
+ <div class="inline-picker">
223
+ <input id="keystorePathInput" type="text" readonly placeholder="release.jks">
224
+ <button id="selectKeystoreButton" class="secondary-action" type="button" data-i18n="chooseKeystore">Escolher</button>
225
+ </div>
226
+ </label>
227
+ <label>
228
+ <small data-i18n="keystoreAlias">Alias</small>
229
+ <input id="keystoreAliasInput" type="text" placeholder="app">
230
+ </label>
231
+ <label>
232
+ <small data-i18n="keystoreStorePassword">Senha da store</small>
233
+ <input id="keystoreStorePasswordInput" type="password" autocomplete="off">
234
+ </label>
235
+ <label>
236
+ <small data-i18n="keystoreKeyPassword">Senha da key</small>
237
+ <input id="keystoreKeyPasswordInput" type="password" autocomplete="off">
238
+ </label>
239
+ </div>
240
+ <small data-i18n="keystoreHint">Opcional para APK debug. Para AAB/release, preencha para assinar o arquivo.</small>
241
+ </div>
208
242
  <div class="field permissions-field">
209
243
  <span data-i18n="androidPermissions">Permissoes Android</span>
210
244
  <div id="permissionGrid" class="permission-grid"></div>
@@ -219,6 +253,13 @@
219
253
  <small data-i18n="debugBuildText">Mantem a pasta Cordova temporaria para inspecao.</small>
220
254
  </span>
221
255
  </label>
256
+ <label class="toggle-card">
257
+ <input id="runtimeLogsInput" type="checkbox">
258
+ <span>
259
+ <strong data-i18n="runtimeLogsBuild">Console no APK</strong>
260
+ <small data-i18n="runtimeLogsBuildText">Mostra um modal Console no app gerado para depurar erros e funcoes interpretadas.</small>
261
+ </span>
262
+ </label>
222
263
  <label class="toggle-card">
223
264
  <input id="releaseInput" type="checkbox">
224
265
  <span>
@@ -260,6 +301,36 @@
260
301
  </div>
261
302
  </section>
262
303
 
304
+ <section id="view-files" class="view">
305
+ <header class="view-header compact">
306
+ <div>
307
+ <p class="eyebrow" data-i18n="filesEyebrow">Editor visual</p>
308
+ <h1 data-i18n="filesTitle">Arquivos do projeto</h1>
309
+ </div>
310
+ <div class="result-actions">
311
+ <button id="newFileButton" class="secondary-action" type="button" data-i18n="newFile">Novo arquivo</button>
312
+ <button id="saveFileButton" class="primary-action" type="button" disabled data-i18n="saveFile">Salvar</button>
313
+ </div>
314
+ </header>
315
+ <div class="file-editor-layout">
316
+ <aside class="file-tree-panel">
317
+ <strong data-i18n="fileTreeTitle">Pastas e arquivos</strong>
318
+ <div id="fileTree" class="file-tree-empty" data-i18n="chooseProjectFirst">Escolha uma pasta primeiro</div>
319
+ </aside>
320
+ <section class="file-editor-panel">
321
+ <div class="file-editor-meta">
322
+ <strong id="currentFileName" data-i18n="noFileSelected">Nenhum arquivo selecionado</strong>
323
+ <span id="fileLanguageBadge">text</span>
324
+ </div>
325
+ <textarea id="fileEditorInput" spellcheck="false" disabled placeholder="index.html"></textarea>
326
+ <div class="syntax-preview-wrap">
327
+ <strong data-i18n="syntaxPreview">Previa com sintaxe</strong>
328
+ <pre id="fileHighlight" class="syntax-preview"><code></code></pre>
329
+ </div>
330
+ </section>
331
+ </div>
332
+ </section>
333
+
263
334
  <section id="view-build" class="view">
264
335
  <header class="view-header compact">
265
336
  <div>