html2apk 0.4.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.
- package/README.md +116 -12
- package/examples/minimal/app.json +2 -0
- package/examples/minimal/dist/MeuApp-1.0.0-debug.apk +0 -0
- package/examples/minimal/dist/MeuApp-1.0.0-release.aab +0 -0
- package/package.json +1 -1
- package/src/cli/index.js +12 -1
- package/src/cordova/apk-finder.js +22 -10
- package/src/cordova/project.js +5 -0
- package/src/core/build-apk.js +102 -22
- package/src/core/config.js +16 -0
- package/src/core/defaults.js +2 -0
- package/src/desktop/main.js +191 -2
- package/src/desktop/preload.js +5 -0
- package/src/desktop/renderer/index.html +71 -0
- package/src/desktop/renderer/renderer.js +473 -12
- package/src/desktop/renderer/styles.css +189 -0
- package/src/templates/cordova-plugin-html2apk-bridge/plugin.xml +2 -0
- package/src/templates/cordova-plugin-html2apk-bridge/src/android/Html2ApkBridge.java +219 -20
- package/src/templates/cordova-plugin-html2apk-bridge/src/android/NotificationClickReceiver.java +28 -0
- package/src/templates/cordova-plugin-html2apk-bridge/src/android/NotificationReceiver.java +1 -1
- package/src/templates/cordova-plugin-html2apk-bridge/www/html2apk-bridge.js +278 -11
- package/src/templates/html2apk-early-bridge.js +860 -0
- package/src/templates/html2apk-runtime-console.js +805 -0
package/src/desktop/main.js
CHANGED
|
@@ -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(`
|
|
791
|
+
sendLog(`Android file generated: ${result.artifactPath || result.apkPath}`, "success");
|
|
603
792
|
return {
|
|
604
793
|
ok: true,
|
|
605
794
|
result
|
package/src/desktop/preload.js
CHANGED
|
@@ -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),
|
|
@@ -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>
|