fix: Android app icon after init and Windows in-app link navigation
This commit is contained in:
39
.github/scripts/generate-app-icons.mjs
vendored
39
.github/scripts/generate-app-icons.mjs
vendored
@@ -10,7 +10,17 @@ const templateDir = path.join(root, "template");
|
|||||||
|
|
||||||
const ICON_PRIORITY = ["logo.png", "logo.jpg", "logo.jpeg", "favicon.ico"];
|
const ICON_PRIORITY = ["logo.png", "logo.jpg", "logo.jpeg", "favicon.ico"];
|
||||||
|
|
||||||
|
const BUNDLE_ICONS = [
|
||||||
|
"icons/32x32.png",
|
||||||
|
"icons/128x128.png",
|
||||||
|
"icons/128x128@2x.png",
|
||||||
|
"icons/icon.icns",
|
||||||
|
"icons/icon.ico",
|
||||||
|
];
|
||||||
|
|
||||||
function findIconSource(baseDir) {
|
function findIconSource(baseDir) {
|
||||||
|
if (!fs.existsSync(baseDir)) return null;
|
||||||
|
|
||||||
for (const name of ICON_PRIORITY) {
|
for (const name of ICON_PRIORITY) {
|
||||||
const direct = path.join(baseDir, name);
|
const direct = path.join(baseDir, name);
|
||||||
if (fs.existsSync(direct)) return direct;
|
if (fs.existsSync(direct)) return direct;
|
||||||
@@ -27,16 +37,26 @@ function findIconSource(baseDir) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateAppIcons() {
|
export function findIconSourceForJob(root, jobId, distDir) {
|
||||||
|
const buildDir = jobId ? path.join(root, "builds", jobId) : null;
|
||||||
|
if (buildDir && fs.existsSync(buildDir)) {
|
||||||
|
const fromUpload = findIconSource(buildDir);
|
||||||
|
if (fromUpload) return fromUpload;
|
||||||
|
}
|
||||||
|
return findIconSource(distDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateAppIcons(options = {}) {
|
||||||
|
const { jobId } = options;
|
||||||
if (!fs.existsSync(distDir)) {
|
if (!fs.existsSync(distDir)) {
|
||||||
console.log("No dist directory, keeping default icons");
|
console.log("No dist directory, keeping default icons");
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const iconSource = findIconSource(distDir);
|
const iconSource = findIconSourceForJob(root, jobId, distDir);
|
||||||
if (!iconSource) {
|
if (!iconSource) {
|
||||||
console.log("No logo.png or favicon.ico found, keeping default icons");
|
console.log("No logo.png or favicon.ico found, keeping default icons");
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Generating app icons from: ${path.relative(root, iconSource)}`);
|
console.log(`Generating app icons from: ${path.relative(root, iconSource)}`);
|
||||||
@@ -44,4 +64,15 @@ export function generateAppIcons() {
|
|||||||
cwd: templateDir,
|
cwd: templateDir,
|
||||||
stdio: "inherit",
|
stdio: "inherit",
|
||||||
});
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { BUNDLE_ICONS };
|
||||||
|
|
||||||
|
const isCli =
|
||||||
|
process.argv[1] &&
|
||||||
|
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
||||||
|
|
||||||
|
if (isCli) {
|
||||||
|
generateAppIcons({ jobId: process.env.JOB_ID });
|
||||||
}
|
}
|
||||||
|
|||||||
24
.github/scripts/inject-in-app-nav.mjs
vendored
Normal file
24
.github/scripts/inject-in-app-nav.mjs
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
const MARKER = "data-web2app-in-app-nav";
|
||||||
|
|
||||||
|
const NAV_SCRIPT = `(function(){if(window.__web2appNavInstalled)return;window.__web2appNavInstalled=true;function ok(a){if(!a||!a.href||a.hasAttribute("download"))return false;var h=a.getAttribute("href");if(!h||h.startsWith("#")||h.startsWith("javascript:"))return false;try{var u=new URL(a.href,location.href);return u.protocol==="http:"||u.protocol==="https:"}catch(e){return false}}function go(url){location.assign(url)}document.addEventListener("click",function(e){var a=e.target&&e.target.closest?e.target.closest("a"):null;if(!ok(a))return;if(a.target==="_blank"||a.target==="_top"||location.protocol==="file:"||location.protocol==="tauri:"||location.hostname==="asset.localhost"){e.preventDefault();go(a.href);return}try{var t=new URL(a.href,location.href),c=new URL(location.href);if(t.origin!==c.origin){e.preventDefault();go(a.href)}}catch(e){}},true);var o=window.open;window.open=function(url,t,f){if(url&&(!t||t==="_blank")){go(url);return null}return o.call(window,url,t,f)}})();`;
|
||||||
|
|
||||||
|
export function injectInAppNav(distDir) {
|
||||||
|
const indexPath = path.join(distDir, "index.html");
|
||||||
|
if (!fs.existsSync(indexPath)) return;
|
||||||
|
|
||||||
|
let html = fs.readFileSync(indexPath, "utf8");
|
||||||
|
if (html.includes(MARKER)) return;
|
||||||
|
|
||||||
|
const tag = `<script ${MARKER}>${NAV_SCRIPT}</script>`;
|
||||||
|
if (html.includes("</body>")) {
|
||||||
|
html = html.replace("</body>", `${tag}\n</body>`);
|
||||||
|
} else {
|
||||||
|
html += `\n${tag}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(indexPath, html);
|
||||||
|
console.log("Injected in-app navigation script into index.html");
|
||||||
|
}
|
||||||
16
.github/scripts/prepare-template.mjs
vendored
16
.github/scripts/prepare-template.mjs
vendored
@@ -2,7 +2,8 @@ import fs from "node:fs";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import AdmZip from "adm-zip";
|
import AdmZip from "adm-zip";
|
||||||
import { generateAppIcons } from "./generate-app-icons.mjs";
|
import { BUNDLE_ICONS, generateAppIcons } from "./generate-app-icons.mjs";
|
||||||
|
import { injectInAppNav } from "./inject-in-app-nav.mjs";
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
const root = path.resolve(__dirname, "../..");
|
const root = path.resolve(__dirname, "../..");
|
||||||
@@ -75,20 +76,29 @@ function patchTauriConfig(filePath) {
|
|||||||
const conf = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
const conf = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||||
const safeBinaryName = toSafeBinaryName(appNameEn, `App${jobId}`);
|
const safeBinaryName = toSafeBinaryName(appNameEn, `App${jobId}`);
|
||||||
conf.productName = appNameZh;
|
conf.productName = appNameZh;
|
||||||
conf.mainBinaryName = safeBinaryName;
|
if (filePath === confPath) {
|
||||||
|
conf.mainBinaryName = safeBinaryName;
|
||||||
|
}
|
||||||
conf.identifier = appIdentifier;
|
conf.identifier = appIdentifier;
|
||||||
conf.version = appVersion;
|
conf.version = appVersion;
|
||||||
if (conf.app?.windows?.[0]) {
|
if (conf.app?.windows?.[0]) {
|
||||||
conf.app.windows[0].title = appNameZh;
|
conf.app.windows[0].title = appNameZh;
|
||||||
}
|
}
|
||||||
|
conf.bundle = {
|
||||||
|
...conf.bundle,
|
||||||
|
active: true,
|
||||||
|
icon: BUNDLE_ICONS,
|
||||||
|
};
|
||||||
fs.writeFileSync(filePath, `${JSON.stringify(conf, null, 2)}\n`);
|
fs.writeFileSync(filePath, `${JSON.stringify(conf, null, 2)}\n`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
injectInAppNav(distDir);
|
||||||
|
|
||||||
patchTauriConfig(confPath);
|
patchTauriConfig(confPath);
|
||||||
if (fs.existsSync(androidConfPath)) {
|
if (fs.existsSync(androidConfPath)) {
|
||||||
patchTauriConfig(androidConfPath);
|
patchTauriConfig(androidConfPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
generateAppIcons();
|
generateAppIcons({ jobId });
|
||||||
|
|
||||||
console.log("Template prepared successfully");
|
console.log("Template prepared successfully");
|
||||||
|
|||||||
5
.github/workflows/build-app.yml
vendored
5
.github/workflows/build-app.yml
vendored
@@ -145,6 +145,11 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
NDK_HOME: ${{ env.ANDROID_HOME }}/ndk/${{ env.NDK_VERSION }}
|
NDK_HOME: ${{ env.ANDROID_HOME }}/ndk/${{ env.NDK_VERSION }}
|
||||||
|
|
||||||
|
- name: Regenerate app icons for Android
|
||||||
|
run: node .github/scripts/generate-app-icons.mjs
|
||||||
|
env:
|
||||||
|
JOB_ID: ${{ inputs.job_id }}
|
||||||
|
|
||||||
- name: Build Android APK
|
- name: Build Android APK
|
||||||
working-directory: template
|
working-directory: template
|
||||||
run: npm run tauri android build -- --apk --target aarch64 --ci
|
run: npm run tauri android build -- --apk --target aarch64 --ci
|
||||||
|
|||||||
4569
template/src-tauri/Cargo.lock
generated
Normal file
4569
template/src-tauri/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
{"default":{"identifier":"default","description":"Capability for the main window","local":true,"windows":["main"],"permissions":["core:default","core:window:allow-start-dragging","core:window:allow-show","core:window:allow-hide","core:window:allow-set-focus","core:window:allow-set-size","core:window:allow-close","allow-fetch-tickers","allow-coin-settings","allow-api-settings"]}}
|
{"default":{"identifier":"default","description":"Default capability for the main window","local":true,"windows":["main"],"permissions":["core:default"]}}
|
||||||
@@ -176,24 +176,6 @@
|
|||||||
"Identifier": {
|
"Identifier": {
|
||||||
"description": "Permission identifier",
|
"description": "Permission identifier",
|
||||||
"oneOf": [
|
"oneOf": [
|
||||||
{
|
|
||||||
"description": "Enables API config commands",
|
|
||||||
"type": "string",
|
|
||||||
"const": "allow-api-settings",
|
|
||||||
"markdownDescription": "Enables API config commands"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Enables coin list and refresh interval commands",
|
|
||||||
"type": "string",
|
|
||||||
"const": "allow-coin-settings",
|
|
||||||
"markdownDescription": "Enables coin list and refresh interval commands"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Enables the fetch_tickers command",
|
|
||||||
"type": "string",
|
|
||||||
"const": "allow-fetch-tickers",
|
|
||||||
"markdownDescription": "Enables the fetch_tickers command"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`",
|
"description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
@@ -176,24 +176,6 @@
|
|||||||
"Identifier": {
|
"Identifier": {
|
||||||
"description": "Permission identifier",
|
"description": "Permission identifier",
|
||||||
"oneOf": [
|
"oneOf": [
|
||||||
{
|
|
||||||
"description": "Enables API config commands",
|
|
||||||
"type": "string",
|
|
||||||
"const": "allow-api-settings",
|
|
||||||
"markdownDescription": "Enables API config commands"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Enables coin list and refresh interval commands",
|
|
||||||
"type": "string",
|
|
||||||
"const": "allow-coin-settings",
|
|
||||||
"markdownDescription": "Enables coin list and refresh interval commands"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Enables the fetch_tickers command",
|
|
||||||
"type": "string",
|
|
||||||
"const": "allow-fetch-tickers",
|
|
||||||
"markdownDescription": "Enables the fetch_tickers command"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`",
|
"description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
@@ -1,6 +1,82 @@
|
|||||||
|
use tauri::Manager;
|
||||||
|
|
||||||
|
const IN_APP_NAV_SCRIPT: &str = r##"
|
||||||
|
(function () {
|
||||||
|
if (window.__web2appNavInstalled) return;
|
||||||
|
window.__web2appNavInstalled = true;
|
||||||
|
|
||||||
|
function shouldHandleLink(anchor) {
|
||||||
|
if (!anchor || !anchor.href) return false;
|
||||||
|
const href = anchor.getAttribute("href");
|
||||||
|
if (!href || href.startsWith("#") || href.startsWith("javascript:")) return false;
|
||||||
|
try {
|
||||||
|
const url = new URL(anchor.href, window.location.href);
|
||||||
|
return url.protocol === "http:" || url.protocol === "https:";
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateInApp(url) {
|
||||||
|
window.location.assign(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener(
|
||||||
|
"click",
|
||||||
|
function (e) {
|
||||||
|
const anchor = e.target && e.target.closest ? e.target.closest("a") : null;
|
||||||
|
if (!shouldHandleLink(anchor)) return;
|
||||||
|
if (anchor.hasAttribute("download")) return;
|
||||||
|
if (
|
||||||
|
anchor.target === "_blank" ||
|
||||||
|
anchor.target === "_top" ||
|
||||||
|
location.protocol === "file:" ||
|
||||||
|
location.protocol === "tauri:" ||
|
||||||
|
location.hostname === "asset.localhost"
|
||||||
|
) {
|
||||||
|
e.preventDefault();
|
||||||
|
navigateInApp(anchor.href);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const target = new URL(anchor.href, window.location.href);
|
||||||
|
const current = new URL(window.location.href);
|
||||||
|
if (target.origin !== current.origin) {
|
||||||
|
e.preventDefault();
|
||||||
|
navigateInApp(anchor.href);
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
/* keep default */
|
||||||
|
}
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const originalOpen = window.open;
|
||||||
|
window.open = function (url, target, features) {
|
||||||
|
if (url && (!target || target === "_blank")) {
|
||||||
|
navigateInApp(url);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return originalOpen.call(window, url, target, features);
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
"##;
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
|
.setup(|app| {
|
||||||
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
|
let _ = window.eval(IN_APP_NAV_SCRIPT);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.on_page_load(|webview, payload| {
|
||||||
|
if payload.event() == tauri::webview::PageLoadEvent::Finished {
|
||||||
|
let _ = webview.eval(IN_APP_NAV_SCRIPT);
|
||||||
|
}
|
||||||
|
})
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user