fix: grid minmax, media polling fallback, content webview layout, swipe timeout
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user