feat: offline package download/import, load progress, local-first open
This commit is contained in:
@@ -10,6 +10,7 @@ import top.yeij.hearth.webapp.Tab
|
||||
import top.yeij.hearth.webapp.WebApp
|
||||
import top.yeij.hearth.webapp.WebAppContainer
|
||||
import top.yeij.hearth.webapp.WebAppRepository
|
||||
import top.yeij.hearth.webapp.WebAppStorage
|
||||
|
||||
// 通知使用权访问接口:由 MainActivity 实现,JsBridge 通过它检查/申请授权,
|
||||
// 保持 JsBridge 可 JVM 单测(不直接依赖 Context/NotificationManagerCompat)
|
||||
@@ -74,9 +75,19 @@ class JsBridge(
|
||||
// Tab panel callback implemented by MainActivity: shows a native tab panel
|
||||
// (PopupWindow above the content WebView, no reserved height)
|
||||
private val onShowTabPanel: (() -> Unit)? = null,
|
||||
// 离线包存储(下载/解压/导入/本地检查)
|
||||
// Offline-package storage (download/extract/import/local check)
|
||||
private val storage: WebAppStorage? = null,
|
||||
// 手动导入离线包回调:由 MainActivity 实现(SAF 选择 zip 后导入)
|
||||
// Manual offline-package import callback implemented by MainActivity (SAF pick + import)
|
||||
private val onImportOfflinePackage: ((String) -> Unit)? = null,
|
||||
) {
|
||||
private val gson = com.google.gson.Gson()
|
||||
|
||||
// 桌面 WebView 引用(媒体/进度推送用),setMediaListener 时保存
|
||||
// Desktop WebView reference (for media/progress push), saved in setMediaListener
|
||||
private var desktopWebViewRef: android.webkit.WebView? = null
|
||||
|
||||
// 清单内存缓存:openWebApp 首次拉取后缓存,后续复用避免重复 I/O
|
||||
// In-memory manifest cache: cached after first fetch to avoid repeated I/O
|
||||
@Volatile
|
||||
@@ -114,9 +125,9 @@ class JsBridge(
|
||||
return gson.toJson(apps)
|
||||
}
|
||||
|
||||
// 打开指定 id 的 webAPP:从清单缓存找 url/name → 记录标签 → 主线程创建内容 WebView → 同步顶栏
|
||||
// Open a web app by id: look up url/name from the cached manifest -> record the tab
|
||||
// -> create content WebView on main thread -> sync the topbar
|
||||
// 打开指定 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
|
||||
@android.webkit.JavascriptInterface
|
||||
fun openWebApp(id: String) {
|
||||
val container = webAppContainer ?: return
|
||||
@@ -126,14 +137,72 @@ class JsBridge(
|
||||
Log.d("HearthBridge", "openWebApp: unknown id=$id")
|
||||
return
|
||||
}
|
||||
// 状态变更与 View 变更统一在主线程串行,避免与 onBackPressed/listTabs 并发读写
|
||||
// Mutate tab state and view on the main thread together, serializing access
|
||||
val st = storage
|
||||
// 1. 本地离线包优先(服务端删除后仍可用)
|
||||
val localUrl = st?.localIndexUrl(id)
|
||||
if (localUrl != null) {
|
||||
openWebAppAt(container, host, id, localUrl, app.name)
|
||||
return
|
||||
}
|
||||
// 2. 有离线包 → 后台下载解压(推送进度),完成后加载本地;失败回退远程
|
||||
val pkg = app.offline
|
||||
if (pkg != null && st != null) {
|
||||
pushWebappProgress(id, 0)
|
||||
Thread {
|
||||
val fullUrl = resolveUrl(pkg)
|
||||
st.downloadAndExtract(id, fullUrl) { p -> pushWebappProgress(id, p) }
|
||||
val url = st.localIndexUrl(id) ?: app.url
|
||||
openWebAppAt(container, host, id, url, app.name)
|
||||
pushWebappProgress(id, 100)
|
||||
}.start()
|
||||
return
|
||||
}
|
||||
// 3. 无离线包 → 远程加载(WebView 自带加载进度)
|
||||
openWebAppAt(container, host, id, app.url, app.name)
|
||||
}
|
||||
|
||||
// 在主线程打开指定 url 的 webAPP 标签
|
||||
// Open a webapp tab at the given url on the main thread
|
||||
private fun openWebAppAt(container: WebAppContainer, host: WebAppHost, id: String, url: String, name: String) {
|
||||
onMain {
|
||||
container.open(id, app.url, app.name)
|
||||
host.openWebView(id, app.url)
|
||||
container.open(id, url, name)
|
||||
host.openWebView(id, url)
|
||||
host.syncTabs(container.tabs())
|
||||
}
|
||||
Log.d("HearthBridge", "openWebApp: id=$id name=${app.name} url=${app.url}")
|
||||
Log.d("HearthBridge", "openWebApp: id=$id url=$url")
|
||||
}
|
||||
|
||||
// 手动导入离线包(由 MainActivity 触发 SAF 选择 zip)
|
||||
// Manually import an offline package (MainActivity triggers SAF to pick a zip)
|
||||
@android.webkit.JavascriptInterface
|
||||
fun importOfflinePackage(id: String) {
|
||||
Log.d("HearthBridge", "importOfflinePackage: id=$id")
|
||||
onImportOfflinePackage?.invoke(id)
|
||||
}
|
||||
|
||||
// 本地是否已有离线包(用于列表显示「离线」标签)
|
||||
// Whether a local offline package exists (for the list's "offline" badge)
|
||||
@android.webkit.JavascriptInterface
|
||||
fun hasLocalPackage(id: String): Boolean = storage?.hasLocalPackage(id) ?: false
|
||||
|
||||
// 推送 webapp 加载/下载进度给 H5(0-100)
|
||||
// Push webapp load/download progress to H5 (0-100)
|
||||
private fun pushWebappProgress(id: String, progress: Int) {
|
||||
val wv = desktopWebViewRef ?: return
|
||||
wv.post {
|
||||
wv.evaluateJavascript(
|
||||
"window.HearthEvents && window.HearthEvents.webappProgress('$id', $progress);",
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 离线包路径解析:绝对 URL 直接用,相对路径拼服务器根
|
||||
// Resolve the offline-package path: absolute URL as-is, relative joined to the server root
|
||||
private fun resolveUrl(relative: String): String {
|
||||
if (relative.startsWith("http://") || relative.startsWith("https://")) return relative
|
||||
val base = serverUrl?.getServerUrl()?.substringBeforeLast('/') ?: return relative
|
||||
return "$base/$relative"
|
||||
}
|
||||
|
||||
// 关闭指定 id 的标签:移除标签 → 主线程销毁 WebView → 若剩标签激活第一个 → 同步顶栏
|
||||
@@ -327,6 +396,7 @@ class JsBridge(
|
||||
|
||||
fun setMediaListener(source: MediaSessionSource, webView: android.webkit.WebView) {
|
||||
mediaSourceRef = source
|
||||
desktopWebViewRef = webView
|
||||
source.start { infos -> pushMedia(infos, webView) }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user