feat: glass effect toggle, media refresh timing fix, page transition perf
This commit is contained in:
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 系统深色:仅在未强制浅色时生效(跟随系统) */
|
/* 系统深色:仅在未强制浅色时生效(跟随系统) */
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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';
|
||||||
if (id === 'home') window.updateHomeCards && window.updateHomeCards();
|
// 延迟内容加载到下一帧:让切换动画先流畅播放,避免加载重活(查应用/拉清单)
|
||||||
if (id === 'applist') window.loadAppList && window.loadAppList();
|
// 在动画帧内阻塞导致卡顿
|
||||||
if (id === 'webapplist') window.loadWebAppList && window.loadWebAppList();
|
// Defer content loading to the next frame: let the switch animation play
|
||||||
if (id === 'settings') window.loadSettings && window.loadSettings();
|
// 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();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,15 +284,27 @@ 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
|
||||||
webView.post {
|
source.start { info -> pushMedia(info, webView) }
|
||||||
val json = info?.toJson() ?: "null"
|
}
|
||||||
webView.evaluateJavascript(
|
|
||||||
"window.HearthEvents && window.HearthEvents.mediaSessionChanged($json);",
|
// H5 页面就绪后重新拉取一次媒体:修复启动时推送早于页面渲染的时序问题
|
||||||
null
|
// 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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user