fix: grid minmax, media polling fallback, content webview layout, swipe timeout

This commit is contained in:
2026-08-16 20:25:21 +08:00
parent 8d10d9e5e9
commit 7a28e2372c
5 changed files with 59 additions and 10 deletions
+16 -5
View File
@@ -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 {
+13 -3
View File
@@ -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 特殊字符,避免媒体元数据注入标签
@@ -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)
@@ -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<MediaController>()
// 轮询兜底: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
}
@@ -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
}