From 6915c54832191e4ac98683b5bec7a02bce3d0f51 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Sun, 16 Aug 2026 18:42:04 +0800 Subject: [PATCH] feat: glass effect toggle, media refresh timing fix, page transition perf --- app/src/main/assets/h5/css/app.css | 27 ++++++++++------- app/src/main/assets/h5/css/tokens.css | 12 ++++++++ app/src/main/assets/h5/index.html | 1 + app/src/main/assets/h5/js/glass.js | 24 +++++++++++++++ app/src/main/assets/h5/js/pages/settings.js | 17 +++++++++++ app/src/main/assets/h5/js/router.js | 15 +++++++--- .../main/java/top/yeij/hearth/MainActivity.kt | 4 +++ .../yeij/hearth/media/MediaSessionSource.kt | 13 +++++++++ .../java/top/yeij/hearth/webview/JsBridge.kt | 29 ++++++++++++++----- 9 files changed, 119 insertions(+), 23 deletions(-) create mode 100644 app/src/main/assets/h5/js/glass.js diff --git a/app/src/main/assets/h5/css/app.css b/app/src/main/assets/h5/css/app.css index 701e32f..fac6438 100644 --- a/app/src/main/assets/h5/css/app.css +++ b/app/src/main/assets/h5/css/app.css @@ -42,9 +42,10 @@ body { .rail { width: 80px; background: var(--glass-bg); - backdrop-filter: blur(20px) saturate(1.2) brightness(1.05) contrast(1.1); - -webkit-backdrop-filter: blur(20px) saturate(1.2) brightness(1.05) contrast(1.1); + backdrop-filter: var(--blur-filter); + -webkit-backdrop-filter: var(--blur-filter); border-right: 1px solid var(--glass-border); + box-shadow: var(--glass-shadow); display: flex; flex-direction: column; align-items: center; @@ -70,6 +71,8 @@ body { display: flex; flex-direction: column; align-items: center; + justify-content: center; + width: 100%; gap: 3px; background: none; border: none; @@ -87,7 +90,7 @@ body { justify-content: center; } .rail-item .ico svg { width: 100%; height: 100%; display: block; } -.rail-item .lbl { font-size: 11px; } +.rail-item .lbl { font-size: 11px; text-align: center; width: 100%; } .rail-item.active { background: var(--accent); color: #fff; } @@ -140,9 +143,10 @@ body { /* Card: soft-glass morphism (reuse Task 4 tokens, not a solid color) */ .card { background: var(--glass-bg); - backdrop-filter: blur(20px) saturate(1.2) brightness(1.05) contrast(1.1); - -webkit-backdrop-filter: blur(20px) saturate(1.2) brightness(1.05) contrast(1.1); + backdrop-filter: var(--blur-filter); + -webkit-backdrop-filter: var(--blur-filter); border: 1px solid var(--glass-border); + box-shadow: var(--glass-shadow); border-radius: 14px; padding: 20px; color: var(--text); @@ -298,8 +302,8 @@ body { padding: 6px 10px; border-radius: 999px; background: var(--glass-bg); - backdrop-filter: blur(20px) saturate(1.2); - -webkit-backdrop-filter: blur(20px) saturate(1.2); + backdrop-filter: var(--blur-filter); + -webkit-backdrop-filter: var(--blur-filter); border: 1px solid var(--glass-border); z-index: 100; } @@ -343,8 +347,8 @@ body { padding: 8px; border-radius: 16px; background: var(--glass-bg); - backdrop-filter: blur(20px) saturate(1.2); - -webkit-backdrop-filter: blur(20px) saturate(1.2); + backdrop-filter: var(--blur-filter); + -webkit-backdrop-filter: var(--blur-filter); border: 1px solid var(--glass-border); z-index: 100; } @@ -408,9 +412,10 @@ body { margin-bottom: 8px; border-radius: 14px; background: var(--glass-bg); - backdrop-filter: blur(20px) saturate(1.2) brightness(1.05) contrast(1.1); - -webkit-backdrop-filter: blur(20px) saturate(1.2) brightness(1.05) contrast(1.1); + backdrop-filter: var(--blur-filter); + -webkit-backdrop-filter: var(--blur-filter); border: 1px solid var(--glass-border); + box-shadow: var(--glass-shadow); color: var(--text); } diff --git a/app/src/main/assets/h5/css/tokens.css b/app/src/main/assets/h5/css/tokens.css index a22c151..6001c49 100644 --- a/app/src/main/assets/h5/css/tokens.css +++ b/app/src/main/assets/h5/css/tokens.css @@ -6,6 +6,18 @@ --text: #1a1a1a; --text-dim: #8a8a90; --accent: #ff6900; + /* 玻璃效果默认关闭:无模糊、无高光(普通半透明) */ + /* Glass effect off by default: no blur, no highlight (plain translucent) */ + --blur-filter: none; + --glass-shadow: none; +} + +/* 玻璃效果开启:磨砂模糊 + 边缘反射高光(液态玻璃,参考 Apple WWDC25) */ +/* Glass effect on: frosted blur + edge-reflection highlight (liquid glass) */ +:root[data-glass="on"] { + --blur-filter: blur(20px) saturate(1.2) brightness(1.05) contrast(1.1); + --glass-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.18), + inset 0 -1px 0 rgba(255, 255, 255, 0.04); } /* 系统深色:仅在未强制浅色时生效(跟随系统) */ diff --git a/app/src/main/assets/h5/index.html b/app/src/main/assets/h5/index.html index 8286d9e..e70c516 100644 --- a/app/src/main/assets/h5/index.html +++ b/app/src/main/assets/h5/index.html @@ -7,6 +7,7 @@ +
diff --git a/app/src/main/assets/h5/js/glass.js b/app/src/main/assets/h5/js/glass.js new file mode 100644 index 0000000..957e0c2 --- /dev/null +++ b/app/src/main/assets/h5/js/glass.js @@ -0,0 +1,24 @@ +// 玻璃效果开关:普通半透明(默认)/ 液态玻璃(backdrop-filter 磨砂 + 边缘高光) +// Glass effect toggle: plain translucent (default) / liquid glass (backdrop-filter +// frost + edge highlight), persisted in localStorage +(function () { + const KEY = 'hearth-glass'; + + window.glass = { + enabled() { + return localStorage.getItem(KEY) === '1'; + }, + setEnabled(on) { + localStorage.setItem(KEY, on ? '1' : '0'); + this.apply(); + }, + // 应用开关:写 data-glass 属性,CSS 据此切换 --blur-filter 等变量 + // Apply: set the data-glass attribute; CSS switches --blur-filter etc. from it + apply() { + document.documentElement.setAttribute('data-glass', this.enabled() ? 'on' : 'off'); + }, + }; + + // 启动即应用(默认关闭 = 普通半透明) + window.glass.apply(); +})(); diff --git a/app/src/main/assets/h5/js/pages/settings.js b/app/src/main/assets/h5/js/pages/settings.js index cc2e887..293a958 100644 --- a/app/src/main/assets/h5/js/pages/settings.js +++ b/app/src/main/assets/h5/js/pages/settings.js @@ -10,6 +10,10 @@ window.loadSettings = function () { 主题 跟随系统 +
+ 玻璃效果 + +
屏幕亮度 @@ -41,6 +45,7 @@ window.loadSettings = function () {
版本 0.1.0
`; setupTheme(page); + setupGlass(page); setupMask(page); setupNotificationAccess(page); setupBrightness(page); @@ -71,6 +76,18 @@ function setupTheme(page) { }; } +// 玻璃效果开关:普通半透明 ↔ 液态玻璃(backdrop-filter 磨砂 + 边缘高光) +// Glass effect toggle: plain translucent vs liquid glass +function setupGlass(page) { + const sw = page.querySelector('#glass-sw'); + const refresh = () => sw.classList.toggle('on', glass.enabled()); + refresh(); + page.querySelector('#glass-item').onclick = () => { + glass.setEnabled(!glass.enabled()); + refresh(); + }; +} + // 背景遮罩:开关 + 明暗度(仅夜间主题显示) // Wallpaper dim: toggle + opacity (shown only under dark theme) function setupMask(page) { diff --git a/app/src/main/assets/h5/js/router.js b/app/src/main/assets/h5/js/router.js index 0458ad7..6d26fa2 100644 --- a/app/src/main/assets/h5/js/router.js +++ b/app/src/main/assets/h5/js/router.js @@ -29,9 +29,16 @@ const router = { // always occupies space; only toggle visibility to avoid shifting icons) const clock = document.getElementById('rail-clock'); if (clock) clock.style.visibility = (id === 'home') ? 'hidden' : 'visible'; - if (id === 'home') window.updateHomeCards && window.updateHomeCards(); - if (id === 'applist') window.loadAppList && window.loadAppList(); - if (id === 'webapplist') window.loadWebAppList && window.loadWebAppList(); - if (id === 'settings') window.loadSettings && window.loadSettings(); + // 延迟内容加载到下一帧:让切换动画先流畅播放,避免加载重活(查应用/拉清单) + // 在动画帧内阻塞导致卡顿 + // Defer content loading to the next frame: let the switch animation play + // smoothly first, avoiding jank from heavy loads (app query / manifest fetch) + // blocking within the animation frame + requestAnimationFrame(() => { + if (id === 'home') window.updateHomeCards && window.updateHomeCards(); + if (id === 'applist') window.loadAppList && window.loadAppList(); + if (id === 'webapplist') window.loadWebAppList && window.loadWebAppList(); + if (id === 'settings') window.loadSettings && window.loadSettings(); + }); } }; diff --git a/app/src/main/java/top/yeij/hearth/MainActivity.kt b/app/src/main/java/top/yeij/hearth/MainActivity.kt index bf27ca9..33b55a5 100644 --- a/app/src/main/java/top/yeij/hearth/MainActivity.kt +++ b/app/src/main/java/top/yeij/hearth/MainActivity.kt @@ -164,6 +164,10 @@ class MainActivity : Activity() { // Log when the main frame finishes loading, to help diagnose a black screen override fun onPageFinished(view: WebView?, url: String?) { Log.d(TAG, "onPageFinished: url=$url") + // H5 就绪后重新拉取一次媒体,修复启动时推送早于页面渲染的时序问题 + // Re-pull media once H5 is ready: fixes the startup timing where the + // media push happens before the page has rendered + bridge.refreshMedia() } // 主帧加载失败时展示错误页,避免白屏 diff --git a/app/src/main/java/top/yeij/hearth/media/MediaSessionSource.kt b/app/src/main/java/top/yeij/hearth/media/MediaSessionSource.kt index 88a0d45..d7b242c 100644 --- a/app/src/main/java/top/yeij/hearth/media/MediaSessionSource.kt +++ b/app/src/main/java/top/yeij/hearth/media/MediaSessionSource.kt @@ -68,6 +68,19 @@ class MediaSessionSource(context: Context) { callback = null } + // 主动拉取当前会话并回调(不重新注册监听),供 H5 页面就绪后刷新媒体卡 + // Actively pull the current session and notify (without re-registering), for + // refreshing the media card once the H5 page is ready + fun refresh() { + val cb = callback ?: return + try { + val current = msm.getActiveSessions(listenerComponent).firstOrNull() + notifyMedia(current, cb) + } catch (e: SecurityException) { + Log.w(TAG, "refresh: no notification access: ${e.message}") + } + } + // 从 controller 提取 metadata + playbackState 构造 MediaInfo 并回调 // Build MediaInfo from the controller's metadata + playbackState and invoke the callback private fun notifyMedia(controller: MediaController?, cb: (MediaInfo?) -> Unit) { 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 1675c0f..4bfbd6f 100644 --- a/app/src/main/java/top/yeij/hearth/webview/JsBridge.kt +++ b/app/src/main/java/top/yeij/hearth/webview/JsBridge.kt @@ -4,6 +4,7 @@ import android.util.Log import top.yeij.hearth.app.AppRepository import top.yeij.hearth.card.Card import top.yeij.hearth.card.CardRepository +import top.yeij.hearth.media.MediaInfo import top.yeij.hearth.media.MediaSessionSource import top.yeij.hearth.webapp.Tab import top.yeij.hearth.webapp.WebApp @@ -283,15 +284,27 @@ class JsBridge( // 注册媒体会话监听,回调推送给 H5(info 为 null 时推 "null") // Register media session listener; push callbacks to H5 (push "null" when info is null) + private var mediaSourceRef: MediaSessionSource? = null + fun setMediaListener(source: MediaSessionSource, webView: android.webkit.WebView) { - source.start { info -> - webView.post { - val json = info?.toJson() ?: "null" - webView.evaluateJavascript( - "window.HearthEvents && window.HearthEvents.mediaSessionChanged($json);", - null - ) - } + mediaSourceRef = source + source.start { info -> pushMedia(info, webView) } + } + + // H5 页面就绪后重新拉取一次媒体:修复启动时推送早于页面渲染的时序问题 + // Re-pull media once the H5 page is ready: fixes the startup timing where the + // push happens before the page has rendered + fun refreshMedia() { + mediaSourceRef?.refresh() + } + + private fun pushMedia(info: MediaInfo?, webView: android.webkit.WebView) { + webView.post { + val json = info?.toJson() ?: "null" + webView.evaluateJavascript( + "window.HearthEvents && window.HearthEvents.mediaSessionChanged($json);", + null + ) } } }