html2apk 0.1.0 → 0.3.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.
@@ -8,11 +8,15 @@ const { validateEntryFile, validateRequiredOptions } = require("./validation");
8
8
  const { createCordovaProject, addAndroidPlatform, buildAndroid, addCordovaPlugin } = require("../cordova/project");
9
9
  const { writeConfigXml } = require("../cordova/config-xml");
10
10
  const { findApk } = require("../cordova/apk-finder");
11
- const { copyWebAssets, ensureDir, removePath, copyFile } = require("../utils/fs-extra");
11
+ const { copyWebAssets, ensureDir, removePath, copyFile, copyDirectory } = require("../utils/fs-extra");
12
12
  const { createCommandRunner } = require("../utils/command-runner");
13
13
  const { installBridgePlugin } = require("../bridge/install-bridge");
14
14
  const { getRuntimeEnvironment } = require("../runtime-manager");
15
15
 
16
+ const AUTO_THEME_SCRIPT_NAME = "html2apk-auto-theme.js";
17
+ const ONESIGNAL_SCRIPT_NAME = "html2apk-onesignal.js";
18
+ const ONESIGNAL_PLUGIN_PACKAGE = "onesignal-cordova-plugin";
19
+
16
20
  function isRemoteAsset(assetPath) {
17
21
  return /^https?:\/\//i.test(String(assetPath || ""));
18
22
  }
@@ -34,6 +38,141 @@ function toCordovaPath(value) {
34
38
  return String(value).replace(/\\/g, "/");
35
39
  }
36
40
 
41
+ function isAutoTheme(options) {
42
+ return String(options.themeMode || options.theme || "").toLowerCase() === "auto";
43
+ }
44
+
45
+ function oneSignalAppId(options) {
46
+ return String(options.oneSignalAppId || options.onesignalAppId || options.oneSignal?.appId || options.onesignal?.appId || "").trim();
47
+ }
48
+
49
+ function hasOneSignal(options) {
50
+ return oneSignalAppId(options).length > 0;
51
+ }
52
+
53
+ function scriptTag(scriptPath) {
54
+ return `<script src="${scriptPath}"></script>`;
55
+ }
56
+
57
+ async function injectScriptIntoHtml(htmlPath, scriptPath) {
58
+ let html = await fs.readFile(htmlPath, "utf8");
59
+ if (html.includes(scriptPath)) {
60
+ return;
61
+ }
62
+
63
+ const tag = scriptTag(scriptPath);
64
+ if (/<\/body>/i.test(html)) {
65
+ html = html.replace(/<\/body>/i, ` ${tag}\n</body>`);
66
+ } else {
67
+ html = `${html}\n${tag}\n`;
68
+ }
69
+
70
+ await fs.writeFile(htmlPath, html, "utf8");
71
+ }
72
+
73
+ async function installAutoThemeScript(buildDir, options) {
74
+ if (!isAutoTheme(options)) {
75
+ return false;
76
+ }
77
+
78
+ const wwwDir = path.join(buildDir, "www");
79
+ const source = path.resolve(__dirname, "..", "templates", AUTO_THEME_SCRIPT_NAME);
80
+ const target = path.join(wwwDir, AUTO_THEME_SCRIPT_NAME);
81
+ const entryHtmlPath = path.resolve(wwwDir, options.entryFile || "index.html");
82
+ const scriptPath = toCordovaPath(path.relative(path.dirname(entryHtmlPath), target));
83
+
84
+ if (!isInside(wwwDir, entryHtmlPath)) {
85
+ throw new Error(`Entry file must stay inside the Cordova www folder: ${options.entryFile}`);
86
+ }
87
+
88
+ await copyFile(source, target);
89
+ await injectScriptIntoHtml(entryHtmlPath, scriptPath || AUTO_THEME_SCRIPT_NAME);
90
+ return true;
91
+ }
92
+
93
+ function jsString(value) {
94
+ return JSON.stringify(String(value || ""));
95
+ }
96
+
97
+ async function installOneSignalScript(buildDir, options) {
98
+ const appId = oneSignalAppId(options);
99
+ if (!appId) {
100
+ return false;
101
+ }
102
+
103
+ const wwwDir = path.join(buildDir, "www");
104
+ const source = path.resolve(__dirname, "..", "templates", ONESIGNAL_SCRIPT_NAME);
105
+ const target = path.join(wwwDir, ONESIGNAL_SCRIPT_NAME);
106
+ const entryHtmlPath = path.resolve(wwwDir, options.entryFile || "index.html");
107
+ const scriptPath = toCordovaPath(path.relative(path.dirname(entryHtmlPath), target));
108
+ const template = await fs.readFile(source, "utf8");
109
+
110
+ if (!isInside(wwwDir, entryHtmlPath)) {
111
+ throw new Error(`Entry file must stay inside the Cordova www folder: ${options.entryFile}`);
112
+ }
113
+
114
+ await fs.writeFile(
115
+ target,
116
+ template.replace("__HTML2APK_ONESIGNAL_APP_ID__", jsString(appId)),
117
+ "utf8"
118
+ );
119
+ await injectScriptIntoHtml(entryHtmlPath, scriptPath || ONESIGNAL_SCRIPT_NAME);
120
+ return true;
121
+ }
122
+
123
+ function removePluginPlatform(pluginXml, platformName) {
124
+ const pattern = new RegExp(`\\n?\\s*<platform name="${platformName}">[\\s\\S]*?\\n\\s*</platform>`, "i");
125
+ return pluginXml.replace(pattern, "");
126
+ }
127
+
128
+ async function prepareBundledPlugin(buildDir, packageName) {
129
+ const localPath = path.resolve(__dirname, "..", "..", "node_modules", packageName);
130
+ try {
131
+ await fs.access(path.join(localPath, "plugin.xml"));
132
+ } catch {
133
+ return packageName;
134
+ }
135
+
136
+ const destination = path.join(buildDir, `${packageName}-android`);
137
+ await removePath(destination);
138
+ await copyDirectory(localPath, destination, () => false);
139
+
140
+ const packageJsonPath = path.join(destination, "package.json");
141
+ const sourcePackage = JSON.parse(await fs.readFile(packageJsonPath, "utf8"));
142
+ const pluginPackage = {
143
+ name: sourcePackage.name,
144
+ version: sourcePackage.version,
145
+ description: sourcePackage.description,
146
+ license: sourcePackage.license,
147
+ main: sourcePackage.main || "dist/index.cjs",
148
+ types: sourcePackage.types,
149
+ cordova: {
150
+ id: sourcePackage.cordova?.id || packageName,
151
+ platforms: ["android"]
152
+ }
153
+ };
154
+ Object.keys(pluginPackage).forEach((key) => {
155
+ if (pluginPackage[key] === undefined) {
156
+ delete pluginPackage[key];
157
+ }
158
+ });
159
+ await fs.writeFile(packageJsonPath, `${JSON.stringify(pluginPackage, null, 2)}\n`, "utf8");
160
+
161
+ const pluginXmlPath = path.join(destination, "plugin.xml");
162
+ let pluginXml = await fs.readFile(pluginXmlPath, "utf8");
163
+ pluginXml = removePluginPlatform(pluginXml, "ios");
164
+ await fs.writeFile(pluginXmlPath, pluginXml, "utf8");
165
+
166
+ return destination;
167
+ }
168
+
169
+ function hasPlugin(plugins, packageName) {
170
+ return (plugins || []).some((plugin) => {
171
+ const text = String(plugin || "").replace(/\\/g, "/").toLowerCase();
172
+ return text === packageName || text.endsWith(`/node_modules/${packageName}`) || text.endsWith(`/${packageName}`);
173
+ });
174
+ }
175
+
37
176
  async function copyCordovaAsset(projectRoot, buildDir, assetPath, assetName) {
38
177
  if (!assetPath || isRemoteAsset(assetPath)) {
39
178
  return assetPath;
@@ -136,10 +275,20 @@ async function buildApk(overrides = {}) {
136
275
  cordovaOptions.androidSplashScreenAnimatedIcon = toBuildAssetPath(buildDir, cordovaOptions.splash);
137
276
  await writeConfigXml(path.join(buildDir, "config.xml"), cordovaOptions);
138
277
  await copyWebAssets(webRoot, path.join(buildDir, "www"), options, projectRoot);
278
+ if (await installAutoThemeScript(buildDir, options)) {
279
+ log("Theme mode: auto (system bars follow the visible screen color).");
280
+ }
281
+ if (await installOneSignalScript(buildDir, options)) {
282
+ log("OneSignal: enabled for remote push notifications.");
283
+ }
139
284
 
140
285
  const bridgePluginPath = await installBridgePlugin(buildDir);
141
286
  await addCordovaPlugin(buildDir, bridgePluginPath, runner);
142
287
 
288
+ if (hasOneSignal(options) && !hasPlugin(options.plugins, ONESIGNAL_PLUGIN_PACKAGE)) {
289
+ await addCordovaPlugin(buildDir, await prepareBundledPlugin(buildDir, ONESIGNAL_PLUGIN_PACKAGE), runner);
290
+ }
291
+
143
292
  for (const plugin of options.plugins) {
144
293
  await addCordovaPlugin(buildDir, plugin, runner);
145
294
  }
@@ -2,7 +2,11 @@
2
2
 
3
3
  const fs = require("fs/promises");
4
4
  const path = require("path");
5
- const { createDefaultOptions } = require("./defaults");
5
+ const {
6
+ DEFAULT_ANDROID_MIN_SDK_VERSION,
7
+ MAX_ANDROID_MIN_SDK_VERSION,
8
+ createDefaultOptions
9
+ } = require("./defaults");
6
10
 
7
11
  const CONFIG_FILES = ["app.json", "config.json"];
8
12
 
@@ -58,24 +62,85 @@ function mergeDeep(base, next) {
58
62
  return output;
59
63
  }
60
64
 
65
+ function normalizeMinSdkVersion(value) {
66
+ const parsed = Number.parseInt(value, 10);
67
+ return Number.isInteger(parsed)
68
+ && parsed >= DEFAULT_ANDROID_MIN_SDK_VERSION
69
+ && parsed <= MAX_ANDROID_MIN_SDK_VERSION
70
+ ? parsed
71
+ : DEFAULT_ANDROID_MIN_SDK_VERSION;
72
+ }
73
+
74
+ function normalizeThemeMode(value) {
75
+ return String(value || "").trim().toLowerCase() === "auto" ? "auto" : "fixed";
76
+ }
77
+
78
+ function normalizeThemeColor(value) {
79
+ const color = String(value || "").trim();
80
+ return /^#[0-9a-fA-F]{6}$/.test(color) ? color : "#126fff";
81
+ }
82
+
61
83
  function normalizeOptions(options) {
62
84
  const normalized = { ...options };
63
85
 
64
- normalized.mode = normalized.mode === "fullscreen" ? "fullscreen" : "standalone";
86
+ normalized.mode = ["fullscreen", "floating"].includes(normalized.mode) ? normalized.mode : "standalone";
87
+ if (normalized.orientation === "vertical") {
88
+ normalized.orientation = "portrait";
89
+ } else if (normalized.orientation === "horizontal") {
90
+ normalized.orientation = "landscape";
91
+ }
92
+ normalized.orientation = ["portrait", "landscape"].includes(normalized.orientation)
93
+ ? normalized.orientation
94
+ : "default";
65
95
  normalized.debug = Boolean(normalized.debug);
66
96
  normalized.release = Boolean(normalized.release);
97
+ const themeColorText = String(normalized.themeColor || "").trim().toLowerCase();
98
+ const themeModeText = String(normalized.themeMode || "").trim().toLowerCase();
99
+ const themeText = String(normalized.theme || "").trim().toLowerCase();
100
+ normalized.themeMode = themeModeText === "auto" || themeText === "auto" || themeColorText === "auto" ? "auto" : "fixed";
101
+ normalized.theme = normalized.themeMode;
102
+ normalized.themeColor = normalizeThemeColor(themeColorText === "auto" ? normalized.backgroundColor : normalized.themeColor);
103
+ normalized.oneSignalAppId = normalizeOneSignalAppId(normalized.oneSignalAppId || normalized.onesignalAppId || normalized.oneSignal?.appId || normalized.onesignal?.appId);
104
+ normalized.minSdkVersion = normalizeMinSdkVersion(normalized.minSdkVersion || normalized.androidMinSdkVersion);
67
105
  normalized.permissions = Array.isArray(normalized.permissions)
68
- ? normalized.permissions.filter(Boolean)
106
+ ? normalized.permissions.map((permission) => String(permission).trim()).filter(Boolean)
69
107
  : [];
70
108
  normalized.plugins = Array.isArray(normalized.plugins)
71
- ? normalized.plugins.filter(Boolean)
109
+ ? normalized.plugins.map((plugin) => String(plugin).trim()).filter(Boolean)
72
110
  : [];
111
+ normalized.deepLinks = normalizeDeepLinks(normalized.deepLinks);
73
112
  normalized.entryFile = normalized.entryFile || "index.html";
74
113
  normalized.webRoot = normalized.webRoot || ".";
75
114
 
76
115
  return normalized;
77
116
  }
78
117
 
118
+ function normalizeOneSignalAppId(value) {
119
+ return String(value || "").trim();
120
+ }
121
+
122
+ function normalizeDeepLinks(value) {
123
+ const input = value && typeof value === "object" ? value : {};
124
+ const schemes = Array.isArray(input.schemes)
125
+ ? input.schemes.map((scheme) => String(scheme).trim()).filter(Boolean)
126
+ : [];
127
+ const appLinks = Array.isArray(input.appLinks)
128
+ ? input.appLinks
129
+ .filter((item) => item && typeof item === "object" && item.host)
130
+ .map((item) => ({
131
+ scheme: item.scheme || "https",
132
+ host: String(item.host).trim(),
133
+ paths: Array.isArray(item.paths) ? item.paths.map((pathItem) => String(pathItem).trim()).filter(Boolean) : [],
134
+ autoVerify: Boolean(item.autoVerify)
135
+ }))
136
+ : [];
137
+
138
+ return {
139
+ schemes,
140
+ appLinks
141
+ };
142
+ }
143
+
79
144
  async function resolveBuildOptions(overrides = {}) {
80
145
  const projectRoot = path.resolve(overrides.projectRoot || process.cwd());
81
146
  const { config, configPath } = await loadProjectConfig(projectRoot);
@@ -95,5 +160,9 @@ module.exports = {
95
160
  loadProjectConfig,
96
161
  resolveBuildOptions,
97
162
  mergeDeep,
98
- normalizeOptions
163
+ normalizeOptions,
164
+ normalizeMinSdkVersion,
165
+ normalizeThemeMode,
166
+ normalizeOneSignalAppId,
167
+ normalizeDeepLinks
99
168
  };
@@ -2,6 +2,9 @@
2
2
 
3
3
  const path = require("path");
4
4
 
5
+ const DEFAULT_ANDROID_MIN_SDK_VERSION = 24;
6
+ const MAX_ANDROID_MIN_SDK_VERSION = 36;
7
+
5
8
  function toPackageSegment(value) {
6
9
  return String(value || "app")
7
10
  .toLowerCase()
@@ -17,14 +20,23 @@ function createDefaultOptions(projectRoot) {
17
20
  packageId: `com.html2apk.${toPackageSegment(appName)}`,
18
21
  version: "1.0.0",
19
22
  mode: "standalone",
23
+ orientation: "default",
20
24
  debug: false,
21
25
  icon: null,
22
26
  splash: null,
23
- permissions: ["INTERNET"],
27
+ permissions: ["INTERNET", "POST_NOTIFICATIONS", "VIBRATE"],
24
28
  plugins: [],
25
29
  release: false,
26
30
  keystore: null,
27
31
  androidPlatform: "android@15.0.0",
32
+ minSdkVersion: DEFAULT_ANDROID_MIN_SDK_VERSION,
33
+ themeColor: "#126fff",
34
+ themeMode: "fixed",
35
+ oneSignalAppId: "",
36
+ deepLinks: {
37
+ schemes: [],
38
+ appLinks: []
39
+ },
28
40
  files: null,
29
41
  entryFile: "index.html",
30
42
  webRoot: ".",
@@ -33,5 +45,7 @@ function createDefaultOptions(projectRoot) {
33
45
  }
34
46
 
35
47
  module.exports = {
48
+ DEFAULT_ANDROID_MIN_SDK_VERSION,
49
+ MAX_ANDROID_MIN_SDK_VERSION,
36
50
  createDefaultOptions
37
51
  };
@@ -14,12 +14,20 @@ const { runDoctor, formatDoctorReport } = require("../runtime-manager/doctor");
14
14
 
15
15
  let mainWindow = null;
16
16
  const smokeTest = process.env.HTML2APK_DESKTOP_SMOKE === "1";
17
+ const APP_ID = "dev.caiomultiversando.html2apk";
18
+ const APP_NAME = "html2apk";
17
19
  const WINDOW_BACKGROUNDS = {
18
20
  light: "#f7fbff",
19
21
  dark: "#10141b"
20
22
  };
21
23
 
22
24
  app.commandLine.appendSwitch("disable-crash-reporter");
25
+ process.title = APP_NAME;
26
+ app.setName(APP_NAME);
27
+
28
+ if (process.platform === "win32") {
29
+ app.setAppUserModelId(APP_ID);
30
+ }
23
31
 
24
32
  function rootPath(...parts) {
25
33
  return path.resolve(__dirname, "..", "..", ...parts);
@@ -57,7 +65,7 @@ function createWindow() {
57
65
  center: true,
58
66
  show: false,
59
67
  frame: false,
60
- title: "html2apk",
68
+ title: APP_NAME,
61
69
  icon: iconPath(),
62
70
  backgroundColor: WINDOW_BACKGROUNDS.light,
63
71
  webPreferences: {
@@ -134,8 +142,8 @@ function cleanBuildOptions(options = {}) {
134
142
  release: Boolean(options.release)
135
143
  };
136
144
 
137
- for (const key of ["mode", "appName", "packageId", "version", "icon", "androidPlatform"]) {
138
- if (options[key]) {
145
+ for (const key of ["mode", "appName", "packageId", "version", "icon", "androidPlatform", "minSdkVersion", "themeColor", "themeMode", "theme", "oneSignalAppId", "orientation", "permissions", "deepLinks"]) {
146
+ if (Array.isArray(options[key]) || options[key]) {
139
147
  output[key] = options[key];
140
148
  }
141
149
  }
@@ -343,6 +351,10 @@ async function installWithWingetAndroidCli(sender) {
343
351
  }
344
352
 
345
353
  app.whenReady().then(() => {
354
+ app.setName(APP_NAME);
355
+ if (process.platform === "win32") {
356
+ app.setAppUserModelId(APP_ID);
357
+ }
346
358
  createWindow();
347
359
 
348
360
  app.on("activate", () => {
@@ -359,7 +371,7 @@ app.on("window-all-closed", () => {
359
371
  });
360
372
 
361
373
  ipcMain.handle("app:info", () => ({
362
- name: "html2apk",
374
+ name: APP_NAME,
363
375
  version: app.getVersion(),
364
376
  credit: "Dev Caio Multiversando",
365
377
  iconPath: iconPath()
@@ -520,3 +532,17 @@ ipcMain.handle("shell:show-item", async (_event, targetPath) => {
520
532
  shell.showItemInFolder(targetPath);
521
533
  return true;
522
534
  });
535
+
536
+ ipcMain.handle("shell:open-external", async (_event, targetUrl) => {
537
+ if (!targetUrl) {
538
+ return false;
539
+ }
540
+
541
+ const url = new URL(String(targetUrl));
542
+ if (!["https:", "http:"].includes(url.protocol)) {
543
+ return false;
544
+ }
545
+
546
+ await shell.openExternal(url.toString());
547
+ return true;
548
+ });
@@ -12,6 +12,7 @@ contextBridge.exposeInMainWorld("html2apkDesktop", {
12
12
  runBuild: (options) => ipcRenderer.invoke("build:run", options),
13
13
  openPath: (targetPath) => ipcRenderer.invoke("shell:open-path", targetPath),
14
14
  showItem: (targetPath) => ipcRenderer.invoke("shell:show-item", targetPath),
15
+ openExternalUrl: (targetUrl) => ipcRenderer.invoke("shell:open-external", targetUrl),
15
16
  minimizeWindow: () => ipcRenderer.invoke("window:minimize"),
16
17
  toggleMaximizeWindow: () => ipcRenderer.invoke("window:toggle-maximize"),
17
18
  closeWindow: () => ipcRenderer.invoke("window:close"),
@@ -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="codes"><span class="nav-icon">J</span><span data-i18n="navCodes">Codigos</span></button>
61
62
  <button class="nav-item" data-view="logs"><span class="nav-icon">L</span><span data-i18n="navLogs">Logs</span></button>
62
63
  </nav>
63
64
 
@@ -140,14 +141,60 @@
140
141
  <span data-i18n="mode">Modo</span>
141
142
  <select id="modeInput">
142
143
  <option value="" data-i18n="chooseMode">Escolha o modo</option>
143
- <option value="fullscreen">fullscreen</option>
144
- <option value="standalone">standalone</option>
144
+ <option value="fullscreen" data-i18n="modeFullscreen">Tela cheia</option>
145
+ <option value="standalone" data-i18n="modeStandalone">Normal</option>
146
+ <option value="floating" data-i18n="modeFloating">Flutuante</option>
147
+ </select>
148
+ </label>
149
+ <label class="field">
150
+ <span data-i18n="orientation">Orientacao</span>
151
+ <select id="orientationInput">
152
+ <option value="default" data-i18n="orientationDefault">Automatico</option>
153
+ <option value="portrait" data-i18n="orientationPortrait">Vertical</option>
154
+ <option value="landscape" data-i18n="orientationLandscape">Horizontal</option>
155
+ </select>
156
+ </label>
157
+ <label class="field">
158
+ <span data-i18n="minSdkVersion">Android minimo</span>
159
+ <select id="minSdkVersionInput">
160
+ <option value="24">Android 7.0 (API 24)</option>
161
+ <option value="25">Android 7.1 (API 25)</option>
162
+ <option value="26">Android 8.0 (API 26)</option>
163
+ <option value="27">Android 8.1 (API 27)</option>
164
+ <option value="28">Android 9 (API 28)</option>
165
+ <option value="29">Android 10 (API 29)</option>
166
+ <option value="30">Android 11 (API 30)</option>
167
+ <option value="31">Android 12 (API 31)</option>
168
+ <option value="32">Android 12L (API 32)</option>
169
+ <option value="33">Android 13 (API 33)</option>
170
+ <option value="34">Android 14 (API 34)</option>
171
+ <option value="35">Android 15 (API 35)</option>
172
+ <option value="36">Android 16 (API 36)</option>
145
173
  </select>
146
174
  </label>
147
175
  <label class="field">
148
176
  <span>cordova-android</span>
149
177
  <input id="androidPlatformInput" type="text" placeholder="android@15.0.0">
150
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>
186
+ <label class="field color-field">
187
+ <span data-i18n="appThemeColor">Cor do tema do app</span>
188
+ <div class="color-picker">
189
+ <input id="themeColorInput" type="color" value="#126fff">
190
+ <input id="themeColorTextInput" type="text" value="#126fff" maxlength="7">
191
+ </div>
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>
151
198
  <div class="field icon-field">
152
199
  <span data-i18n="appIcon">Icone do app</span>
153
200
  <div class="icon-picker">
@@ -158,6 +205,10 @@
158
205
  </div>
159
206
  </div>
160
207
  </div>
208
+ <div class="field permissions-field">
209
+ <span data-i18n="androidPermissions">Permissoes Android</span>
210
+ <div id="permissionGrid" class="permission-grid"></div>
211
+ </div>
161
212
  </div>
162
213
 
163
214
  <div class="toggle-grid">
@@ -275,6 +326,17 @@
275
326
  <div id="logConsole" class="log-console"></div>
276
327
  </section>
277
328
 
329
+ <section id="view-codes" class="view">
330
+ <header class="view-header compact">
331
+ <div>
332
+ <p class="eyebrow" data-i18n="codesEyebrow">Bridge nativa</p>
333
+ <h1 data-i18n="codesTitle">Codigos interpretados</h1>
334
+ </div>
335
+ </header>
336
+ <p class="view-intro" data-i18n="codesIntro">Estas funcoes chamadas no JavaScript do app sao interpretadas pelo plugin Cordova e executadas no Java Android.</p>
337
+ <div id="nativeCodeGrid" class="code-grid"></div>
338
+ </section>
339
+
278
340
  <section id="view-help" class="view">
279
341
  <header class="view-header compact">
280
342
  <div>
@@ -295,6 +357,11 @@
295
357
  <strong data-i18n="helpDepsTitle">Dependencias no EXE</strong>
296
358
  <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>
297
359
  </article>
360
+ <article class="creator-card">
361
+ <strong data-i18n="helpCreatorTitle">Criador</strong>
362
+ <p data-i18n="helpCreatorText">Conheca o Dev Caio Multiversando, criador desta linda aplicacao html2apk.</p>
363
+ <button id="devInstagramButton" class="secondary-action instagram-action" type="button" data-i18n="openInstagram">Conhecer o dev</button>
364
+ </article>
298
365
  </div>
299
366
  </section>
300
367
  </main>