From 9fc3272923a8027e50e8577307c14b361f73e5d4 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Sun, 16 Aug 2026 21:41:05 +0800 Subject: [PATCH] feat: native tab panel overlay, webapp height no longer reserved --- app/src/main/assets/h5/js/webapp/topbar.js | 6 +- .../main/java/top/yeij/hearth/MainActivity.kt | 79 +++++++++++++++++-- .../java/top/yeij/hearth/webview/JsBridge.kt | 13 +++ 3 files changed, 90 insertions(+), 8 deletions(-) diff --git a/app/src/main/assets/h5/js/webapp/topbar.js b/app/src/main/assets/h5/js/webapp/topbar.js index ed7181d..e8d786f 100644 --- a/app/src/main/assets/h5/js/webapp/topbar.js +++ b/app/src/main/assets/h5/js/webapp/topbar.js @@ -28,9 +28,9 @@ window.showTopbar = function (tabs) { fwd.onclick = () => bridge.call('webGoForward'); reload.onclick = () => bridge.call('webReload'); } - // 每次刷新都重绑,避免捕获旧 tabs(陈旧闭包) - // Rebind on every refresh to avoid capturing stale tabs (stale closure) - document.getElementById('tb-tabs').onclick = () => showTabPanel(tabs); + // 标签按钮:调原生弹出标签面板(PopupWindow 在内容 WebView 之上) + // Tab button: show the native tab panel (PopupWindow above the content WebView) + document.getElementById('tb-tabs').onclick = () => bridge.call('showTabPanel'); document.getElementById('tb-tabs').textContent = `标签 (${tabs.length})`; const active = tabs.find(t => t.active); document.getElementById('tb-title').textContent = active ? active.name : ''; diff --git a/app/src/main/java/top/yeij/hearth/MainActivity.kt b/app/src/main/java/top/yeij/hearth/MainActivity.kt index ca46ef2..dc74078 100644 --- a/app/src/main/java/top/yeij/hearth/MainActivity.kt +++ b/app/src/main/java/top/yeij/hearth/MainActivity.kt @@ -6,12 +6,15 @@ import android.content.Intent import android.content.res.Configuration import android.graphics.Bitmap import android.graphics.Canvas +import android.graphics.Color +import android.graphics.drawable.GradientDrawable import android.net.Uri import android.os.Bundle import android.provider.Settings import android.text.TextUtils import android.util.Base64 import android.util.Log +import android.view.Gravity import android.view.View import android.view.ViewGroup import android.view.WindowInsets @@ -19,6 +22,9 @@ import android.view.WindowInsetsController import android.webkit.WebView import android.webkit.WebViewClient import android.widget.FrameLayout +import android.widget.LinearLayout +import android.widget.PopupWindow +import android.widget.TextView import androidx.core.app.NotificationManagerCompat import com.google.gson.Gson import top.yeij.hearth.app.AndroidAppSource @@ -51,11 +57,12 @@ class MainActivity : Activity() { // Placeholder manifest/catalog URLs; replace with the real server on device private const val MANIFEST_URL = "https://example.com/hearth/manifest.json" private const val CATALOG_URL = "https://example.com/hearth/catalog.json" - // 内容 WebView 顶部预留高度(dp):顶栏(约 54dp)+ 横向标签面板(约 45dp), - // 取 104dp 留余量,确保标签面板完全不被内容 WebView 覆盖 - // Content WebView top reserve (dp): topbar (~54dp) + horizontal tab panel - // (~45dp); use 104dp with margin so the tab panel is never covered - private const val TOPBAR_HEIGHT_DP = 104 + // 内容 WebView 顶部预留高度(dp):默认只留顶栏(约 54dp);标签面板弹出时 + // 通过 setContentTopMargin 临时增大,收起后恢复,避免一直压缩 webapp 高度 + // Content WebView top reserve (dp): default only the topbar (~54dp); the tab + // panel temporarily grows it via setContentTopMargin and restores on collapse, + // avoiding permanently shrinking the webapp height + private const val TOPBAR_HEIGHT_DP = 56 // 全局侧边栏收起宽度(dp),内容 WebView 左侧预留,避免挡住侧边栏 // Global sidebar collapsed width (dp); content WebViews reserve it on the // left to avoid covering the sidebar @@ -197,6 +204,7 @@ class MainActivity : Activity() { brightness = brightnessProvider, serverUrl = serverUrlProvider, wallpaper = wallpaperProvider, + onShowTabPanel = { showNativeTabPanel() }, ) root = FrameLayout(this) setContentView(root) @@ -347,6 +355,67 @@ class MainActivity : Activity() { } } + // 弹出原生标签面板(PopupWindow,在内容 WebView 之上,不占用预留高度) + // Show the native tab panel (PopupWindow above the content WebView, no reserved height) + private fun showNativeTabPanel() { + val tabs = webAppContainer.tabs() + if (tabs.isEmpty()) return + + val container = LinearLayout(this).apply { + orientation = LinearLayout.VERTICAL + setPadding(dp(8), dp(8), dp(8), dp(8)) + background = GradientDrawable().apply { + setColor(0xE6202024.toInt()) + cornerRadius = dp(16).toFloat() + } + } + + val popup = PopupWindow(container, dp(220), ViewGroup.LayoutParams.WRAP_CONTENT, true) + popup.isOutsideTouchable = true + tabs.forEach { tab -> + val row = LinearLayout(this).apply { + orientation = LinearLayout.HORIZONTAL + gravity = Gravity.CENTER_VERTICAL + setPadding(dp(12), dp(8), dp(12), dp(8)) + if (tab.active) background = GradientDrawable().apply { + setColor(Color.parseColor("#ff6900")) + cornerRadius = dp(10).toFloat() + } + } + val name = TextView(this).apply { + text = tab.name + textSize = 14f + setTextColor(Color.WHITE) + isSingleLine = true + ellipsize = TextUtils.TruncateAt.END + layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f) + } + val close = TextView(this).apply { + text = "✕" + textSize = 14f + setTextColor(Color.WHITE) + setPadding(dp(10), 0, 0, 0) + setOnClickListener { + bridge.closeWebApp(tab.id) + popup.dismiss() + } + } + row.addView(name) + row.addView(close) + row.setOnClickListener { + bridge.switchTab(tab.id) + popup.dismiss() + } + container.addView(row) + } + + popup.showAtLocation(root, Gravity.TOP or Gravity.END, dp(16), dp(64)) + } + + // dp 转 px + // dp to px + private fun dp(v: Int): Int = (v * resources.displayMetrics.density).toInt() + // 检测通知使用权是否已授予,未授予时打日志提示(UI 引导入口留后续) // Check whether notification access is granted; log a hint when not // (the settings UI entry is deferred to a later task) diff --git a/app/src/main/java/top/yeij/hearth/webview/JsBridge.kt b/app/src/main/java/top/yeij/hearth/webview/JsBridge.kt index ede4f1c..8a60194 100644 --- a/app/src/main/java/top/yeij/hearth/webview/JsBridge.kt +++ b/app/src/main/java/top/yeij/hearth/webview/JsBridge.kt @@ -69,6 +69,11 @@ class JsBridge( // 壁纸提供接口(base64) // Wallpaper provider (base64) private val wallpaper: WallpaperProvider? = null, + // 标签面板回调:由 MainActivity 实现,弹出原生标签面板(PopupWindow,在内容 + // WebView 之上,不占用预留高度) + // Tab panel callback implemented by MainActivity: shows a native tab panel + // (PopupWindow above the content WebView, no reserved height) + private val onShowTabPanel: (() -> Unit)? = null, ) { private val gson = com.google.gson.Gson() @@ -212,6 +217,14 @@ class JsBridge( Log.d("HearthBridge", "hideWebApps called") } + // 弹出原生标签面板(H5 顶栏标签按钮点击时调用) + // Show the native tab panel (called when the H5 topbar tab button is tapped) + @android.webkit.JavascriptInterface + fun showTabPanel() { + Log.d("HearthBridge", "showTabPanel called") + onShowTabPanel?.invoke() + } + // 清单内存缓存:首次拉取成功后缓存,后续复用(避免 openWebApp 重复 I/O) // 空结果(离线失败)不缓存,下次调用重试拉取,避免空清单被永久缓存 // In-memory manifest cache: cache only on a successful non-empty fetch (avoid