feat: offline-package builtin icon + icon rounding; fix: async check-update, wallpaper cover scaling
This commit is contained in:
1 parent
885be2fe85
commit
ef59941953
6 files changed
+89
-21
No files matched your search
@@ -462,7 +462,7 @@ body {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.cell .ic { width: 48px; height: 48px; border-radius: 12px; }
|
||||
.cell .ic { width: 48px; height: 48px; border-radius: 12px; object-fit: cover; display: block; }
|
||||
.cell .lbl { font-size: 12px; color: var(--text-dim); text-align: center; }
|
||||
|
||||
/* H5 应用列表页:富卡片 + 在线/离线标签 */
|
||||
@@ -481,7 +481,7 @@ body {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.wcell .ic { width: 48px; height: 48px; border-radius: 12px; }
|
||||
.wcell .ic { width: 48px; height: 48px; border-radius: 12px; object-fit: cover; display: block; }
|
||||
.wcell .lbl { font-size: 12px; color: var(--text-dim); text-align: center; }
|
||||
|
||||
.wcell .tag {
|
||||
|
||||
@@ -226,24 +226,27 @@ function setupServerUrl(page) {
|
||||
};
|
||||
}
|
||||
|
||||
// 检查更新:触发重新拉取 webAPP 清单 + 卡片目录,完成后提示
|
||||
// Check update: re-fetch the webAPP manifest + card catalog, then show a hint
|
||||
// 检查更新:后台异步拉取,完成后通过 checkUpdateResult 事件回调更新提示
|
||||
// Check update: async fetch on a background thread; the result is delivered via
|
||||
// the checkUpdateResult event callback
|
||||
function setupCheckUpdate(page) {
|
||||
const item = page.querySelector('#check-update-item');
|
||||
const state = page.querySelector('#check-update-state');
|
||||
item.onclick = () => {
|
||||
state.textContent = '检查中…';
|
||||
bridge.call('refreshManifest').then(result => {
|
||||
let r = { added: 0, removed: 0, changed: 0 };
|
||||
try { r = JSON.parse(result); } catch (e) {}
|
||||
// 注册结果回调(原生 refreshManifest 完成后推送)
|
||||
// Register the result callback (pushed by native refreshManifest when done)
|
||||
window.HearthEvents = window.HearthEvents || {};
|
||||
window.HearthEvents.checkUpdateResult = function (json) {
|
||||
let r = json;
|
||||
try { r = typeof json === 'string' ? JSON.parse(json) : json; } catch (e) { r = {}; }
|
||||
const parts = [];
|
||||
if (r.added > 0) parts.push('新增 ' + r.added);
|
||||
if (r.removed > 0) parts.push('移除 ' + r.removed);
|
||||
if (r.changed > 0) parts.push('更新 ' + r.changed);
|
||||
state.textContent = parts.length ? parts.join(',') + ' ›' : '无更新 ›';
|
||||
}).catch(() => {
|
||||
state.textContent = '失败 ›';
|
||||
});
|
||||
};
|
||||
item.onclick = () => {
|
||||
state.textContent = '检查中…';
|
||||
bridge.call('refreshManifest'); // 异步,不 await 结果
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -149,9 +149,25 @@ class MainActivity : Activity() {
|
||||
if (wm.wallpaperInfo != null) return ""
|
||||
val d = wm.drawable ?: return ""
|
||||
val dm = resources.displayMetrics
|
||||
val bmp = Bitmap.createBitmap(dm.widthPixels, dm.heightPixels, Bitmap.Config.ARGB_8888)
|
||||
val sw = dm.widthPixels
|
||||
val sh = dm.heightPixels
|
||||
val bmp = Bitmap.createBitmap(sw, sh, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(bmp)
|
||||
d.setBounds(0, 0, dm.widthPixels, dm.heightPixels)
|
||||
// cover 缩放:保持宽高比铺满屏幕并居中裁剪,避免竖屏壁纸在横屏下
|
||||
// 被拉伸或底部留黑边
|
||||
// cover-scale: keep aspect ratio and center-crop to fill the screen,
|
||||
// avoiding stretch or a bottom black band from portrait wallpaper on
|
||||
// a landscape screen
|
||||
val dw = d.intrinsicWidth
|
||||
val dh = d.intrinsicHeight
|
||||
if (dw > 0 && dh > 0) {
|
||||
val scale = maxOf(sw.toFloat() / dw, sh.toFloat() / dh)
|
||||
val w = (dw * scale).toInt()
|
||||
val h = (dh * scale).toInt()
|
||||
d.setBounds((sw - w) / 2, (sh - h) / 2, (sw - w) / 2 + w, (sh - h) / 2 + h)
|
||||
} else {
|
||||
d.setBounds(0, 0, sw, sh)
|
||||
}
|
||||
d.draw(canvas)
|
||||
val out = ByteArrayOutputStream()
|
||||
bmp.compress(Bitmap.CompressFormat.JPEG, 80, out)
|
||||
|
||||
@@ -16,6 +16,7 @@ data class WebApp(
|
||||
val url: String,
|
||||
val offline: String? = null,
|
||||
val offlineVersion: String? = null,
|
||||
val offlineIcon: String? = null,
|
||||
val ua: String? = null,
|
||||
val scale: Int? = null,
|
||||
)
|
||||
@@ -108,6 +109,10 @@ class WebAppRepository(
|
||||
val offlineMap = map["offline"] as? Map<*, *>
|
||||
val offline = offlineMap?.get("package") as? String
|
||||
val offlineVersion = offlineMap?.get("version") as? String
|
||||
// 离线包内图标路径(相对离线包根,如 icon.svg),本地包有图标时优先显示
|
||||
// Icon path inside the offline package (relative to its root, e.g.
|
||||
// icon.svg); used in preference when the local package has the icon
|
||||
val offlineIcon = offlineMap?.get("icon") as? String
|
||||
// ua 可选:desktop/pc 用 PC UA,其他视为自定义 UA 字符串,不指定默认平板 UA
|
||||
// ua optional: desktop/pc uses a desktop UA, others are custom UA strings,
|
||||
// absent means the default tablet UA
|
||||
@@ -121,6 +126,7 @@ class WebAppRepository(
|
||||
resolvePath(url),
|
||||
offline?.let { resolvePath(it) },
|
||||
offlineVersion,
|
||||
offlineIcon,
|
||||
ua,
|
||||
scale,
|
||||
)
|
||||
|
||||
@@ -42,6 +42,24 @@ class WebAppStorage(private val context: Context) {
|
||||
return "file://" + path
|
||||
}
|
||||
|
||||
// 本地离线包内图标 URL(file:// 绝对路径),图标文件不存在则 null。
|
||||
// iconPath 是相对离线包根的路径(如 icon.svg),做路径穿越校验
|
||||
// Local offline-package icon URL (file:// absolute path), null when the icon
|
||||
// file doesn't exist. iconPath is relative to the package root (e.g. icon.svg),
|
||||
// with a path-traversal guard
|
||||
fun localIconUrl(id: String, iconPath: String): String? {
|
||||
val name = iconPath.replace('\\', '/').trimStart('/')
|
||||
if (name.contains("..") || name.isEmpty()) return null
|
||||
val f = File(appDir(id), name)
|
||||
if (!f.isFile) return null
|
||||
val path = try {
|
||||
f.canonicalPath
|
||||
} catch (e: Exception) {
|
||||
f.absolutePath
|
||||
}
|
||||
return "file://" + path
|
||||
}
|
||||
|
||||
// 本地离线包版本号(解压目录里的 .version 文件),无则 null
|
||||
// Local offline-package version (from the .version file), null when absent
|
||||
fun getLocalVersion(id: String): String? {
|
||||
|
||||
@@ -120,14 +120,27 @@ class JsBridge(
|
||||
fun launchApp(packageName: String): Boolean = appRepository?.launchApp(packageName) ?: false
|
||||
|
||||
// 返回 H5 应用清单 JSON(无仓库时返回空数组),并写入内存缓存供 openWebApp 复用
|
||||
// 本地包内置图标优先:离线包内有 offlineIcon 且本地已安装时,用本地 file:// 图标
|
||||
// Return the H5 web app manifest JSON (empty array when no repository wired) and
|
||||
// seed the in-memory cache for openWebApp reuse
|
||||
// seed the in-memory cache for openWebApp reuse. Local package icon wins: when the
|
||||
// offline package declares offlineIcon and is installed locally, use the local icon
|
||||
@android.webkit.JavascriptInterface
|
||||
fun fetchWebApps(): String {
|
||||
val apps = manifest()
|
||||
val apps = manifest().map { app ->
|
||||
resolveLocalIcon(app)?.let { app.copy(icon = it) } ?: app
|
||||
}
|
||||
return gson.toJson(apps)
|
||||
}
|
||||
|
||||
// 解析本地离线包图标:offlineIcon 指定且本地文件存在 → file:// URL,否则 null
|
||||
// Resolve the local offline-package icon: when offlineIcon is set and the local
|
||||
// file exists, return its file:// URL; otherwise null
|
||||
private fun resolveLocalIcon(app: WebApp): String? {
|
||||
val iconPath = app.offlineIcon ?: return null
|
||||
val st = storage ?: return null
|
||||
return st.localIconUrl(app.id, iconPath)
|
||||
}
|
||||
|
||||
// 打开指定 id 的 webAPP:本地离线包优先 → 有离线包则下载解压(带进度)→ 否则远程加载
|
||||
// Open a web app by id: prefer the local offline package -> download+extract (with
|
||||
// progress) if it has an offline package -> otherwise load the remote URL
|
||||
@@ -419,10 +432,14 @@ class JsBridge(
|
||||
@android.webkit.JavascriptInterface
|
||||
fun getWallpaper(): String = wallpaper?.getWallpaperBase64() ?: ""
|
||||
|
||||
// 重新拉取 webAPP 清单 + 卡片目录,返回 { added, removed, changed }
|
||||
// Re-fetch the webAPP manifest + card catalog, return { added, removed, changed }
|
||||
// 重新拉取 webAPP 清单 + 卡片目录(后台线程,避免阻塞 JS 线程导致卡顿),
|
||||
// 完成后通过 HearthEvents.checkUpdateResult 推送 { added, removed, changed }
|
||||
// Re-fetch the webAPP manifest + card catalog on a background thread (avoid
|
||||
// blocking the JS thread), then push { added, removed, changed } via
|
||||
// HearthEvents.checkUpdateResult
|
||||
@android.webkit.JavascriptInterface
|
||||
fun refreshManifest(): String {
|
||||
Thread {
|
||||
val oldApps = manifestCache ?: emptyList<WebApp>()
|
||||
manifestCache = null
|
||||
val apps = webAppRepository?.fetchManifest() ?: emptyList<WebApp>()
|
||||
@@ -442,15 +459,23 @@ class JsBridge(
|
||||
}
|
||||
Log.d("HearthBridge", "refreshManifest: added=$added removed=$removed changed=$changed (${apps.size} apps)")
|
||||
|
||||
// 推送 H5 刷新列表(清 preload 缓存并重渲染)
|
||||
// Push H5 to refresh the list (clear preload cache and re-render)
|
||||
val resultJson = gson.toJson(
|
||||
mapOf("added" to added, "removed" to removed, "changed" to changed)
|
||||
)
|
||||
// 推送结果 + 刷新列表(清 preload 缓存并重渲染)
|
||||
// Push the result + refresh the list (clear preload cache and re-render)
|
||||
desktopWebViewRef?.post {
|
||||
desktopWebViewRef?.evaluateJavascript(
|
||||
"window.HearthEvents && window.HearthEvents.checkUpdateResult($resultJson);",
|
||||
null
|
||||
)
|
||||
desktopWebViewRef?.evaluateJavascript(
|
||||
"window.HearthEvents && window.HearthEvents.webappUpdated();",
|
||||
null
|
||||
)
|
||||
}
|
||||
return gson.toJson(mapOf("added" to added, "removed" to removed, "changed" to changed))
|
||||
}.start()
|
||||
return "checking"
|
||||
}
|
||||
|
||||
// 注册媒体会话监听,回调推送给 H5(info 为 null 时推 "null")
|
||||
|
||||
Reference in new issue
Block a user