html2apk 0.1.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 +472 -0
- package/bin/html2apk-desktop.js +23 -0
- package/bin/html2apk.js +19 -0
- package/examples/minimal/app.json +27 -0
- package/examples/minimal/dist/MeuApp-1.0.0-debug.apk +0 -0
- package/examples/minimal/index.html +41 -0
- package/html2apk.png +0 -0
- package/index.js +3 -0
- package/package.json +76 -0
- package/src/android/README.md +7 -0
- package/src/bridge/install-bridge.js +16 -0
- package/src/cli/index.js +163 -0
- package/src/cordova/apk-finder.js +45 -0
- package/src/cordova/config-xml.js +110 -0
- package/src/cordova/project.js +56 -0
- package/src/core/build-apk.js +189 -0
- package/src/core/config.js +99 -0
- package/src/core/defaults.js +37 -0
- package/src/core/validation.js +58 -0
- package/src/desktop/main.js +522 -0
- package/src/desktop/preload.js +30 -0
- package/src/desktop/renderer/index.html +323 -0
- package/src/desktop/renderer/renderer.js +1074 -0
- package/src/desktop/renderer/styles.css +1208 -0
- package/src/index.js +12 -0
- package/src/runtime-manager/doctor.js +164 -0
- package/src/runtime-manager/index.js +190 -0
- package/src/templates/cordova-plugin-html2apk-bridge/package.json +16 -0
- package/src/templates/cordova-plugin-html2apk-bridge/plugin.xml +39 -0
- package/src/templates/cordova-plugin-html2apk-bridge/src/android/BootReceiver.java +20 -0
- package/src/templates/cordova-plugin-html2apk-bridge/src/android/Html2ApkBridge.java +375 -0
- package/src/templates/cordova-plugin-html2apk-bridge/src/android/NotificationReceiver.java +112 -0
- package/src/templates/cordova-plugin-html2apk-bridge/src/android/NotificationStore.java +91 -0
- package/src/templates/cordova-plugin-html2apk-bridge/www/html2apk-bridge.js +129 -0
- package/src/utils/command-runner.js +124 -0
- package/src/utils/fs-extra.js +111 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs/promises");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
async function exists(filePath) {
|
|
7
|
+
try {
|
|
8
|
+
await fs.access(filePath);
|
|
9
|
+
return true;
|
|
10
|
+
} catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function assertInside(parent, child, label) {
|
|
16
|
+
const relative = path.relative(parent, child);
|
|
17
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
18
|
+
throw new Error(`${label} must stay inside the project root.`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function validateEntryFile(projectRoot, options) {
|
|
23
|
+
const webRoot = path.resolve(projectRoot, options.webRoot || ".");
|
|
24
|
+
assertInside(projectRoot, webRoot, "webRoot");
|
|
25
|
+
|
|
26
|
+
const entryFile = options.entryFile || "index.html";
|
|
27
|
+
const entryPath = path.resolve(webRoot, entryFile);
|
|
28
|
+
assertInside(webRoot, entryPath, "entryFile");
|
|
29
|
+
|
|
30
|
+
if (!(await exists(entryPath))) {
|
|
31
|
+
throw new Error(`Entry file not found: ${path.relative(projectRoot, entryPath)}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
webRoot,
|
|
36
|
+
entryPath
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function validatePackageId(packageId) {
|
|
41
|
+
const pattern = /^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)+$/;
|
|
42
|
+
if (!pattern.test(packageId)) {
|
|
43
|
+
throw new Error(`Invalid packageId "${packageId}". Example: com.company.app`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function validateRequiredOptions(options) {
|
|
48
|
+
if (!options.appName) {
|
|
49
|
+
throw new Error("appName is required.");
|
|
50
|
+
}
|
|
51
|
+
validatePackageId(options.packageId);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = {
|
|
55
|
+
validateEntryFile,
|
|
56
|
+
validateRequiredOptions,
|
|
57
|
+
assertInside
|
|
58
|
+
};
|
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs/promises");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { spawn } = require("child_process");
|
|
6
|
+
const { app, BrowserWindow, dialog, ipcMain, screen, shell } = require("electron");
|
|
7
|
+
const { buildApk } = require("../core/build-apk");
|
|
8
|
+
const {
|
|
9
|
+
REQUIRED_ANDROID_BUILD_TOOLS,
|
|
10
|
+
REQUIRED_ANDROID_PLATFORM,
|
|
11
|
+
getRuntimeEnvironment
|
|
12
|
+
} = require("../runtime-manager");
|
|
13
|
+
const { runDoctor, formatDoctorReport } = require("../runtime-manager/doctor");
|
|
14
|
+
|
|
15
|
+
let mainWindow = null;
|
|
16
|
+
const smokeTest = process.env.HTML2APK_DESKTOP_SMOKE === "1";
|
|
17
|
+
const WINDOW_BACKGROUNDS = {
|
|
18
|
+
light: "#f7fbff",
|
|
19
|
+
dark: "#10141b"
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
app.commandLine.appendSwitch("disable-crash-reporter");
|
|
23
|
+
|
|
24
|
+
function rootPath(...parts) {
|
|
25
|
+
return path.resolve(__dirname, "..", "..", ...parts);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function iconPath() {
|
|
29
|
+
return rootPath("html2apk.png");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function animateWindowIn(window) {
|
|
33
|
+
let opacity = 0;
|
|
34
|
+
window.setOpacity(opacity);
|
|
35
|
+
window.show();
|
|
36
|
+
window.focus();
|
|
37
|
+
|
|
38
|
+
const timer = setInterval(() => {
|
|
39
|
+
opacity = Math.min(1, opacity + 0.08);
|
|
40
|
+
window.setOpacity(opacity);
|
|
41
|
+
if (opacity >= 1) {
|
|
42
|
+
clearInterval(timer);
|
|
43
|
+
}
|
|
44
|
+
}, 16);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function createWindow() {
|
|
48
|
+
const { width: screenWidth, height: screenHeight } = screen.getPrimaryDisplay().workAreaSize;
|
|
49
|
+
const width = Math.min(1280, Math.max(860, Math.floor(screenWidth * 0.92)));
|
|
50
|
+
const height = Math.min(820, Math.max(620, Math.floor(screenHeight * 0.9)));
|
|
51
|
+
|
|
52
|
+
mainWindow = new BrowserWindow({
|
|
53
|
+
width,
|
|
54
|
+
height,
|
|
55
|
+
minWidth: Math.min(860, width),
|
|
56
|
+
minHeight: Math.min(620, height),
|
|
57
|
+
center: true,
|
|
58
|
+
show: false,
|
|
59
|
+
frame: false,
|
|
60
|
+
title: "html2apk",
|
|
61
|
+
icon: iconPath(),
|
|
62
|
+
backgroundColor: WINDOW_BACKGROUNDS.light,
|
|
63
|
+
webPreferences: {
|
|
64
|
+
preload: path.join(__dirname, "preload.js"),
|
|
65
|
+
contextIsolation: true,
|
|
66
|
+
nodeIntegration: false,
|
|
67
|
+
sandbox: false
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
mainWindow.loadFile(path.join(__dirname, "renderer", "index.html"));
|
|
72
|
+
if (smokeTest) {
|
|
73
|
+
mainWindow.webContents.once("did-finish-load", () => {
|
|
74
|
+
setTimeout(() => app.quit(), 500);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
mainWindow.once("ready-to-show", () => animateWindowIn(mainWindow));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function pathExists(filePath) {
|
|
81
|
+
try {
|
|
82
|
+
await fs.access(filePath);
|
|
83
|
+
return true;
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function readJsonIfExists(filePath) {
|
|
90
|
+
try {
|
|
91
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
92
|
+
return JSON.parse(raw);
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function inspectProject(projectRoot) {
|
|
99
|
+
if (!projectRoot) {
|
|
100
|
+
throw new Error("Project folder is required.");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const stat = await fs.stat(projectRoot);
|
|
104
|
+
if (!stat.isDirectory()) {
|
|
105
|
+
throw new Error("Please choose a folder, not a file.");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const appJsonPath = path.join(projectRoot, "app.json");
|
|
109
|
+
const configJsonPath = path.join(projectRoot, "config.json");
|
|
110
|
+
const indexPath = path.join(projectRoot, "index.html");
|
|
111
|
+
const appConfig = await readJsonIfExists(appJsonPath);
|
|
112
|
+
const fallbackConfig = appConfig ? null : await readJsonIfExists(configJsonPath);
|
|
113
|
+
const config = appConfig || fallbackConfig || {};
|
|
114
|
+
const webRoot = path.resolve(projectRoot, config.webRoot || ".");
|
|
115
|
+
const entryPath = path.resolve(webRoot, config.entryFile || "index.html");
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
projectRoot,
|
|
119
|
+
name: path.basename(projectRoot),
|
|
120
|
+
hasAppJson: await pathExists(appJsonPath),
|
|
121
|
+
hasConfigJson: await pathExists(configJsonPath),
|
|
122
|
+
hasRootIndex: await pathExists(indexPath),
|
|
123
|
+
hasEntryFile: await pathExists(entryPath),
|
|
124
|
+
entryPath,
|
|
125
|
+
config,
|
|
126
|
+
distPath: path.join(projectRoot, "dist")
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function cleanBuildOptions(options = {}) {
|
|
131
|
+
const output = {
|
|
132
|
+
projectRoot: options.projectRoot,
|
|
133
|
+
debug: Boolean(options.debug),
|
|
134
|
+
release: Boolean(options.release)
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
for (const key of ["mode", "appName", "packageId", "version", "icon", "androidPlatform"]) {
|
|
138
|
+
if (options[key]) {
|
|
139
|
+
output[key] = options[key];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return output;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function quoteForCmd(value) {
|
|
147
|
+
const text = String(value);
|
|
148
|
+
if (!text.length) {
|
|
149
|
+
return "\"\"";
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (/^[a-zA-Z0-9_@./:\\=+\-;]+$/.test(text)) {
|
|
153
|
+
return text;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return `"${text.replace(/(["^&|<>])/g, "^$1")}"`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function createSpawnSpec(command, args) {
|
|
160
|
+
const needsCmd = process.platform === "win32" && /\.(cmd|bat)$/i.test(command);
|
|
161
|
+
if (!needsCmd) {
|
|
162
|
+
return { command, args };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
command: "cmd.exe",
|
|
167
|
+
args: ["/d", "/s", "/c", [quoteForCmd(command), ...args.map(quoteForCmd)].join(" ")]
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function sdkManagerPath(runtime) {
|
|
172
|
+
if (!runtime.cmdlineTools) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return path.join(runtime.cmdlineTools, process.platform === "win32" ? "sdkmanager.bat" : "sdkmanager");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function androidPackageArgs() {
|
|
180
|
+
return [
|
|
181
|
+
"platform-tools",
|
|
182
|
+
`platforms;${REQUIRED_ANDROID_PLATFORM}`,
|
|
183
|
+
`build-tools;${REQUIRED_ANDROID_BUILD_TOOLS}`,
|
|
184
|
+
"cmdline-tools;latest"
|
|
185
|
+
];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function sendInstallLog(sender, line, kind = "raw") {
|
|
189
|
+
sender.send("install:log", {
|
|
190
|
+
line,
|
|
191
|
+
kind,
|
|
192
|
+
time: new Date().toISOString()
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function findAndroidCli() {
|
|
197
|
+
if (process.platform !== "win32") {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const candidates = [
|
|
202
|
+
process.env.LOCALAPPDATA
|
|
203
|
+
? path.join(
|
|
204
|
+
process.env.LOCALAPPDATA,
|
|
205
|
+
"Microsoft",
|
|
206
|
+
"WinGet",
|
|
207
|
+
"Packages",
|
|
208
|
+
"Google.AndroidCLI_Microsoft.Winget.Source_8wekyb3d8bbwe",
|
|
209
|
+
"android.exe"
|
|
210
|
+
)
|
|
211
|
+
: null,
|
|
212
|
+
"android.exe"
|
|
213
|
+
].filter(Boolean);
|
|
214
|
+
|
|
215
|
+
for (const candidate of candidates) {
|
|
216
|
+
if (candidate === "android.exe" || await pathExists(candidate)) {
|
|
217
|
+
return candidate;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function runInstallCommand(sender, command, args, options = {}) {
|
|
225
|
+
const pretty = `${command} ${args.join(" ")}`.trim();
|
|
226
|
+
const spawnSpec = createSpawnSpec(command, args);
|
|
227
|
+
const env = options.env || process.env;
|
|
228
|
+
|
|
229
|
+
sendInstallLog(sender, `$ ${pretty}`, "system");
|
|
230
|
+
|
|
231
|
+
return new Promise((resolve, reject) => {
|
|
232
|
+
const child = spawn(spawnSpec.command, spawnSpec.args, {
|
|
233
|
+
env,
|
|
234
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
235
|
+
shell: false,
|
|
236
|
+
windowsHide: true
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
let stdout = "";
|
|
240
|
+
let stderr = "";
|
|
241
|
+
|
|
242
|
+
function acceptLicenseIfPrompted(text) {
|
|
243
|
+
if (!options.acceptLicenses || !child.stdin.writable) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (/(accept|license|licen[cç]a|y\/n|y\/N|\[y\/n\]|\(y\/N\))/i.test(text)) {
|
|
248
|
+
child.stdin.write("y\n");
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
child.stdout.on("data", (chunk) => {
|
|
253
|
+
const text = chunk.toString();
|
|
254
|
+
stdout += text;
|
|
255
|
+
sendInstallLog(sender, text);
|
|
256
|
+
acceptLicenseIfPrompted(text);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
child.stderr.on("data", (chunk) => {
|
|
260
|
+
const text = chunk.toString();
|
|
261
|
+
stderr += text;
|
|
262
|
+
sendInstallLog(sender, text, "error");
|
|
263
|
+
acceptLicenseIfPrompted(text);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
child.on("error", (error) => {
|
|
267
|
+
reject(new Error(`Failed to run "${pretty}": ${error.message}`));
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
child.on("close", (code) => {
|
|
271
|
+
if (code === 0) {
|
|
272
|
+
resolve({ code, stdout, stderr });
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const error = new Error(`Command failed (${code}): ${pretty}`);
|
|
277
|
+
error.code = code;
|
|
278
|
+
error.stdout = stdout;
|
|
279
|
+
error.stderr = stderr;
|
|
280
|
+
reject(error);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function askAndroidInstallPermission() {
|
|
286
|
+
const result = await dialog.showMessageBox(mainWindow, {
|
|
287
|
+
type: "question",
|
|
288
|
+
buttons: ["Instalar", "Cancelar"],
|
|
289
|
+
defaultId: 0,
|
|
290
|
+
cancelId: 1,
|
|
291
|
+
title: "html2apk",
|
|
292
|
+
message: "Instalar pacotes Android necessarios?",
|
|
293
|
+
detail: [
|
|
294
|
+
"O html2apk precisa do Android SDK com platform-tools, Android Platform 36, Build Tools 36.0.0 e command line tools.",
|
|
295
|
+
"Ao continuar, o instalador pode baixar pacotes e aceitar as licencas Android necessarias para o build."
|
|
296
|
+
].join("\n\n")
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
return result.response === 0;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function installWithSdkManager(sender, sdkManager, runtime) {
|
|
303
|
+
const args = androidPackageArgs();
|
|
304
|
+
await runInstallCommand(sender, sdkManager, ["--licenses"], {
|
|
305
|
+
env: runtime.env,
|
|
306
|
+
acceptLicenses: true
|
|
307
|
+
}).catch((error) => {
|
|
308
|
+
sendInstallLog(sender, error.message, "error");
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
await runInstallCommand(sender, sdkManager, args, {
|
|
312
|
+
env: runtime.env,
|
|
313
|
+
acceptLicenses: true
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function installWithWingetAndroidCli(sender) {
|
|
318
|
+
if (process.platform !== "win32") {
|
|
319
|
+
throw new Error("Automatic Android CLI installation is only available on Windows in this desktop app.");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
await runInstallCommand(sender, "winget", [
|
|
324
|
+
"install",
|
|
325
|
+
"--id",
|
|
326
|
+
"Google.AndroidCLI",
|
|
327
|
+
"-e",
|
|
328
|
+
"--accept-package-agreements",
|
|
329
|
+
"--accept-source-agreements"
|
|
330
|
+
]);
|
|
331
|
+
} catch (error) {
|
|
332
|
+
sendInstallLog(sender, error.message, "error");
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const androidCli = await findAndroidCli();
|
|
336
|
+
if (!androidCli) {
|
|
337
|
+
throw new Error("Android CLI was not found after the Winget step.");
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
await runInstallCommand(sender, androidCli, ["sdk", "install", ...androidPackageArgs()], {
|
|
341
|
+
acceptLicenses: true
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
app.whenReady().then(() => {
|
|
346
|
+
createWindow();
|
|
347
|
+
|
|
348
|
+
app.on("activate", () => {
|
|
349
|
+
if (BrowserWindow.getAllWindows().length === 0) {
|
|
350
|
+
createWindow();
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
app.on("window-all-closed", () => {
|
|
356
|
+
if (process.platform !== "darwin") {
|
|
357
|
+
app.quit();
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
ipcMain.handle("app:info", () => ({
|
|
362
|
+
name: "html2apk",
|
|
363
|
+
version: app.getVersion(),
|
|
364
|
+
credit: "Dev Caio Multiversando",
|
|
365
|
+
iconPath: iconPath()
|
|
366
|
+
}));
|
|
367
|
+
|
|
368
|
+
ipcMain.handle("window:minimize", () => {
|
|
369
|
+
BrowserWindow.getFocusedWindow()?.minimize();
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
ipcMain.handle("window:toggle-maximize", () => {
|
|
373
|
+
const window = BrowserWindow.getFocusedWindow();
|
|
374
|
+
if (!window) {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (window.isMaximized()) {
|
|
379
|
+
window.unmaximize();
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
window.maximize();
|
|
384
|
+
return true;
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
ipcMain.handle("window:close", () => {
|
|
388
|
+
BrowserWindow.getFocusedWindow()?.close();
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
ipcMain.handle("window:set-theme", (_event, theme) => {
|
|
392
|
+
const nextTheme = theme === "dark" ? "dark" : "light";
|
|
393
|
+
BrowserWindow.getFocusedWindow()?.setBackgroundColor(WINDOW_BACKGROUNDS[nextTheme]);
|
|
394
|
+
return nextTheme;
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
ipcMain.handle("dialog:select-folder", async () => {
|
|
398
|
+
const result = await dialog.showOpenDialog(mainWindow, {
|
|
399
|
+
title: "Select project folder",
|
|
400
|
+
properties: ["openDirectory"]
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
if (result.canceled || !result.filePaths.length) {
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return inspectProject(result.filePaths[0]);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
ipcMain.handle("dialog:select-icon", async () => {
|
|
411
|
+
const result = await dialog.showOpenDialog(mainWindow, {
|
|
412
|
+
title: "Select app icon",
|
|
413
|
+
properties: ["openFile"],
|
|
414
|
+
filters: [
|
|
415
|
+
{ name: "PNG images", extensions: ["png"] },
|
|
416
|
+
{ name: "All files", extensions: ["*"] }
|
|
417
|
+
]
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
if (result.canceled || !result.filePaths.length) {
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return result.filePaths[0];
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
ipcMain.handle("project:inspect", async (_event, projectRoot) => {
|
|
428
|
+
return inspectProject(projectRoot);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
ipcMain.handle("doctor:run", async (_event, projectRoot) => {
|
|
432
|
+
const report = await runDoctor({ projectRoot });
|
|
433
|
+
return {
|
|
434
|
+
ok: report.ok,
|
|
435
|
+
report,
|
|
436
|
+
text: formatDoctorReport(report)
|
|
437
|
+
};
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
ipcMain.handle("install:android-requirements", async (event) => {
|
|
441
|
+
const approved = await askAndroidInstallPermission();
|
|
442
|
+
if (!approved) {
|
|
443
|
+
return {
|
|
444
|
+
ok: false,
|
|
445
|
+
canceled: true,
|
|
446
|
+
message: "Installation canceled by user."
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const sender = event.sender;
|
|
451
|
+
|
|
452
|
+
try {
|
|
453
|
+
const runtime = getRuntimeEnvironment();
|
|
454
|
+
const sdkManager = sdkManagerPath(runtime);
|
|
455
|
+
sendInstallLog(sender, "Preparing Android SDK requirements.", "system");
|
|
456
|
+
|
|
457
|
+
if (sdkManager && await pathExists(sdkManager)) {
|
|
458
|
+
await installWithSdkManager(sender, sdkManager, runtime);
|
|
459
|
+
} else {
|
|
460
|
+
await installWithWingetAndroidCli(sender);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return {
|
|
464
|
+
ok: true,
|
|
465
|
+
message: "Android SDK requirements installed."
|
|
466
|
+
};
|
|
467
|
+
} catch (error) {
|
|
468
|
+
sendInstallLog(sender, error.message, "error");
|
|
469
|
+
return {
|
|
470
|
+
ok: false,
|
|
471
|
+
message: error.message
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
ipcMain.handle("build:run", async (event, options) => {
|
|
477
|
+
const buildOptions = cleanBuildOptions(options);
|
|
478
|
+
const sendLog = (line, kind = "raw") => {
|
|
479
|
+
event.sender.send("build:log", {
|
|
480
|
+
line,
|
|
481
|
+
kind,
|
|
482
|
+
time: new Date().toISOString()
|
|
483
|
+
});
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
try {
|
|
487
|
+
sendLog("Starting html2apk build.", "system");
|
|
488
|
+
const result = await buildApk({
|
|
489
|
+
...buildOptions,
|
|
490
|
+
onLog: (line) => sendLog(line)
|
|
491
|
+
});
|
|
492
|
+
sendLog(`APK generated: ${result.apkPath}`, "success");
|
|
493
|
+
return {
|
|
494
|
+
ok: true,
|
|
495
|
+
result
|
|
496
|
+
};
|
|
497
|
+
} catch (error) {
|
|
498
|
+
sendLog(error.message, "error");
|
|
499
|
+
return {
|
|
500
|
+
ok: false,
|
|
501
|
+
message: error.message,
|
|
502
|
+
logs: error.logs || [],
|
|
503
|
+
buildDir: error.buildDir || null
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
ipcMain.handle("shell:open-path", async (_event, targetPath) => {
|
|
509
|
+
if (!targetPath) {
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
await shell.openPath(targetPath);
|
|
513
|
+
return true;
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
ipcMain.handle("shell:show-item", async (_event, targetPath) => {
|
|
517
|
+
if (!targetPath) {
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
shell.showItemInFolder(targetPath);
|
|
521
|
+
return true;
|
|
522
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { contextBridge, ipcRenderer, webUtils } = require("electron");
|
|
4
|
+
|
|
5
|
+
contextBridge.exposeInMainWorld("html2apkDesktop", {
|
|
6
|
+
appInfo: () => ipcRenderer.invoke("app:info"),
|
|
7
|
+
selectFolder: () => ipcRenderer.invoke("dialog:select-folder"),
|
|
8
|
+
selectIcon: () => ipcRenderer.invoke("dialog:select-icon"),
|
|
9
|
+
inspectProject: (projectRoot) => ipcRenderer.invoke("project:inspect", projectRoot),
|
|
10
|
+
runDoctor: (projectRoot) => ipcRenderer.invoke("doctor:run", projectRoot),
|
|
11
|
+
installAndroidRequirements: () => ipcRenderer.invoke("install:android-requirements"),
|
|
12
|
+
runBuild: (options) => ipcRenderer.invoke("build:run", options),
|
|
13
|
+
openPath: (targetPath) => ipcRenderer.invoke("shell:open-path", targetPath),
|
|
14
|
+
showItem: (targetPath) => ipcRenderer.invoke("shell:show-item", targetPath),
|
|
15
|
+
minimizeWindow: () => ipcRenderer.invoke("window:minimize"),
|
|
16
|
+
toggleMaximizeWindow: () => ipcRenderer.invoke("window:toggle-maximize"),
|
|
17
|
+
closeWindow: () => ipcRenderer.invoke("window:close"),
|
|
18
|
+
setWindowTheme: (theme) => ipcRenderer.invoke("window:set-theme", theme),
|
|
19
|
+
pathForFile: (file) => webUtils.getPathForFile(file),
|
|
20
|
+
onBuildLog: (listener) => {
|
|
21
|
+
const handler = (_event, payload) => listener(payload);
|
|
22
|
+
ipcRenderer.on("build:log", handler);
|
|
23
|
+
return () => ipcRenderer.removeListener("build:log", handler);
|
|
24
|
+
},
|
|
25
|
+
onInstallLog: (listener) => {
|
|
26
|
+
const handler = (_event, payload) => listener(payload);
|
|
27
|
+
ipcRenderer.on("install:log", handler);
|
|
28
|
+
return () => ipcRenderer.removeListener("install:log", handler);
|
|
29
|
+
}
|
|
30
|
+
});
|