feat: glass effect toggle, media refresh timing fix, page transition perf

This commit is contained in:
2026-08-16 18:42:04 +08:00
parent a65205b8d2
commit 6915c54832
9 changed files with 119 additions and 23 deletions
+16 -11
View File
@@ -42,9 +42,10 @@ body {
.rail { .rail {
width: 80px; width: 80px;
background: var(--glass-bg); background: var(--glass-bg);
backdrop-filter: blur(20px) saturate(1.2) brightness(1.05) contrast(1.1); backdrop-filter: var(--blur-filter);
-webkit-backdrop-filter: blur(20px) saturate(1.2) brightness(1.05) contrast(1.1); -webkit-backdrop-filter: var(--blur-filter);
border-right: 1px solid var(--glass-border); border-right: 1px solid var(--glass-border);
box-shadow: var(--glass-shadow);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
@@ -70,6 +71,8 @@ body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center;
width: 100%;
gap: 3px; gap: 3px;
background: none; background: none;
border: none; border: none;
@@ -87,7 +90,7 @@ body {
justify-content: center; justify-content: center;
} }
.rail-item .ico svg { width: 100%; height: 100%; display: block; } .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; } .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: soft-glass morphism (reuse Task 4 tokens, not a solid color) */
.card { .card {
background: var(--glass-bg); background: var(--glass-bg);
backdrop-filter: blur(20px) saturate(1.2) brightness(1.05) contrast(1.1); backdrop-filter: var(--blur-filter);
-webkit-backdrop-filter: blur(20px) saturate(1.2) brightness(1.05) contrast(1.1); -webkit-backdrop-filter: var(--blur-filter);
border: 1px solid var(--glass-border); border: 1px solid var(--glass-border);
box-shadow: var(--glass-shadow);
border-radius: 14px; border-radius: 14px;
padding: 20px; padding: 20px;
color: var(--text); color: var(--text);
@@ -298,8 +302,8 @@ body {
padding: 6px 10px; padding: 6px 10px;
border-radius: 999px; border-radius: 999px;
background: var(--glass-bg); background: var(--glass-bg);
backdrop-filter: blur(20px) saturate(1.2); backdrop-filter: var(--blur-filter);
-webkit-backdrop-filter: blur(20px) saturate(1.2); -webkit-backdrop-filter: var(--blur-filter);
border: 1px solid var(--glass-border); border: 1px solid var(--glass-border);
z-index: 100; z-index: 100;
} }
@@ -343,8 +347,8 @@ body {
padding: 8px; padding: 8px;
border-radius: 16px; border-radius: 16px;
background: var(--glass-bg); background: var(--glass-bg);
backdrop-filter: blur(20px) saturate(1.2); backdrop-filter: var(--blur-filter);
-webkit-backdrop-filter: blur(20px) saturate(1.2); -webkit-backdrop-filter: var(--blur-filter);
border: 1px solid var(--glass-border); border: 1px solid var(--glass-border);
z-index: 100; z-index: 100;
} }
@@ -408,9 +412,10 @@ body {
margin-bottom: 8px; margin-bottom: 8px;
border-radius: 14px; border-radius: 14px;
background: var(--glass-bg); background: var(--glass-bg);
backdrop-filter: blur(20px) saturate(1.2) brightness(1.05) contrast(1.1); backdrop-filter: var(--blur-filter);
-webkit-backdrop-filter: blur(20px) saturate(1.2) brightness(1.05) contrast(1.1); -webkit-backdrop-filter: var(--blur-filter);
border: 1px solid var(--glass-border); border: 1px solid var(--glass-border);
box-shadow: var(--glass-shadow);
color: var(--text); color: var(--text);
} }
+12
View File
@@ -6,6 +6,18 @@
--text: #1a1a1a; --text: #1a1a1a;
--text-dim: #8a8a90; --text-dim: #8a8a90;
--accent: #ff6900; --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);
} }
/* 系统深色:仅在未强制浅色时生效(跟随系统) */ /* 系统深色:仅在未强制浅色时生效(跟随系统) */
+1
View File
@@ -7,6 +7,7 @@
<link rel="stylesheet" href="css/tokens.css"> <link rel="stylesheet" href="css/tokens.css">
<link rel="stylesheet" href="css/app.css"> <link rel="stylesheet" href="css/app.css">
<script src="js/theme.js"></script> <script src="js/theme.js"></script>
<script src="js/glass.js"></script>
</head> </head>
<body> <body>
<div id="wallpaper-dim"></div> <div id="wallpaper-dim"></div>
+24
View File
@@ -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();
})();
@@ -10,6 +10,10 @@ window.loadSettings = function () {
<span class="lbl">主题</span> <span class="lbl">主题</span>
<span class="arrow" id="theme-state">跟随系统</span> <span class="arrow" id="theme-state">跟随系统</span>
</div> </div>
<div class="st-item" id="glass-item">
<span class="lbl">玻璃效果</span>
<span class="sw" id="glass-sw"><i></i></span>
</div>
<div class="st-item" id="brightness-item"> <div class="st-item" id="brightness-item">
<span class="lbl">屏幕亮度</span> <span class="lbl">屏幕亮度</span>
<span class="arrow" id="brightness-state"></span> <span class="arrow" id="brightness-state"></span>
@@ -41,6 +45,7 @@ window.loadSettings = function () {
<div class="st-item"><span class="lbl">版本 0.1.0</span><span class="arrow"></span></div> <div class="st-item"><span class="lbl">版本 0.1.0</span><span class="arrow"></span></div>
</div>`; </div>`;
setupTheme(page); setupTheme(page);
setupGlass(page);
setupMask(page); setupMask(page);
setupNotificationAccess(page); setupNotificationAccess(page);
setupBrightness(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) // Wallpaper dim: toggle + opacity (shown only under dark theme)
function setupMask(page) { function setupMask(page) {
+7
View File
@@ -29,9 +29,16 @@ const router = {
// always occupies space; only toggle visibility to avoid shifting icons) // always occupies space; only toggle visibility to avoid shifting icons)
const clock = document.getElementById('rail-clock'); const clock = document.getElementById('rail-clock');
if (clock) clock.style.visibility = (id === 'home') ? 'hidden' : 'visible'; if (clock) clock.style.visibility = (id === 'home') ? 'hidden' : 'visible';
// 延迟内容加载到下一帧:让切换动画先流畅播放,避免加载重活(查应用/拉清单)
// 在动画帧内阻塞导致卡顿
// 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 === 'home') window.updateHomeCards && window.updateHomeCards();
if (id === 'applist') window.loadAppList && window.loadAppList(); if (id === 'applist') window.loadAppList && window.loadAppList();
if (id === 'webapplist') window.loadWebAppList && window.loadWebAppList(); if (id === 'webapplist') window.loadWebAppList && window.loadWebAppList();
if (id === 'settings') window.loadSettings && window.loadSettings(); if (id === 'settings') window.loadSettings && window.loadSettings();
});
} }
}; };
@@ -164,6 +164,10 @@ class MainActivity : Activity() {
// Log when the main frame finishes loading, to help diagnose a black screen // Log when the main frame finishes loading, to help diagnose a black screen
override fun onPageFinished(view: WebView?, url: String?) { override fun onPageFinished(view: WebView?, url: String?) {
Log.d(TAG, "onPageFinished: url=$url") 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()
} }
// 主帧加载失败时展示错误页,避免白屏 // 主帧加载失败时展示错误页,避免白屏
@@ -68,6 +68,19 @@ class MediaSessionSource(context: Context) {
callback = null 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 并回调 // 从 controller 提取 metadata + playbackState 构造 MediaInfo 并回调
// Build MediaInfo from the controller's metadata + playbackState and invoke the callback // Build MediaInfo from the controller's metadata + playbackState and invoke the callback
private fun notifyMedia(controller: MediaController?, cb: (MediaInfo?) -> Unit) { private fun notifyMedia(controller: MediaController?, cb: (MediaInfo?) -> Unit) {
@@ -4,6 +4,7 @@ import android.util.Log
import top.yeij.hearth.app.AppRepository import top.yeij.hearth.app.AppRepository
import top.yeij.hearth.card.Card import top.yeij.hearth.card.Card
import top.yeij.hearth.card.CardRepository import top.yeij.hearth.card.CardRepository
import top.yeij.hearth.media.MediaInfo
import top.yeij.hearth.media.MediaSessionSource import top.yeij.hearth.media.MediaSessionSource
import top.yeij.hearth.webapp.Tab import top.yeij.hearth.webapp.Tab
import top.yeij.hearth.webapp.WebApp import top.yeij.hearth.webapp.WebApp
@@ -283,8 +284,21 @@ class JsBridge(
// 注册媒体会话监听,回调推送给 H5(info 为 null 时推 "null" // 注册媒体会话监听,回调推送给 H5(info 为 null 时推 "null"
// Register media session listener; push callbacks to H5 (push "null" when info is 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) { fun setMediaListener(source: MediaSessionSource, webView: android.webkit.WebView) {
source.start { info -> 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 { webView.post {
val json = info?.toJson() ?: "null" val json = info?.toJson() ?: "null"
webView.evaluateJavascript( webView.evaluateJavascript(
@@ -293,5 +307,4 @@ class JsBridge(
) )
} }
} }
}
} }