fix: Android app icon after init and Windows in-app link navigation

This commit is contained in:
2026-05-29 15:28:20 +08:00
parent ef0e406ec8
commit fa083c5ddc
10 changed files with 4724 additions and 45 deletions

View File

@@ -10,7 +10,17 @@ const templateDir = path.join(root, "template");
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) {
if (!fs.existsSync(baseDir)) return null;
for (const name of ICON_PRIORITY) {
const direct = path.join(baseDir, name);
if (fs.existsSync(direct)) return direct;
@@ -27,16 +37,26 @@ function findIconSource(baseDir) {
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)) {
console.log("No dist directory, keeping default icons");
return;
return false;
}
const iconSource = findIconSource(distDir);
const iconSource = findIconSourceForJob(root, jobId, distDir);
if (!iconSource) {
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)}`);
@@ -44,4 +64,15 @@ export function generateAppIcons() {
cwd: templateDir,
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
View 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");
}

View File

@@ -2,7 +2,8 @@ import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
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 root = path.resolve(__dirname, "../..");
@@ -75,20 +76,29 @@ function patchTauriConfig(filePath) {
const conf = JSON.parse(fs.readFileSync(filePath, "utf8"));
const safeBinaryName = toSafeBinaryName(appNameEn, `App${jobId}`);
conf.productName = appNameZh;
conf.mainBinaryName = safeBinaryName;
if (filePath === confPath) {
conf.mainBinaryName = safeBinaryName;
}
conf.identifier = appIdentifier;
conf.version = appVersion;
if (conf.app?.windows?.[0]) {
conf.app.windows[0].title = appNameZh;
}
conf.bundle = {
...conf.bundle,
active: true,
icon: BUNDLE_ICONS,
};
fs.writeFileSync(filePath, `${JSON.stringify(conf, null, 2)}\n`);
}
injectInAppNav(distDir);
patchTauriConfig(confPath);
if (fs.existsSync(androidConfPath)) {
patchTauriConfig(androidConfPath);
}
generateAppIcons();
generateAppIcons({ jobId });
console.log("Template prepared successfully");

View File

@@ -145,6 +145,11 @@ jobs:
env:
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
working-directory: template
run: npm run tauri android build -- --apk --target aarch64 --ci

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

View File

@@ -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"]}}

View File

@@ -176,24 +176,6 @@
"Identifier": {
"description": "Permission identifier",
"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`",
"type": "string",

View File

@@ -176,24 +176,6 @@
"Identifier": {
"description": "Permission identifier",
"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`",
"type": "string",

View File

@@ -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)]
pub fn run() {
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!())
.expect("error while running tauri application");
}