html2apk 0.2.0 → 0.4.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.
@@ -1,10 +1,11 @@
1
1
  "use strict";
2
2
 
3
+ const nodeFs = require("fs");
3
4
  const fs = require("fs/promises");
4
5
  const path = require("path");
5
6
  const { spawn } = require("child_process");
6
7
  const { app, BrowserWindow, dialog, ipcMain, screen, shell } = require("electron");
7
- const { buildApk } = require("../core/build-apk");
8
+ const { buildApk, runDebugUsb } = require("../core/build-apk");
8
9
  const {
9
10
  REQUIRED_ANDROID_BUILD_TOOLS,
10
11
  REQUIRED_ANDROID_PLATFORM,
@@ -13,6 +14,9 @@ const {
13
14
  const { runDoctor, formatDoctorReport } = require("../runtime-manager/doctor");
14
15
 
15
16
  let mainWindow = null;
17
+ let projectWatcher = null;
18
+ let projectWatchTimer = null;
19
+ let watchedProjectRoot = null;
16
20
  const smokeTest = process.env.HTML2APK_DESKTOP_SMOKE === "1";
17
21
  const APP_ID = "dev.caiomultiversando.html2apk";
18
22
  const APP_NAME = "html2apk";
@@ -22,6 +26,7 @@ const WINDOW_BACKGROUNDS = {
22
26
  };
23
27
 
24
28
  app.commandLine.appendSwitch("disable-crash-reporter");
29
+ process.title = APP_NAME;
25
30
  app.setName(APP_NAME);
26
31
 
27
32
  if (process.platform === "win32") {
@@ -134,6 +139,90 @@ async function inspectProject(projectRoot) {
134
139
  };
135
140
  }
136
141
 
142
+ function shouldIgnoreProjectWatchPath(fileName) {
143
+ const normalized = String(fileName || "").replace(/\\/g, "/");
144
+ return normalized
145
+ && (normalized.includes(".html2apk-doctor-")
146
+ || normalized.startsWith("dist/")
147
+ || normalized === "dist"
148
+ || normalized.startsWith("node_modules/")
149
+ || normalized === "node_modules"
150
+ || normalized.startsWith(".git/")
151
+ || normalized === ".git");
152
+ }
153
+
154
+ function stopProjectWatcher() {
155
+ if (projectWatchTimer) {
156
+ clearTimeout(projectWatchTimer);
157
+ projectWatchTimer = null;
158
+ }
159
+
160
+ if (projectWatcher) {
161
+ projectWatcher.close();
162
+ projectWatcher = null;
163
+ }
164
+
165
+ watchedProjectRoot = null;
166
+ }
167
+
168
+ function startProjectWatcher(projectRoot, sender) {
169
+ stopProjectWatcher();
170
+ watchedProjectRoot = projectRoot;
171
+
172
+ try {
173
+ projectWatcher = nodeFs.watch(projectRoot, { recursive: true }, (eventType, fileName) => {
174
+ if (shouldIgnoreProjectWatchPath(fileName)) {
175
+ return;
176
+ }
177
+
178
+ const changedPath = path.join(projectRoot, String(fileName || ""));
179
+ if (projectWatchTimer) {
180
+ clearTimeout(projectWatchTimer);
181
+ }
182
+
183
+ projectWatchTimer = setTimeout(async () => {
184
+ projectWatchTimer = null;
185
+ try {
186
+ if (watchedProjectRoot !== projectRoot) {
187
+ return;
188
+ }
189
+
190
+ const project = await inspectProject(projectRoot);
191
+ sender.send("project:changed", {
192
+ eventType,
193
+ changedPath,
194
+ project,
195
+ time: new Date().toISOString()
196
+ });
197
+ } catch (error) {
198
+ sender.send("project:watch-error", {
199
+ message: error.message,
200
+ time: new Date().toISOString()
201
+ });
202
+ }
203
+ }, 350);
204
+ });
205
+
206
+ projectWatcher.on("error", (error) => {
207
+ sender.send("project:watch-error", {
208
+ message: error.message,
209
+ time: new Date().toISOString()
210
+ });
211
+ });
212
+
213
+ return {
214
+ ok: true,
215
+ projectRoot
216
+ };
217
+ } catch (error) {
218
+ stopProjectWatcher();
219
+ return {
220
+ ok: false,
221
+ message: error.message
222
+ };
223
+ }
224
+ }
225
+
137
226
  function cleanBuildOptions(options = {}) {
138
227
  const output = {
139
228
  projectRoot: options.projectRoot,
@@ -141,7 +230,7 @@ function cleanBuildOptions(options = {}) {
141
230
  release: Boolean(options.release)
142
231
  };
143
232
 
144
- for (const key of ["mode", "appName", "packageId", "version", "icon", "androidPlatform", "minSdkVersion", "themeColor", "orientation", "permissions"]) {
233
+ for (const key of ["mode", "appName", "packageId", "version", "icon", "androidPlatform", "minSdkVersion", "themeColor", "themeMode", "theme", "oneSignalAppId", "orientation", "permissions", "deepLinks"]) {
145
234
  if (Array.isArray(options[key]) || options[key]) {
146
235
  output[key] = options[key];
147
236
  }
@@ -350,6 +439,10 @@ async function installWithWingetAndroidCli(sender) {
350
439
  }
351
440
 
352
441
  app.whenReady().then(() => {
442
+ app.setName(APP_NAME);
443
+ if (process.platform === "win32") {
444
+ app.setAppUserModelId(APP_ID);
445
+ }
353
446
  createWindow();
354
447
 
355
448
  app.on("activate", () => {
@@ -360,6 +453,7 @@ app.whenReady().then(() => {
360
453
  });
361
454
 
362
455
  app.on("window-all-closed", () => {
456
+ stopProjectWatcher();
363
457
  if (process.platform !== "darwin") {
364
458
  app.quit();
365
459
  }
@@ -435,6 +529,15 @@ ipcMain.handle("project:inspect", async (_event, projectRoot) => {
435
529
  return inspectProject(projectRoot);
436
530
  });
437
531
 
532
+ ipcMain.handle("project:watch", async (event, projectRoot) => {
533
+ return startProjectWatcher(projectRoot, event.sender);
534
+ });
535
+
536
+ ipcMain.handle("project:unwatch", async () => {
537
+ stopProjectWatcher();
538
+ return { ok: true };
539
+ });
540
+
438
541
  ipcMain.handle("doctor:run", async (_event, projectRoot) => {
439
542
  const report = await runDoctor({ projectRoot });
440
543
  return {
@@ -512,6 +615,39 @@ ipcMain.handle("build:run", async (event, options) => {
512
615
  }
513
616
  });
514
617
 
618
+ ipcMain.handle("build:run-usb-debug", async (event, options) => {
619
+ const buildOptions = cleanBuildOptions(options);
620
+ const sendLog = (line, kind = "raw") => {
621
+ event.sender.send("build:log", {
622
+ line,
623
+ kind,
624
+ time: new Date().toISOString()
625
+ });
626
+ };
627
+
628
+ try {
629
+ sendLog("Starting html2apk USB debug build.", "system");
630
+ const result = await runDebugUsb({
631
+ ...buildOptions,
632
+ release: false,
633
+ onLog: (line) => sendLog(line)
634
+ });
635
+ sendLog(`USB debug installed on device: ${result.device?.id || "Android device"}`, "success");
636
+ return {
637
+ ok: true,
638
+ result
639
+ };
640
+ } catch (error) {
641
+ sendLog(error.message, "error");
642
+ return {
643
+ ok: false,
644
+ message: error.message,
645
+ logs: error.logs || [],
646
+ buildDir: error.buildDir || null
647
+ };
648
+ }
649
+ });
650
+
515
651
  ipcMain.handle("shell:open-path", async (_event, targetPath) => {
516
652
  if (!targetPath) {
517
653
  return false;
@@ -527,3 +663,17 @@ ipcMain.handle("shell:show-item", async (_event, targetPath) => {
527
663
  shell.showItemInFolder(targetPath);
528
664
  return true;
529
665
  });
666
+
667
+ ipcMain.handle("shell:open-external", async (_event, targetUrl) => {
668
+ if (!targetUrl) {
669
+ return false;
670
+ }
671
+
672
+ const url = new URL(String(targetUrl));
673
+ if (!["https:", "http:"].includes(url.protocol)) {
674
+ return false;
675
+ }
676
+
677
+ await shell.openExternal(url.toString());
678
+ return true;
679
+ });
@@ -7,11 +7,15 @@ contextBridge.exposeInMainWorld("html2apkDesktop", {
7
7
  selectFolder: () => ipcRenderer.invoke("dialog:select-folder"),
8
8
  selectIcon: () => ipcRenderer.invoke("dialog:select-icon"),
9
9
  inspectProject: (projectRoot) => ipcRenderer.invoke("project:inspect", projectRoot),
10
+ watchProject: (projectRoot) => ipcRenderer.invoke("project:watch", projectRoot),
11
+ unwatchProject: () => ipcRenderer.invoke("project:unwatch"),
10
12
  runDoctor: (projectRoot) => ipcRenderer.invoke("doctor:run", projectRoot),
11
13
  installAndroidRequirements: () => ipcRenderer.invoke("install:android-requirements"),
12
14
  runBuild: (options) => ipcRenderer.invoke("build:run", options),
15
+ runUsbDebugBuild: (options) => ipcRenderer.invoke("build:run-usb-debug", options),
13
16
  openPath: (targetPath) => ipcRenderer.invoke("shell:open-path", targetPath),
14
17
  showItem: (targetPath) => ipcRenderer.invoke("shell:show-item", targetPath),
18
+ openExternalUrl: (targetUrl) => ipcRenderer.invoke("shell:open-external", targetUrl),
15
19
  minimizeWindow: () => ipcRenderer.invoke("window:minimize"),
16
20
  toggleMaximizeWindow: () => ipcRenderer.invoke("window:toggle-maximize"),
17
21
  closeWindow: () => ipcRenderer.invoke("window:close"),
@@ -26,5 +30,15 @@ contextBridge.exposeInMainWorld("html2apkDesktop", {
26
30
  const handler = (_event, payload) => listener(payload);
27
31
  ipcRenderer.on("install:log", handler);
28
32
  return () => ipcRenderer.removeListener("install:log", handler);
33
+ },
34
+ onProjectChanged: (listener) => {
35
+ const handler = (_event, payload) => listener(payload);
36
+ ipcRenderer.on("project:changed", handler);
37
+ return () => ipcRenderer.removeListener("project:changed", handler);
38
+ },
39
+ onProjectWatchError: (listener) => {
40
+ const handler = (_event, payload) => listener(payload);
41
+ ipcRenderer.on("project:watch-error", handler);
42
+ return () => ipcRenderer.removeListener("project:watch-error", handler);
29
43
  }
30
44
  });
@@ -176,6 +176,13 @@
176
176
  <span>cordova-android</span>
177
177
  <input id="androidPlatformInput" type="text" placeholder="android@15.0.0">
178
178
  </label>
179
+ <label class="field">
180
+ <span data-i18n="themeMode">Tema do APK</span>
181
+ <select id="themeModeInput">
182
+ <option value="fixed" data-i18n="themeModeFixed">Cor fixa</option>
183
+ <option value="auto" data-i18n="themeModeAuto">Automatico pela tela</option>
184
+ </select>
185
+ </label>
179
186
  <label class="field color-field">
180
187
  <span data-i18n="appThemeColor">Cor do tema do app</span>
181
188
  <div class="color-picker">
@@ -183,6 +190,11 @@
183
190
  <input id="themeColorTextInput" type="text" value="#126fff" maxlength="7">
184
191
  </div>
185
192
  </label>
193
+ <label class="field onesignal-field">
194
+ <span data-i18n="oneSignalAppId">OneSignal App ID</span>
195
+ <input id="oneSignalAppIdInput" type="text" placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx">
196
+ <small data-i18n="oneSignalAppIdHint">Opcional. Use o App ID do OneSignal, nao a REST API Key.</small>
197
+ </label>
186
198
  <div class="field icon-field">
187
199
  <span data-i18n="appIcon">Icone do app</span>
188
200
  <div class="icon-picker">
@@ -275,6 +287,7 @@
275
287
  </div>
276
288
  <section class="action-strip">
277
289
  <button id="buildButton" class="primary-action" type="button" disabled data-i18n="startBuild">Gerar APK</button>
290
+ <button id="usbDebugButton" class="secondary-action" type="button" disabled data-i18n="startUsbDebug">Testar no USB</button>
278
291
  </section>
279
292
  <div id="resultPanel" class="result-panel hidden">
280
293
  <div>
@@ -292,8 +305,8 @@
292
305
  <div class="success-panel">
293
306
  <img src="../../../html2apk.png" alt="" class="success-icon">
294
307
  <p class="eyebrow" data-i18n="successEyebrow">Concluido</p>
295
- <h1 data-i18n="successTitle">APK gerado com sucesso</h1>
296
- <p data-i18n="successText">Seu arquivo Android esta pronto na pasta dist.</p>
308
+ <h1 id="successTitle" data-i18n="successTitle">APK gerado com sucesso</h1>
309
+ <p id="successText" data-i18n="successText">Seu arquivo Android esta pronto na pasta dist.</p>
297
310
  <code id="successApkPath">-</code>
298
311
  <div class="result-actions">
299
312
  <button id="successOpenDistButton" class="secondary-action" type="button" data-i18n="openDist">Abrir dist</button>
@@ -345,6 +358,11 @@
345
358
  <strong data-i18n="helpDepsTitle">Dependencias no EXE</strong>
346
359
  <p data-i18n="helpDeps">O executavel empacota html2apk, a interface e dependencias Node/Cordova. JDK e Android SDK ainda precisam existir na maquina por tamanho e licenca.</p>
347
360
  </article>
361
+ <article class="creator-card">
362
+ <strong data-i18n="helpCreatorTitle">Criador</strong>
363
+ <p data-i18n="helpCreatorText">Conheca o Dev Caio Multiversando, criador desta linda aplicacao html2apk.</p>
364
+ <button id="devInstagramButton" class="secondary-action instagram-action" type="button" data-i18n="openInstagram">Conhecer o dev</button>
365
+ </article>
348
366
  </div>
349
367
  </section>
350
368
  </main>