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,1074 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const api = window.html2apkDesktop;
|
|
4
|
+
|
|
5
|
+
const i18n = {
|
|
6
|
+
pt: {
|
|
7
|
+
creditTitle: "Creditos iniciais",
|
|
8
|
+
creditText: "Criado por Dev Caio Multiversando",
|
|
9
|
+
navHome: "Inicio",
|
|
10
|
+
navSettings: "Configuracoes",
|
|
11
|
+
navAppearance: "Aparencia",
|
|
12
|
+
navBuild: "Build",
|
|
13
|
+
navLogs: "Logs",
|
|
14
|
+
navHelp: "Ajuda",
|
|
15
|
+
theme: "Tema",
|
|
16
|
+
themeLight: "Claro",
|
|
17
|
+
themeDark: "Escuro",
|
|
18
|
+
homeEyebrow: "Comece aqui",
|
|
19
|
+
homeTitle: "Arraste a pasta do projeto para o html2apk",
|
|
20
|
+
chooseFolder: "Escolher pasta",
|
|
21
|
+
dropTitle: "Solte a pasta do seu projeto HTML aqui",
|
|
22
|
+
dropText: "Depois confirme o build. O APK sera salvo em dist.",
|
|
23
|
+
indexHint: "A pasta deve conter index.html ou configurar entryFile no app.json",
|
|
24
|
+
project: "Projeto",
|
|
25
|
+
configHint: "Configuracao detectada",
|
|
26
|
+
runDoctor: "Verificar ambiente",
|
|
27
|
+
confirmBuild: "Confirmar e gerar APK",
|
|
28
|
+
continueSettings: "Continuar para configuracoes",
|
|
29
|
+
settingsEyebrow: "Antes de compilar",
|
|
30
|
+
settingsTitle: "Configuracoes do build",
|
|
31
|
+
appName: "Nome do app",
|
|
32
|
+
packageId: "Package ID",
|
|
33
|
+
appVersion: "Versao do app",
|
|
34
|
+
mode: "Modo",
|
|
35
|
+
chooseMode: "Escolha o modo",
|
|
36
|
+
appIcon: "Icone do app",
|
|
37
|
+
chooseIcon: "Escolher icone PNG",
|
|
38
|
+
reviewBuild: "Revisar build",
|
|
39
|
+
debugBuild: "Debug tecnico",
|
|
40
|
+
debugBuildText: "Mantem a pasta Cordova temporaria para inspecao.",
|
|
41
|
+
releaseBuild: "Release",
|
|
42
|
+
releaseBuildText: "Usa configuracao de assinatura se houver keystore.",
|
|
43
|
+
appearanceEyebrow: "Preferencias",
|
|
44
|
+
appearanceTitle: "Idioma e tema",
|
|
45
|
+
language: "Idioma",
|
|
46
|
+
languageText: "Escolha como os feedbacks aparecem durante o build.",
|
|
47
|
+
themeText: "O botao tambem fica na barra lateral para acesso rapido.",
|
|
48
|
+
toggleTheme: "Alternar tema",
|
|
49
|
+
buildEyebrow: "Revisao",
|
|
50
|
+
buildTitle: "Confirme as informacoes e gere o APK",
|
|
51
|
+
startBuild: "Gerar APK",
|
|
52
|
+
stepFolder: "Pasta recebida",
|
|
53
|
+
stepSettings: "Configuracoes completas",
|
|
54
|
+
stepDoctor: "Ambiente verificado",
|
|
55
|
+
stepBuild: "APK gerado",
|
|
56
|
+
apkReady: "APK pronto",
|
|
57
|
+
openDist: "Abrir dist",
|
|
58
|
+
showApk: "Mostrar APK",
|
|
59
|
+
logsEyebrow: "Ao vivo",
|
|
60
|
+
logsTitle: "Logs do processo",
|
|
61
|
+
clearLogs: "Limpar logs",
|
|
62
|
+
helpEyebrow: "Sem misterio",
|
|
63
|
+
helpTitle: "Doctor, build e dependencias",
|
|
64
|
+
helpDoctor: "Verifica se Java, Gradle, Cordova e Android SDK estao prontos. Nao gera APK.",
|
|
65
|
+
helpBuild: "Cria um projeto Cordova temporario, copia seu app web e gera o arquivo APK em dist.",
|
|
66
|
+
helpDepsTitle: "Dependencias no EXE",
|
|
67
|
+
helpDeps: "O executavel empacota html2apk, a interface e dependencias Node/Cordova. Se faltarem pacotes Android, ele pede permissao e tenta instalar; JDK/Gradle podem exigir instalacao do sistema.",
|
|
68
|
+
ready: "Pronto",
|
|
69
|
+
selected: "Selecionado",
|
|
70
|
+
missing: "Nao encontrado",
|
|
71
|
+
detected: "Detectado",
|
|
72
|
+
notDetected: "Nao detectado",
|
|
73
|
+
folderReady: "Pasta pronta para build",
|
|
74
|
+
chooseProjectFirst: "Escolha uma pasta primeiro",
|
|
75
|
+
doctorRunning: "Verificando ambiente",
|
|
76
|
+
doctorOk: "Ambiente pronto",
|
|
77
|
+
doctorFail: "Ambiente incompleto",
|
|
78
|
+
buildRunning: "Gerando APK",
|
|
79
|
+
buildOk: "APK gerado com sucesso",
|
|
80
|
+
buildFail: "Build falhou",
|
|
81
|
+
droppedFolder: "Pasta recebida",
|
|
82
|
+
noFolderDrop: "Solte uma pasta do projeto",
|
|
83
|
+
projectLoaded: "Projeto carregado",
|
|
84
|
+
logsCleared: "Logs limpos",
|
|
85
|
+
settingsOk: "Configuracoes completas",
|
|
86
|
+
settingsMissing: "Complete as configuracoes obrigatorias",
|
|
87
|
+
requiredFieldsTitle: "Antes de gerar o APK, corrija:",
|
|
88
|
+
missingProject: "Escolha a pasta do projeto.",
|
|
89
|
+
missingAppName: "Informe o nome do app.",
|
|
90
|
+
invalidPackageId: "Informe um Package ID valido, exemplo: com.seuapp.meuapp.",
|
|
91
|
+
invalidVersion: "Informe a versao no formato 1.0.0.",
|
|
92
|
+
missingMode: "Escolha o modo do app.",
|
|
93
|
+
missingIcon: "Escolha o icone do app.",
|
|
94
|
+
invalidIconType: "Use um icone PNG para evitar falhas no Android.",
|
|
95
|
+
iconSelected: "Icone selecionado",
|
|
96
|
+
progressLabel: "Progresso",
|
|
97
|
+
progressIdle: "Aguardando pasta",
|
|
98
|
+
progressFolder: "Pasta recebida",
|
|
99
|
+
progressSettings: "Configuracoes prontas",
|
|
100
|
+
progressDoctor: "Verificando ambiente",
|
|
101
|
+
progressBuild: "Gerando APK",
|
|
102
|
+
progressDone: "APK pronto",
|
|
103
|
+
progressError: "Algo precisa de atencao",
|
|
104
|
+
environmentPreparing: "Verificando ambiente antes de avancar",
|
|
105
|
+
environmentNeedsInstall: "Pacotes Android ausentes. O app vai pedir permissao para instalar.",
|
|
106
|
+
environmentInstalling: "Instalando pacotes Android",
|
|
107
|
+
environmentInstallOk: "Pacotes Android instalados",
|
|
108
|
+
environmentInstallFail: "Nao foi possivel instalar os pacotes Android",
|
|
109
|
+
environmentCanceled: "Instalacao cancelada",
|
|
110
|
+
environmentBlocked: "Corrija o ambiente para continuar",
|
|
111
|
+
bottomLogsTitle: "Logs ao vivo",
|
|
112
|
+
showLogs: "Mostrar logs",
|
|
113
|
+
hideLogs: "Ocultar logs",
|
|
114
|
+
successEyebrow: "Concluido",
|
|
115
|
+
successTitle: "APK gerado com sucesso",
|
|
116
|
+
successText: "Seu arquivo Android esta pronto na pasta dist.",
|
|
117
|
+
newBuild: "Novo build"
|
|
118
|
+
},
|
|
119
|
+
en: {
|
|
120
|
+
creditTitle: "Opening credits",
|
|
121
|
+
creditText: "Created by Dev Caio Multiversando",
|
|
122
|
+
navHome: "Home",
|
|
123
|
+
navSettings: "Settings",
|
|
124
|
+
navAppearance: "Appearance",
|
|
125
|
+
navBuild: "Build",
|
|
126
|
+
navLogs: "Logs",
|
|
127
|
+
navHelp: "Help",
|
|
128
|
+
theme: "Theme",
|
|
129
|
+
themeLight: "Light",
|
|
130
|
+
themeDark: "Dark",
|
|
131
|
+
homeEyebrow: "Start here",
|
|
132
|
+
homeTitle: "Drag your project folder into html2apk",
|
|
133
|
+
chooseFolder: "Choose folder",
|
|
134
|
+
dropTitle: "Drop your HTML project folder here",
|
|
135
|
+
dropText: "Then confirm the build. The APK will be saved in dist.",
|
|
136
|
+
indexHint: "The folder must contain index.html or configure entryFile in app.json",
|
|
137
|
+
project: "Project",
|
|
138
|
+
configHint: "Configuration detected",
|
|
139
|
+
runDoctor: "Check environment",
|
|
140
|
+
confirmBuild: "Confirm and build APK",
|
|
141
|
+
continueSettings: "Continue to settings",
|
|
142
|
+
settingsEyebrow: "Before compiling",
|
|
143
|
+
settingsTitle: "Build settings",
|
|
144
|
+
appName: "App name",
|
|
145
|
+
packageId: "Package ID",
|
|
146
|
+
appVersion: "App version",
|
|
147
|
+
mode: "Mode",
|
|
148
|
+
chooseMode: "Choose mode",
|
|
149
|
+
appIcon: "App icon",
|
|
150
|
+
chooseIcon: "Choose PNG icon",
|
|
151
|
+
reviewBuild: "Review build",
|
|
152
|
+
debugBuild: "Technical debug",
|
|
153
|
+
debugBuildText: "Keeps the temporary Cordova folder for inspection.",
|
|
154
|
+
releaseBuild: "Release",
|
|
155
|
+
releaseBuildText: "Uses signing configuration when a keystore exists.",
|
|
156
|
+
appearanceEyebrow: "Preferences",
|
|
157
|
+
appearanceTitle: "Language and theme",
|
|
158
|
+
language: "Language",
|
|
159
|
+
languageText: "Choose how feedback appears during the build.",
|
|
160
|
+
themeText: "The button also stays in the sidebar for quick access.",
|
|
161
|
+
toggleTheme: "Toggle theme",
|
|
162
|
+
buildEyebrow: "Review",
|
|
163
|
+
buildTitle: "Confirm the information and build the APK",
|
|
164
|
+
startBuild: "Build APK",
|
|
165
|
+
stepFolder: "Folder received",
|
|
166
|
+
stepSettings: "Settings complete",
|
|
167
|
+
stepDoctor: "Environment checked",
|
|
168
|
+
stepBuild: "APK generated",
|
|
169
|
+
apkReady: "APK ready",
|
|
170
|
+
openDist: "Open dist",
|
|
171
|
+
showApk: "Show APK",
|
|
172
|
+
logsEyebrow: "Live",
|
|
173
|
+
logsTitle: "Process logs",
|
|
174
|
+
clearLogs: "Clear logs",
|
|
175
|
+
helpEyebrow: "Plain and simple",
|
|
176
|
+
helpTitle: "Doctor, build and dependencies",
|
|
177
|
+
helpDoctor: "Checks whether Java, Gradle, Cordova and Android SDK are ready. It does not generate an APK.",
|
|
178
|
+
helpBuild: "Creates a temporary Cordova project, copies your web app and generates the APK in dist.",
|
|
179
|
+
helpDepsTitle: "Dependencies in the EXE",
|
|
180
|
+
helpDeps: "The executable bundles html2apk, the interface and Node/Cordova dependencies. If Android packages are missing, it asks permission and tries to install them; JDK/Gradle may still require system installation.",
|
|
181
|
+
ready: "Ready",
|
|
182
|
+
selected: "Selected",
|
|
183
|
+
missing: "Missing",
|
|
184
|
+
detected: "Detected",
|
|
185
|
+
notDetected: "Not detected",
|
|
186
|
+
folderReady: "Folder ready for build",
|
|
187
|
+
chooseProjectFirst: "Choose a folder first",
|
|
188
|
+
doctorRunning: "Checking environment",
|
|
189
|
+
doctorOk: "Environment ready",
|
|
190
|
+
doctorFail: "Environment incomplete",
|
|
191
|
+
buildRunning: "Building APK",
|
|
192
|
+
buildOk: "APK generated successfully",
|
|
193
|
+
buildFail: "Build failed",
|
|
194
|
+
droppedFolder: "Folder received",
|
|
195
|
+
noFolderDrop: "Drop a project folder",
|
|
196
|
+
projectLoaded: "Project loaded",
|
|
197
|
+
logsCleared: "Logs cleared",
|
|
198
|
+
settingsOk: "Settings complete",
|
|
199
|
+
settingsMissing: "Complete the required settings",
|
|
200
|
+
requiredFieldsTitle: "Before building the APK, fix:",
|
|
201
|
+
missingProject: "Choose the project folder.",
|
|
202
|
+
missingAppName: "Enter the app name.",
|
|
203
|
+
invalidPackageId: "Enter a valid Package ID, example: com.yourapp.name.",
|
|
204
|
+
invalidVersion: "Enter the version as 1.0.0.",
|
|
205
|
+
missingMode: "Choose the app mode.",
|
|
206
|
+
missingIcon: "Choose the app icon.",
|
|
207
|
+
invalidIconType: "Use a PNG icon to avoid Android build failures.",
|
|
208
|
+
iconSelected: "Icon selected",
|
|
209
|
+
progressLabel: "Progress",
|
|
210
|
+
progressIdle: "Waiting for folder",
|
|
211
|
+
progressFolder: "Folder received",
|
|
212
|
+
progressSettings: "Settings ready",
|
|
213
|
+
progressDoctor: "Checking environment",
|
|
214
|
+
progressBuild: "Building APK",
|
|
215
|
+
progressDone: "APK ready",
|
|
216
|
+
progressError: "Something needs attention",
|
|
217
|
+
environmentPreparing: "Checking the environment before continuing",
|
|
218
|
+
environmentNeedsInstall: "Android packages are missing. The app will ask permission to install them.",
|
|
219
|
+
environmentInstalling: "Installing Android packages",
|
|
220
|
+
environmentInstallOk: "Android packages installed",
|
|
221
|
+
environmentInstallFail: "Could not install Android packages",
|
|
222
|
+
environmentCanceled: "Installation canceled",
|
|
223
|
+
environmentBlocked: "Fix the environment to continue",
|
|
224
|
+
bottomLogsTitle: "Live logs",
|
|
225
|
+
showLogs: "Show logs",
|
|
226
|
+
hideLogs: "Hide logs",
|
|
227
|
+
successEyebrow: "Complete",
|
|
228
|
+
successTitle: "APK generated successfully",
|
|
229
|
+
successText: "Your Android file is ready in the dist folder.",
|
|
230
|
+
newBuild: "New build"
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const animatedBuildLines = [
|
|
235
|
+
"Preparando projeto / Preparing project",
|
|
236
|
+
"Conferindo app.json / Reading app.json",
|
|
237
|
+
"Montando projeto Cordova / Creating Cordova project",
|
|
238
|
+
"Copiando HTML, CSS e JS / Copying HTML, CSS and JS",
|
|
239
|
+
"Instalando bridge nativa / Installing native bridge",
|
|
240
|
+
"Chamando Gradle / Calling Gradle",
|
|
241
|
+
"Procurando APK final / Finding final APK"
|
|
242
|
+
];
|
|
243
|
+
|
|
244
|
+
const state = {
|
|
245
|
+
language: localStorage.getItem("html2apk.language") || null,
|
|
246
|
+
theme: localStorage.getItem("html2apk.theme") || "light",
|
|
247
|
+
project: null,
|
|
248
|
+
doctorOk: false,
|
|
249
|
+
environmentOk: false,
|
|
250
|
+
environmentChecking: false,
|
|
251
|
+
environmentFailed: false,
|
|
252
|
+
settingsValid: false,
|
|
253
|
+
buildRunning: false,
|
|
254
|
+
lastApkPath: null,
|
|
255
|
+
lastDistPath: null,
|
|
256
|
+
animationTimer: null,
|
|
257
|
+
progress: 0,
|
|
258
|
+
logsVisible: localStorage.getItem("html2apk.logsVisible") === "true"
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const elements = {};
|
|
262
|
+
|
|
263
|
+
function $(id) {
|
|
264
|
+
return document.getElementById(id);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function text(key) {
|
|
268
|
+
return i18n[state.language || "pt"][key] || i18n.pt[key] || key;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function collectElements() {
|
|
272
|
+
[
|
|
273
|
+
"creditOverlay",
|
|
274
|
+
"languageOverlay",
|
|
275
|
+
"dragRegion",
|
|
276
|
+
"appVersion",
|
|
277
|
+
"themeToggle",
|
|
278
|
+
"themeToggleLarge",
|
|
279
|
+
"themeName",
|
|
280
|
+
"dropZone",
|
|
281
|
+
"selectFolderButton",
|
|
282
|
+
"nextSettingsButton",
|
|
283
|
+
"projectSummary",
|
|
284
|
+
"projectName",
|
|
285
|
+
"projectPath",
|
|
286
|
+
"configStatus",
|
|
287
|
+
"entryStatus",
|
|
288
|
+
"entryPath",
|
|
289
|
+
"doctorButton",
|
|
290
|
+
"buildButton",
|
|
291
|
+
"appNameInput",
|
|
292
|
+
"packageIdInput",
|
|
293
|
+
"versionInput",
|
|
294
|
+
"modeInput",
|
|
295
|
+
"androidPlatformInput",
|
|
296
|
+
"iconPathInput",
|
|
297
|
+
"iconPreview",
|
|
298
|
+
"selectIconButton",
|
|
299
|
+
"settingsValidation",
|
|
300
|
+
"settingsNextButton",
|
|
301
|
+
"debugInput",
|
|
302
|
+
"releaseInput",
|
|
303
|
+
"stepFolderText",
|
|
304
|
+
"stepSettingsText",
|
|
305
|
+
"stepDoctorText",
|
|
306
|
+
"stepBuildText",
|
|
307
|
+
"progressText",
|
|
308
|
+
"progressBar",
|
|
309
|
+
"progressPercent",
|
|
310
|
+
"reviewGrid",
|
|
311
|
+
"resultPanel",
|
|
312
|
+
"apkPath",
|
|
313
|
+
"openDistButton",
|
|
314
|
+
"showApkButton",
|
|
315
|
+
"successApkPath",
|
|
316
|
+
"successOpenDistButton",
|
|
317
|
+
"successShowApkButton",
|
|
318
|
+
"newBuildButton",
|
|
319
|
+
"logConsole",
|
|
320
|
+
"bottomLogConsole",
|
|
321
|
+
"clearLogsButton",
|
|
322
|
+
"toggleLogsButton",
|
|
323
|
+
"bottomToggleLogsButton",
|
|
324
|
+
"bottomClearLogsButton",
|
|
325
|
+
"minimizeButton",
|
|
326
|
+
"maximizeButton",
|
|
327
|
+
"closeButton",
|
|
328
|
+
"statusDot",
|
|
329
|
+
"statusText"
|
|
330
|
+
].forEach((id) => {
|
|
331
|
+
elements[id] = $(id);
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function applyTheme() {
|
|
336
|
+
document.documentElement.dataset.theme = state.theme;
|
|
337
|
+
localStorage.setItem("html2apk.theme", state.theme);
|
|
338
|
+
elements.themeName.textContent = state.theme === "dark" ? text("themeDark") : text("themeLight");
|
|
339
|
+
api.setWindowTheme(state.theme).catch(() => {});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function applyLanguage() {
|
|
343
|
+
document.documentElement.lang = state.language === "en" ? "en" : "pt-BR";
|
|
344
|
+
document.querySelectorAll("[data-i18n]").forEach((node) => {
|
|
345
|
+
node.textContent = text(node.dataset.i18n);
|
|
346
|
+
});
|
|
347
|
+
document.querySelectorAll("[data-language-choice]").forEach((button) => {
|
|
348
|
+
button.classList.toggle("active", button.dataset.languageChoice === state.language);
|
|
349
|
+
});
|
|
350
|
+
applyTheme();
|
|
351
|
+
applyLogBarVisibility();
|
|
352
|
+
if (state.project) {
|
|
353
|
+
validateSettings();
|
|
354
|
+
renderReview();
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function setStatus(kind, message) {
|
|
359
|
+
elements.statusDot.classList.remove("busy", "error");
|
|
360
|
+
if (kind === "busy") {
|
|
361
|
+
elements.statusDot.classList.add("busy");
|
|
362
|
+
}
|
|
363
|
+
if (kind === "error") {
|
|
364
|
+
elements.statusDot.classList.add("error");
|
|
365
|
+
}
|
|
366
|
+
elements.statusText.textContent = message;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function setView(viewName) {
|
|
370
|
+
document.querySelectorAll(".nav-item").forEach((button) => {
|
|
371
|
+
button.classList.toggle("active", button.dataset.view === viewName);
|
|
372
|
+
});
|
|
373
|
+
document.querySelectorAll(".view").forEach((view) => {
|
|
374
|
+
view.classList.toggle("active", view.id === `view-${viewName}`);
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function appendLogTo(container, line, kind) {
|
|
379
|
+
if (!container) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const node = document.createElement("div");
|
|
384
|
+
node.className = `log-line ${kind}`;
|
|
385
|
+
const time = new Date().toLocaleTimeString();
|
|
386
|
+
node.textContent = `[${time}] ${line}`;
|
|
387
|
+
container.appendChild(node);
|
|
388
|
+
|
|
389
|
+
while (container.children.length > 500) {
|
|
390
|
+
container.removeChild(container.firstChild);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
container.scrollTop = container.scrollHeight;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function appendLog(line, kind = "raw") {
|
|
397
|
+
const lines = String(line || "").split(/\r?\n/).filter(Boolean);
|
|
398
|
+
for (const item of lines) {
|
|
399
|
+
appendLogTo(elements.logConsole, item, kind);
|
|
400
|
+
appendLogTo(elements.bottomLogConsole, item, kind);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function applyLogBarVisibility() {
|
|
405
|
+
document.body.classList.toggle("logs-visible", state.logsVisible);
|
|
406
|
+
localStorage.setItem("html2apk.logsVisible", state.logsVisible ? "true" : "false");
|
|
407
|
+
const label = text(state.logsVisible ? "hideLogs" : "showLogs");
|
|
408
|
+
|
|
409
|
+
[elements.toggleLogsButton, elements.bottomToggleLogsButton].forEach((button) => {
|
|
410
|
+
if (!button) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
button.textContent = label;
|
|
415
|
+
button.setAttribute("aria-expanded", String(state.logsVisible));
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function toggleLogBar() {
|
|
420
|
+
state.logsVisible = !state.logsVisible;
|
|
421
|
+
applyLogBarVisibility();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function clearLogs() {
|
|
425
|
+
elements.logConsole.innerHTML = "";
|
|
426
|
+
if (elements.bottomLogConsole) {
|
|
427
|
+
elements.bottomLogConsole.innerHTML = "";
|
|
428
|
+
}
|
|
429
|
+
appendLog(text("logsCleared"), "system");
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function setStep(step, status, message) {
|
|
433
|
+
const card = document.querySelector(`[data-step="${step}"]`);
|
|
434
|
+
if (!card) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
card.classList.remove("active", "done", "error");
|
|
438
|
+
if (status) {
|
|
439
|
+
card.classList.add(status);
|
|
440
|
+
}
|
|
441
|
+
const targets = {
|
|
442
|
+
folder: "stepFolderText",
|
|
443
|
+
settings: "stepSettingsText",
|
|
444
|
+
doctor: "stepDoctorText",
|
|
445
|
+
build: "stepBuildText"
|
|
446
|
+
};
|
|
447
|
+
const target = elements[targets[step]];
|
|
448
|
+
if (target) {
|
|
449
|
+
target.textContent = message || "-";
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function setProgress(percent, message, stateClass = "") {
|
|
454
|
+
const safePercent = Math.max(0, Math.min(100, Math.round(percent)));
|
|
455
|
+
state.progress = safePercent;
|
|
456
|
+
|
|
457
|
+
if (elements.progressBar) {
|
|
458
|
+
elements.progressBar.style.width = `${safePercent}%`;
|
|
459
|
+
elements.progressBar.classList.remove("active", "error");
|
|
460
|
+
if (stateClass) {
|
|
461
|
+
elements.progressBar.classList.add(stateClass);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (elements.progressPercent) {
|
|
466
|
+
elements.progressPercent.textContent = `${safePercent}%`;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (elements.progressText) {
|
|
470
|
+
elements.progressText.textContent = message;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function updateActionButtons() {
|
|
475
|
+
const hasProject = Boolean(state.project);
|
|
476
|
+
const isBusy = state.environmentChecking || state.buildRunning;
|
|
477
|
+
|
|
478
|
+
elements.nextSettingsButton.disabled = !hasProject || isBusy || !state.environmentOk;
|
|
479
|
+
elements.doctorButton.disabled = !hasProject || isBusy;
|
|
480
|
+
elements.settingsNextButton.disabled = !hasProject || !state.settingsValid || !state.environmentOk || isBusy;
|
|
481
|
+
setBuildButtons(hasProject && state.settingsValid && state.environmentOk && !isBusy);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function setBuildButtons(enabled) {
|
|
485
|
+
elements.buildButton.disabled = !enabled;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function packageSegment(value) {
|
|
489
|
+
return String(value || "app")
|
|
490
|
+
.toLowerCase()
|
|
491
|
+
.replace(/[^a-z0-9_]+/g, "")
|
|
492
|
+
.replace(/^[^a-z]+/, "") || "app";
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function toFileUrl(filePath) {
|
|
496
|
+
if (!filePath) {
|
|
497
|
+
return "../../../html2apk.png";
|
|
498
|
+
}
|
|
499
|
+
return `file:///${String(filePath).replace(/\\/g, "/").replace(/^\/+/, "")}`;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function isAbsolutePath(filePath) {
|
|
503
|
+
return /^[a-zA-Z]:[\\/]/.test(String(filePath || "")) || String(filePath || "").startsWith("/");
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function iconPreviewPath(iconPath) {
|
|
507
|
+
if (!iconPath || !state.project) {
|
|
508
|
+
return "../../../html2apk.png";
|
|
509
|
+
}
|
|
510
|
+
if (isAbsolutePath(iconPath)) {
|
|
511
|
+
return toFileUrl(iconPath);
|
|
512
|
+
}
|
|
513
|
+
return toFileUrl(`${state.project.projectRoot}\\${iconPath}`);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function escapeHtml(value) {
|
|
517
|
+
return String(value || "")
|
|
518
|
+
.replace(/&/g, "&")
|
|
519
|
+
.replace(/</g, "<")
|
|
520
|
+
.replace(/>/g, ">")
|
|
521
|
+
.replace(/"/g, """);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function populateSettings(config = {}, project = state.project) {
|
|
525
|
+
const projectName = project ? project.name : "MeuApp";
|
|
526
|
+
elements.appNameInput.value = config.appName || projectName || "";
|
|
527
|
+
elements.packageIdInput.value = config.packageId || `com.html2apk.${packageSegment(projectName)}`;
|
|
528
|
+
elements.versionInput.value = config.version || "1.0.0";
|
|
529
|
+
elements.modeInput.value = config.mode || "fullscreen";
|
|
530
|
+
elements.androidPlatformInput.value = config.androidPlatform || "android@15.0.0";
|
|
531
|
+
elements.iconPathInput.value = config.icon || "";
|
|
532
|
+
elements.iconPreview.src = iconPreviewPath(config.icon || "");
|
|
533
|
+
elements.debugInput.checked = Boolean(config.debug);
|
|
534
|
+
elements.releaseInput.checked = Boolean(config.release);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function validateSettings() {
|
|
538
|
+
const errors = [];
|
|
539
|
+
const packagePattern = /^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)+$/;
|
|
540
|
+
const versionPattern = /^\d+\.\d+\.\d+([-.+][0-9A-Za-z.-]+)?$/;
|
|
541
|
+
|
|
542
|
+
if (!state.project) {
|
|
543
|
+
errors.push(text("missingProject"));
|
|
544
|
+
}
|
|
545
|
+
if (!elements.appNameInput.value.trim()) {
|
|
546
|
+
errors.push(text("missingAppName"));
|
|
547
|
+
}
|
|
548
|
+
if (!packagePattern.test(elements.packageIdInput.value.trim())) {
|
|
549
|
+
errors.push(text("invalidPackageId"));
|
|
550
|
+
}
|
|
551
|
+
if (!versionPattern.test(elements.versionInput.value.trim())) {
|
|
552
|
+
errors.push(text("invalidVersion"));
|
|
553
|
+
}
|
|
554
|
+
if (!elements.modeInput.value) {
|
|
555
|
+
errors.push(text("missingMode"));
|
|
556
|
+
}
|
|
557
|
+
if (!elements.iconPathInput.value.trim()) {
|
|
558
|
+
errors.push(text("missingIcon"));
|
|
559
|
+
} else if (!/\.png$/i.test(elements.iconPathInput.value.trim())) {
|
|
560
|
+
errors.push(text("invalidIconType"));
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
state.settingsValid = errors.length === 0;
|
|
564
|
+
setStep("settings", state.settingsValid ? "done" : "active", state.settingsValid ? text("settingsOk") : text("settingsMissing"));
|
|
565
|
+
updateActionButtons();
|
|
566
|
+
|
|
567
|
+
if (state.project && !state.buildRunning && !state.lastApkPath) {
|
|
568
|
+
if (state.environmentChecking) {
|
|
569
|
+
setProgress(35, text("progressDoctor"), "active");
|
|
570
|
+
} else if (!state.environmentOk) {
|
|
571
|
+
setProgress(state.environmentFailed ? 45 : 25, state.environmentFailed ? text("progressError") : text("progressFolder"), state.environmentFailed ? "error" : "");
|
|
572
|
+
} else {
|
|
573
|
+
setProgress(state.settingsValid ? 70 : 50, state.settingsValid ? text("progressSettings") : text("settingsMissing"));
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (!errors.length) {
|
|
578
|
+
elements.settingsValidation.classList.add("hidden");
|
|
579
|
+
elements.settingsValidation.innerHTML = "";
|
|
580
|
+
return true;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
elements.settingsValidation.classList.remove("hidden");
|
|
584
|
+
elements.settingsValidation.innerHTML = `<p>${text("requiredFieldsTitle")}</p><ul>${errors.map((error) => `<li>${error}</li>`).join("")}</ul>`;
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function renderReview() {
|
|
589
|
+
if (!state.project) {
|
|
590
|
+
elements.reviewGrid.innerHTML = "";
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const items = [
|
|
595
|
+
[text("project"), state.project.projectRoot],
|
|
596
|
+
[text("appName"), elements.appNameInput.value.trim()],
|
|
597
|
+
[text("packageId"), elements.packageIdInput.value.trim()],
|
|
598
|
+
[text("appVersion"), elements.versionInput.value.trim()],
|
|
599
|
+
[text("mode"), elements.modeInput.value],
|
|
600
|
+
[text("appIcon"), elements.iconPathInput.value.trim()]
|
|
601
|
+
];
|
|
602
|
+
|
|
603
|
+
elements.reviewGrid.innerHTML = items.map(([label, value]) => `
|
|
604
|
+
<article class="review-item">
|
|
605
|
+
<span>${escapeHtml(label)}</span>
|
|
606
|
+
<strong title="${escapeHtml(value)}">${escapeHtml(value)}</strong>
|
|
607
|
+
</article>
|
|
608
|
+
`).join("");
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function goToReview() {
|
|
612
|
+
if (!state.environmentOk) {
|
|
613
|
+
setView("build");
|
|
614
|
+
setStatus(state.environmentChecking ? "busy" : "error", state.environmentChecking ? text("doctorRunning") : text("environmentBlocked"));
|
|
615
|
+
setProgress(state.environmentChecking ? 35 : 45, state.environmentChecking ? text("progressDoctor") : text("progressError"), state.environmentChecking ? "active" : "error");
|
|
616
|
+
return false;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (!validateSettings()) {
|
|
620
|
+
setView("settings");
|
|
621
|
+
setStatus("error", text("settingsMissing"));
|
|
622
|
+
return false;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
renderReview();
|
|
626
|
+
setView("build");
|
|
627
|
+
setStatus("ready", text("settingsOk"));
|
|
628
|
+
return true;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function hasAndroidInstallableFailure(response) {
|
|
632
|
+
const checks = response && response.report && Array.isArray(response.report.checks)
|
|
633
|
+
? response.report.checks
|
|
634
|
+
: [];
|
|
635
|
+
|
|
636
|
+
return checks.some((check) => {
|
|
637
|
+
const name = String(check.name || "");
|
|
638
|
+
return !check.ok && (
|
|
639
|
+
name.includes("ANDROID_HOME") ||
|
|
640
|
+
name.includes("Android platform-tools") ||
|
|
641
|
+
name.includes("Android cmdline-tools") ||
|
|
642
|
+
name.includes("Android sdkmanager") ||
|
|
643
|
+
name.includes("Android build-tools") ||
|
|
644
|
+
name.includes("Android platform ")
|
|
645
|
+
);
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function markEnvironmentReady() {
|
|
650
|
+
state.doctorOk = true;
|
|
651
|
+
state.environmentOk = true;
|
|
652
|
+
state.environmentFailed = false;
|
|
653
|
+
setStep("doctor", "done", text("doctorOk"));
|
|
654
|
+
setStatus("ready", text("doctorOk"));
|
|
655
|
+
setProgress(state.settingsValid ? 70 : 50, state.settingsValid ? text("progressSettings") : text("doctorOk"));
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function markEnvironmentBlocked(message = text("doctorFail")) {
|
|
659
|
+
state.doctorOk = false;
|
|
660
|
+
state.environmentOk = false;
|
|
661
|
+
state.environmentFailed = true;
|
|
662
|
+
setStep("doctor", "error", message);
|
|
663
|
+
setStatus("error", message);
|
|
664
|
+
setProgress(45, text("progressError"), "error");
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
async function installAndroidRequirements() {
|
|
668
|
+
setStatus("busy", text("environmentInstalling"));
|
|
669
|
+
setStep("doctor", "active", text("environmentInstalling"));
|
|
670
|
+
setProgress(45, text("environmentInstalling"), "active");
|
|
671
|
+
appendLog(text("environmentNeedsInstall"), "system");
|
|
672
|
+
|
|
673
|
+
try {
|
|
674
|
+
const result = await api.installAndroidRequirements();
|
|
675
|
+
if (result && result.canceled) {
|
|
676
|
+
appendLog(text("environmentCanceled"), "system");
|
|
677
|
+
markEnvironmentBlocked(text("environmentCanceled"));
|
|
678
|
+
return false;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
appendLog(result && result.message ? result.message : (result && result.ok ? text("environmentInstallOk") : text("environmentInstallFail")), result && result.ok ? "success" : "error");
|
|
682
|
+
return Boolean(result && result.ok);
|
|
683
|
+
} catch (error) {
|
|
684
|
+
appendLog(error.message, "error");
|
|
685
|
+
markEnvironmentBlocked(text("environmentInstallFail"));
|
|
686
|
+
return false;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
async function verifyEnvironment({ allowInstall = true, requireSettings = false } = {}) {
|
|
691
|
+
if (!state.project) {
|
|
692
|
+
setStatus("error", text("chooseProjectFirst"));
|
|
693
|
+
return false;
|
|
694
|
+
}
|
|
695
|
+
if (requireSettings && !validateSettings()) {
|
|
696
|
+
setView("settings");
|
|
697
|
+
setStatus("error", text("settingsMissing"));
|
|
698
|
+
return false;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
state.environmentChecking = true;
|
|
702
|
+
state.environmentOk = false;
|
|
703
|
+
state.environmentFailed = false;
|
|
704
|
+
updateActionButtons();
|
|
705
|
+
setView("build");
|
|
706
|
+
setStatus("busy", text("doctorRunning"));
|
|
707
|
+
setStep("doctor", "active", text("doctorRunning"));
|
|
708
|
+
setProgress(35, text("progressDoctor"), "active");
|
|
709
|
+
appendLog(text("environmentPreparing"), "animated");
|
|
710
|
+
|
|
711
|
+
try {
|
|
712
|
+
const firstResponse = await api.runDoctor(state.project.projectRoot);
|
|
713
|
+
appendLog(firstResponse.text, firstResponse.ok ? "success" : "error");
|
|
714
|
+
|
|
715
|
+
if (firstResponse.ok) {
|
|
716
|
+
markEnvironmentReady();
|
|
717
|
+
return true;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
if (allowInstall && hasAndroidInstallableFailure(firstResponse)) {
|
|
721
|
+
const installed = await installAndroidRequirements();
|
|
722
|
+
if (!installed) {
|
|
723
|
+
return false;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
setStatus("busy", text("doctorRunning"));
|
|
727
|
+
setStep("doctor", "active", text("doctorRunning"));
|
|
728
|
+
setProgress(55, text("progressDoctor"), "active");
|
|
729
|
+
appendLog(text("environmentPreparing"), "animated");
|
|
730
|
+
|
|
731
|
+
const retryResponse = await api.runDoctor(state.project.projectRoot);
|
|
732
|
+
appendLog(retryResponse.text, retryResponse.ok ? "success" : "error");
|
|
733
|
+
if (retryResponse.ok) {
|
|
734
|
+
markEnvironmentReady();
|
|
735
|
+
return true;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
markEnvironmentBlocked(text("doctorFail"));
|
|
740
|
+
return false;
|
|
741
|
+
} catch (error) {
|
|
742
|
+
appendLog(error.message, "error");
|
|
743
|
+
markEnvironmentBlocked(error.message);
|
|
744
|
+
return false;
|
|
745
|
+
} finally {
|
|
746
|
+
state.environmentChecking = false;
|
|
747
|
+
validateSettings();
|
|
748
|
+
updateActionButtons();
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
async function ensureEnvironmentBeforeSettings() {
|
|
753
|
+
const ready = state.environmentOk || await verifyEnvironment({ allowInstall: true, requireSettings: false });
|
|
754
|
+
if (ready) {
|
|
755
|
+
validateSettings();
|
|
756
|
+
setView("settings");
|
|
757
|
+
} else {
|
|
758
|
+
setView("build");
|
|
759
|
+
}
|
|
760
|
+
return ready;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
async function summarizeProject(project) {
|
|
764
|
+
state.project = project;
|
|
765
|
+
state.doctorOk = false;
|
|
766
|
+
state.environmentOk = false;
|
|
767
|
+
state.environmentChecking = false;
|
|
768
|
+
state.environmentFailed = false;
|
|
769
|
+
state.settingsValid = false;
|
|
770
|
+
state.lastApkPath = null;
|
|
771
|
+
state.lastDistPath = project.distPath;
|
|
772
|
+
|
|
773
|
+
elements.projectSummary.classList.remove("hidden");
|
|
774
|
+
elements.projectName.textContent = project.name;
|
|
775
|
+
elements.projectPath.textContent = project.projectRoot;
|
|
776
|
+
elements.configStatus.textContent = project.hasAppJson || project.hasConfigJson ? text("detected") : text("notDetected");
|
|
777
|
+
elements.entryStatus.textContent = project.hasEntryFile ? text("detected") : text("missing");
|
|
778
|
+
elements.entryPath.textContent = project.entryPath;
|
|
779
|
+
elements.nextSettingsButton.disabled = false;
|
|
780
|
+
elements.doctorButton.disabled = true;
|
|
781
|
+
setBuildButtons(false);
|
|
782
|
+
populateSettings(project.config || {}, project);
|
|
783
|
+
setStep("folder", project.hasEntryFile ? "done" : "active", project.hasEntryFile ? text("folderReady") : text("missing"));
|
|
784
|
+
setStep("settings", "active", text("settingsMissing"));
|
|
785
|
+
setStep("doctor", null, "-");
|
|
786
|
+
setStep("build", null, "-");
|
|
787
|
+
elements.resultPanel.classList.add("hidden");
|
|
788
|
+
setStatus("ready", text("projectLoaded"));
|
|
789
|
+
appendLog(`${text("droppedFolder")}: ${project.projectRoot}`, "system");
|
|
790
|
+
validateSettings();
|
|
791
|
+
setProgress(project.hasEntryFile ? 25 : 15, project.hasEntryFile ? text("progressFolder") : text("missing"), project.hasEntryFile ? "" : "error");
|
|
792
|
+
await ensureEnvironmentBeforeSettings();
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
async function loadProject(projectRoot) {
|
|
796
|
+
try {
|
|
797
|
+
const project = await api.inspectProject(projectRoot);
|
|
798
|
+
await summarizeProject(project);
|
|
799
|
+
} catch (error) {
|
|
800
|
+
setStatus("error", error.message);
|
|
801
|
+
appendLog(error.message, "error");
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
async function chooseFolder() {
|
|
806
|
+
try {
|
|
807
|
+
const project = await api.selectFolder();
|
|
808
|
+
if (project) {
|
|
809
|
+
await summarizeProject(project);
|
|
810
|
+
}
|
|
811
|
+
} catch (error) {
|
|
812
|
+
setStatus("error", error.message);
|
|
813
|
+
appendLog(error.message, "error");
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
async function runDoctorOnly() {
|
|
818
|
+
if (!state.project) {
|
|
819
|
+
setStatus("error", text("chooseProjectFirst"));
|
|
820
|
+
return false;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
return verifyEnvironment({ allowInstall: true, requireSettings: false });
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function buildOptions() {
|
|
827
|
+
return {
|
|
828
|
+
projectRoot: state.project.projectRoot,
|
|
829
|
+
appName: elements.appNameInput.value.trim(),
|
|
830
|
+
packageId: elements.packageIdInput.value.trim(),
|
|
831
|
+
version: elements.versionInput.value.trim(),
|
|
832
|
+
mode: elements.modeInput.value,
|
|
833
|
+
icon: elements.iconPathInput.value.trim(),
|
|
834
|
+
androidPlatform: elements.androidPlatformInput.value.trim(),
|
|
835
|
+
debug: elements.debugInput.checked,
|
|
836
|
+
release: elements.releaseInput.checked
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function startAnimatedLogs() {
|
|
841
|
+
stopAnimatedLogs();
|
|
842
|
+
let index = 0;
|
|
843
|
+
state.animationTimer = setInterval(() => {
|
|
844
|
+
appendLog(animatedBuildLines[index % animatedBuildLines.length], "animated");
|
|
845
|
+
index += 1;
|
|
846
|
+
}, 1650);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function stopAnimatedLogs() {
|
|
850
|
+
if (state.animationTimer) {
|
|
851
|
+
clearInterval(state.animationTimer);
|
|
852
|
+
state.animationTimer = null;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
async function runBuildFlow() {
|
|
857
|
+
if (!state.project || state.buildRunning) {
|
|
858
|
+
setStatus("error", state.project ? text("buildRunning") : text("chooseProjectFirst"));
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
if (!goToReview()) {
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
state.buildRunning = true;
|
|
866
|
+
updateActionButtons();
|
|
867
|
+
elements.resultPanel.classList.add("hidden");
|
|
868
|
+
setView("build");
|
|
869
|
+
setStep("folder", "done", text("folderReady"));
|
|
870
|
+
|
|
871
|
+
const doctorOk = await runDoctorOnly();
|
|
872
|
+
if (!doctorOk) {
|
|
873
|
+
state.buildRunning = false;
|
|
874
|
+
updateActionButtons();
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
setStatus("busy", text("buildRunning"));
|
|
879
|
+
setStep("build", "active", text("buildRunning"));
|
|
880
|
+
setProgress(90, text("progressBuild"), "active");
|
|
881
|
+
startAnimatedLogs();
|
|
882
|
+
|
|
883
|
+
try {
|
|
884
|
+
const response = await api.runBuild(buildOptions());
|
|
885
|
+
stopAnimatedLogs();
|
|
886
|
+
if (!response.ok) {
|
|
887
|
+
setStep("build", "error", text("buildFail"));
|
|
888
|
+
setStatus("error", text("buildFail"));
|
|
889
|
+
setProgress(90, text("progressError"), "error");
|
|
890
|
+
appendLog(response.message || text("buildFail"), "error");
|
|
891
|
+
if (response.buildDir) {
|
|
892
|
+
appendLog(`Build directory kept: ${response.buildDir}`, "system");
|
|
893
|
+
}
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
const result = response.result;
|
|
898
|
+
state.lastApkPath = result.apkPath;
|
|
899
|
+
state.lastDistPath = state.project.distPath;
|
|
900
|
+
elements.apkPath.textContent = result.apkPath;
|
|
901
|
+
elements.successApkPath.textContent = result.apkPath;
|
|
902
|
+
elements.resultPanel.classList.remove("hidden");
|
|
903
|
+
setStep("build", "done", text("buildOk"));
|
|
904
|
+
setStatus("ready", text("buildOk"));
|
|
905
|
+
setProgress(100, text("progressDone"));
|
|
906
|
+
appendLog(`${text("buildOk")}: ${result.apkPath}`, "success");
|
|
907
|
+
setView("success");
|
|
908
|
+
} catch (error) {
|
|
909
|
+
stopAnimatedLogs();
|
|
910
|
+
setStep("build", "error", text("buildFail"));
|
|
911
|
+
setStatus("error", error.message);
|
|
912
|
+
setProgress(90, text("progressError"), "error");
|
|
913
|
+
appendLog(error.message, "error");
|
|
914
|
+
} finally {
|
|
915
|
+
state.buildRunning = false;
|
|
916
|
+
updateActionButtons();
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function toggleTheme() {
|
|
921
|
+
state.theme = state.theme === "dark" ? "light" : "dark";
|
|
922
|
+
applyTheme();
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function chooseLanguage(language) {
|
|
926
|
+
state.language = language;
|
|
927
|
+
localStorage.setItem("html2apk.language", language);
|
|
928
|
+
applyLanguage();
|
|
929
|
+
hideOverlay(elements.languageOverlay);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function hideOverlay(overlay) {
|
|
933
|
+
overlay.classList.add("hiding");
|
|
934
|
+
setTimeout(() => overlay.classList.add("hidden"), 360);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function finishBoot() {
|
|
938
|
+
hideOverlay(elements.creditOverlay);
|
|
939
|
+
if (!state.language) {
|
|
940
|
+
setTimeout(() => elements.languageOverlay.classList.remove("hidden"), 380);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function bindEvents() {
|
|
945
|
+
document.querySelectorAll(".nav-item[data-view]").forEach((button) => {
|
|
946
|
+
button.addEventListener("click", async () => {
|
|
947
|
+
if (button.dataset.view === "settings" && state.project && !state.environmentOk) {
|
|
948
|
+
await ensureEnvironmentBeforeSettings();
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
if (button.dataset.view === "build" && !goToReview()) {
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
setView(button.dataset.view);
|
|
955
|
+
});
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
document.querySelectorAll("[data-language-choice]").forEach((button) => {
|
|
959
|
+
button.addEventListener("click", () => chooseLanguage(button.dataset.languageChoice));
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
elements.themeToggle.addEventListener("click", toggleTheme);
|
|
963
|
+
elements.themeToggleLarge.addEventListener("click", toggleTheme);
|
|
964
|
+
elements.selectFolderButton.addEventListener("click", chooseFolder);
|
|
965
|
+
elements.nextSettingsButton.addEventListener("click", ensureEnvironmentBeforeSettings);
|
|
966
|
+
elements.settingsNextButton.addEventListener("click", goToReview);
|
|
967
|
+
elements.doctorButton.addEventListener("click", runDoctorOnly);
|
|
968
|
+
elements.buildButton.addEventListener("click", runBuildFlow);
|
|
969
|
+
elements.clearLogsButton.addEventListener("click", clearLogs);
|
|
970
|
+
elements.toggleLogsButton.addEventListener("click", toggleLogBar);
|
|
971
|
+
elements.bottomToggleLogsButton.addEventListener("click", toggleLogBar);
|
|
972
|
+
elements.bottomClearLogsButton.addEventListener("click", clearLogs);
|
|
973
|
+
elements.selectIconButton.addEventListener("click", async () => {
|
|
974
|
+
const iconPath = await api.selectIcon();
|
|
975
|
+
if (!iconPath) {
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
elements.iconPathInput.value = iconPath;
|
|
979
|
+
elements.iconPreview.src = iconPreviewPath(iconPath);
|
|
980
|
+
appendLog(`${text("iconSelected")}: ${iconPath}`, "system");
|
|
981
|
+
validateSettings();
|
|
982
|
+
});
|
|
983
|
+
[
|
|
984
|
+
elements.appNameInput,
|
|
985
|
+
elements.packageIdInput,
|
|
986
|
+
elements.versionInput,
|
|
987
|
+
elements.modeInput,
|
|
988
|
+
elements.androidPlatformInput,
|
|
989
|
+
elements.debugInput,
|
|
990
|
+
elements.releaseInput
|
|
991
|
+
].forEach((input) => {
|
|
992
|
+
input.addEventListener("input", validateSettings);
|
|
993
|
+
input.addEventListener("change", validateSettings);
|
|
994
|
+
});
|
|
995
|
+
elements.dragRegion.addEventListener("dblclick", () => api.toggleMaximizeWindow());
|
|
996
|
+
elements.minimizeButton.addEventListener("click", () => api.minimizeWindow());
|
|
997
|
+
elements.maximizeButton.addEventListener("click", () => api.toggleMaximizeWindow());
|
|
998
|
+
elements.closeButton.addEventListener("click", () => api.closeWindow());
|
|
999
|
+
elements.openDistButton.addEventListener("click", () => {
|
|
1000
|
+
if (state.lastDistPath) {
|
|
1001
|
+
api.openPath(state.lastDistPath);
|
|
1002
|
+
}
|
|
1003
|
+
});
|
|
1004
|
+
elements.showApkButton.addEventListener("click", () => {
|
|
1005
|
+
if (state.lastApkPath) {
|
|
1006
|
+
api.showItem(state.lastApkPath);
|
|
1007
|
+
}
|
|
1008
|
+
});
|
|
1009
|
+
elements.successOpenDistButton.addEventListener("click", () => {
|
|
1010
|
+
if (state.lastDistPath) {
|
|
1011
|
+
api.openPath(state.lastDistPath);
|
|
1012
|
+
}
|
|
1013
|
+
});
|
|
1014
|
+
elements.successShowApkButton.addEventListener("click", () => {
|
|
1015
|
+
if (state.lastApkPath) {
|
|
1016
|
+
api.showItem(state.lastApkPath);
|
|
1017
|
+
}
|
|
1018
|
+
});
|
|
1019
|
+
elements.newBuildButton.addEventListener("click", () => {
|
|
1020
|
+
state.lastApkPath = null;
|
|
1021
|
+
elements.resultPanel.classList.add("hidden");
|
|
1022
|
+
goToReview();
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
elements.dropZone.addEventListener("dragover", (event) => {
|
|
1026
|
+
event.preventDefault();
|
|
1027
|
+
elements.dropZone.classList.add("dragging");
|
|
1028
|
+
});
|
|
1029
|
+
elements.dropZone.addEventListener("dragleave", () => {
|
|
1030
|
+
elements.dropZone.classList.remove("dragging");
|
|
1031
|
+
});
|
|
1032
|
+
elements.dropZone.addEventListener("drop", async (event) => {
|
|
1033
|
+
event.preventDefault();
|
|
1034
|
+
elements.dropZone.classList.remove("dragging");
|
|
1035
|
+
const file = event.dataTransfer.files && event.dataTransfer.files[0];
|
|
1036
|
+
if (!file) {
|
|
1037
|
+
setStatus("error", text("noFolderDrop"));
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
const projectRoot = api.pathForFile(file);
|
|
1041
|
+
await loadProject(projectRoot);
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
api.onBuildLog((payload) => {
|
|
1045
|
+
appendLog(payload.line, payload.kind || "raw");
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
api.onInstallLog((payload) => {
|
|
1049
|
+
appendLog(payload.line, payload.kind || "raw");
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
async function init() {
|
|
1054
|
+
collectElements();
|
|
1055
|
+
bindEvents();
|
|
1056
|
+
applyLanguage();
|
|
1057
|
+
applyTheme();
|
|
1058
|
+
applyLogBarVisibility();
|
|
1059
|
+
setStatus("ready", text("ready"));
|
|
1060
|
+
setProgress(0, text("progressIdle"));
|
|
1061
|
+
updateActionButtons();
|
|
1062
|
+
appendLog("html2apk desktop ready / interface pronta", "system");
|
|
1063
|
+
|
|
1064
|
+
try {
|
|
1065
|
+
const info = await api.appInfo();
|
|
1066
|
+
elements.appVersion.textContent = `v${info.version}`;
|
|
1067
|
+
} catch {
|
|
1068
|
+
elements.appVersion.textContent = "v0.1.0";
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
setTimeout(finishBoot, 1800);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
document.addEventListener("DOMContentLoaded", init);
|