From 7a28e2372cff8ddeeefa66cb4cfe39f842a910be Mon Sep 17 00:00:00 2001 From: AskaEth Date: Sun, 16 Aug 2026 20:25:21 +0800 Subject: [PATCH] fix: grid minmax, media polling fallback, content webview layout, swipe timeout --- app/src/main/assets/h5/css/app.css | 21 ++++++++++++++----- app/src/main/assets/h5/js/pages/home.js | 16 +++++++++++--- .../main/java/top/yeij/hearth/MainActivity.kt | 8 +++++++ .../yeij/hearth/media/MediaSessionSource.kt | 18 ++++++++++++++++ .../top/yeij/hearth/webview/WebViewManager.kt | 6 ++++-- 5 files changed, 59 insertions(+), 10 deletions(-) diff --git a/app/src/main/assets/h5/css/app.css b/app/src/main/assets/h5/css/app.css index c82b266..7f5e6a6 100644 --- a/app/src/main/assets/h5/css/app.css +++ b/app/src/main/assets/h5/css/app.css @@ -126,7 +126,7 @@ body { flex: 1; min-height: 0; display: grid; - grid-template-columns: repeat(3, 1fr); + grid-template-columns: repeat(3, minmax(0, 1fr)); grid-auto-rows: 1fr; gap: 16px; padding: 16px; @@ -172,7 +172,7 @@ body { } .time-card .big { - font-size: 56px; + font-size: clamp(26px, 3.5vw, 52px); font-weight: 300; line-height: 1; /* 等宽数字:1 和 0 等宽,避免时间刷新时卡片宽度抖动 */ @@ -275,9 +275,8 @@ body { margin-top: 2px; font-size: 13px; color: var(--text-dim); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + white-space: normal; + word-break: break-word; } /* 进度条 */ @@ -298,6 +297,18 @@ body { transition: width .3s linear; } +/* 无时长信息时的活动脉冲进度条 */ +/* Indeterminate pulse when no duration is available */ +.media-card .bar.indeterminate { + width: 40%; + animation: bar-pulse 1.5s ease-in-out infinite; +} + +@keyframes bar-pulse { + 0%, 100% { transform: translateX(0); } + 50% { transform: translateX(150%); } +} + /* 控制按钮:SVG 图标 + 按压动画 */ /* Controls: SVG icons + press animation */ .media-card .controls { diff --git a/app/src/main/assets/h5/js/pages/home.js b/app/src/main/assets/h5/js/pages/home.js index 4070967..7bea464 100644 --- a/app/src/main/assets/h5/js/pages/home.js +++ b/app/src/main/assets/h5/js/pages/home.js @@ -192,7 +192,13 @@ function startMediaTicker() { const bar = document.querySelector('.media-card .bar'); if (!bar || !mediaProgressState) return; const { position, duration, playing, updatedAt } = mediaProgressState; - if (!duration) return; + if (!duration) { + // 无时长信息(部分 App 不提供 duration):显示活动脉冲而非空白 + // No duration (some apps omit it): show an indeterminate pulse instead of blank + bar.classList.add('indeterminate'); + return; + } + bar.classList.remove('indeterminate'); const pos = playing ? position + (Date.now() - updatedAt) : position; bar.style.width = Math.min(100, (pos / duration) * 100) + '%'; }, 1000); @@ -242,11 +248,15 @@ function switchMedia(delta) { const outClass = delta > 0 ? 'slide-out-left' : 'slide-out-right'; const inClass = delta > 0 ? 'slide-in-right' : 'slide-in-left'; old.classList.add(outClass); - old.addEventListener('animationend', () => { + // 用 setTimeout 替代 animationend(更可靠,避免动画事件丢失导致"只能划一次") + // Use setTimeout instead of animationend (more reliable, avoids the "swipe once + // then stuck" issue when the animation event is lost) + setTimeout(() => { + if (!old.isConnected) return; const fresh = renderMediaCard(info); fresh.classList.add(inClass); old.replaceWith(fresh); - }, { once: true }); + }, 180); } // 转义 HTML 特殊字符,避免媒体元数据注入标签 diff --git a/app/src/main/java/top/yeij/hearth/MainActivity.kt b/app/src/main/java/top/yeij/hearth/MainActivity.kt index 6527446..d583cb8 100644 --- a/app/src/main/java/top/yeij/hearth/MainActivity.kt +++ b/app/src/main/java/top/yeij/hearth/MainActivity.kt @@ -54,6 +54,10 @@ class MainActivity : Activity() { // 内容 WebView 顶栏预留高度(dp) // Topbar reserved height for content WebViews (dp) private const val TOPBAR_HEIGHT_DP = 44 + // 全局侧边栏收起宽度(dp),内容 WebView 左侧预留,避免挡住侧边栏 + // Global sidebar collapsed width (dp); content WebViews reserve it on the + // left to avoid covering the sidebar + private const val SIDEBAR_WIDTH_DP = 80 } private val webAppContainer = WebAppContainer() @@ -237,6 +241,9 @@ class MainActivity : Activity() { private val topBarHeightPx by lazy { (TOPBAR_HEIGHT_DP * resources.displayMetrics.density).toInt() } + private val sidebarWidthPx by lazy { + (SIDEBAR_WIDTH_DP * resources.displayMetrics.density).toInt() + } override fun openWebView(id: String, url: String) { if (contentWebViews.containsKey(id)) { @@ -261,6 +268,7 @@ class MainActivity : Activity() { ViewGroup.LayoutParams.MATCH_PARENT ) params.topMargin = topBarHeightPx + params.leftMargin = sidebarWidthPx contentWebViews[id] = webView root.addView(webView, params) webView.loadUrl(url) 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 25bd30a..d14fe20 100644 --- a/app/src/main/java/top/yeij/hearth/media/MediaSessionSource.kt +++ b/app/src/main/java/top/yeij/hearth/media/MediaSessionSource.kt @@ -8,6 +8,8 @@ import android.media.MediaMetadata import android.media.session.MediaController import android.media.session.MediaSessionManager import android.media.session.PlaybackState +import android.os.Handler +import android.os.Looper import android.util.Base64 import android.util.Log import java.io.ByteArrayOutputStream @@ -33,6 +35,18 @@ class MediaSessionSource(context: Context) { private var registered = false private val controllers = mutableListOf() + // 轮询兜底:registerCallback 在部分场景不触发(如歌词实时更新时), + // 每 2 秒主动拉取一次进度/状态,保证进度条与信息持续更新 + // Polling fallback: registerCallback doesn't always fire (e.g. live lyrics), + // so actively pull progress/state every 2s to keep the bar and info fresh + private val pollHandler = Handler(Looper.getMainLooper()) + private val pollRunnable = object : Runnable { + override fun run() { + refresh() + pollHandler.postDelayed(this, 2000) + } + } + // 元数据/播放状态变化回调:切歌、暂停/播放时实时推送 // Metadata/playback-state change callback: push in real time on track switch or play/pause private val mediaCallback = object : MediaController.Callback() { @@ -69,6 +83,9 @@ class MediaSessionSource(context: Context) { Log.d(TAG, "start: media session listener registered") updateControllers(msm.getActiveSessions(listenerComponent)) pushCurrent() + // 启动轮询兜底 + pollHandler.removeCallbacks(pollRunnable) + pollHandler.postDelayed(pollRunnable, 2000) } catch (e: SecurityException) { // 未授权「通知使用权」时系统抛 SecurityException:媒体监听降级为不可用,不崩溃。 // MediaSessionService throws SecurityException without notification access; @@ -87,6 +104,7 @@ class MediaSessionSource(context: Context) { Log.d(TAG, "stop: media session listener removed") } unregisterCallbacks() + pollHandler.removeCallbacks(pollRunnable) callback = null } diff --git a/app/src/main/java/top/yeij/hearth/webview/WebViewManager.kt b/app/src/main/java/top/yeij/hearth/webview/WebViewManager.kt index 3eb81fa..0ee52c5 100644 --- a/app/src/main/java/top/yeij/hearth/webview/WebViewManager.kt +++ b/app/src/main/java/top/yeij/hearth/webview/WebViewManager.kt @@ -26,12 +26,14 @@ class WebViewManager(private val activity: Activity) { } // 创建内容 WebView(webAPP 标签页),仅配置不加载、不挂载(由宿主管理) + // 透明背景:webAPP 未声明背景色时透出桌面壁纸,而非默认白底 // Create a content WebView (webapp tab), configured but not loaded/attached - // (lifecycle is managed by the host) + // (lifecycle is managed by the host); transparent background so a webapp without + // a declared background shows the wallpaper instead of a default white @SuppressLint("SetJavaScriptEnabled") fun createContentWebView(): WebView { val webView = WebView(activity) - webView.setBackgroundColor(Color.WHITE) + webView.setBackgroundColor(Color.TRANSPARENT) configure(webView) return webView }