feat: draggable progress bar (seek) and settings sliders

This commit is contained in:
2026-08-17 12:20:05 +08:00
parent 91665bbbd5
commit d1d903278e
4 changed files with 77 additions and 9 deletions
+39
View File
@@ -212,6 +212,8 @@ function renderMediaCard(info) {
// Cover tap opens the media app's playback UI
const coverEl = el.querySelector('.cover');
if (coverEl) coverEl.onclick = () => bridge.call('openMediaApp', currentMediaIndex);
// 进度条拖动 seek
setupMediaSeek(el);
// 进度状态 + 全局 ticker
updateMediaProgressState(info);
startMediaTicker();
@@ -268,6 +270,43 @@ function fmtDuration(ms) {
return m + ':' + String(s % 60).padStart(2, '0');
}
// 进度条拖动 seek:拖动预览进度,松手后 seek 到目标位置
// Progress-bar drag seek: preview while dragging, seek on release
function setupMediaSeek(el) {
const prog = el.querySelector('.prog');
if (!prog) return;
let dragging = false;
const ratioAt = (clientX) => {
const rect = prog.getBoundingClientRect();
return Math.min(1, Math.max(0, (clientX - rect.left) / rect.width));
};
const preview = (clientX) => {
const bar = el.querySelector('.bar');
if (bar) bar.style.width = (ratioAt(clientX) * 100) + '%';
};
prog.addEventListener('touchstart', (e) => {
dragging = true;
preview(e.touches[0].clientX);
}, { passive: true });
prog.addEventListener('touchmove', (e) => {
if (dragging) preview(e.touches[0].clientX);
}, { passive: true });
prog.addEventListener('touchend', (e) => {
if (!dragging) return;
dragging = false;
const ratio = ratioAt(e.changedTouches[0].clientX);
const duration = mediaProgressState ? mediaProgressState.duration : 0;
if (duration > 0) {
const pos = Math.floor(ratio * duration);
bridge.call('mediaSeekTo', currentMediaIndex, pos);
if (mediaProgressState) {
mediaProgressState.position = pos;
mediaProgressState.updatedAt = Date.now();
}
}
}, { passive: true });
}
// 布局方向:默认上下(封面在上、信息在下),横条(宽>高)时左右
// Layout direction: vertical (cover on top, info below) by default; horizontal
// (cover left, info right) when the card is wider than tall
+24 -9
View File
@@ -116,12 +116,10 @@ function setupMask(page) {
window.mask.setEnabled(!window.mask.enabled());
refresh();
};
slider.onclick = (e) => {
const rect = slider.getBoundingClientRect();
const a = Math.min(1, Math.max(0, (e.clientX - rect.left) / rect.width));
makeSliderDraggable(slider, (a) => {
window.mask.setAlpha(Math.round(a * 100) / 100);
refresh();
};
});
}
// 通知使用权授权项:异步查状态,未授权点击跳转系统授权页
@@ -155,12 +153,10 @@ function setupBrightness(page) {
const pct = value / 255 * 100;
sliderEl.style.setProperty('--fill', pct + '%');
sliderEl.querySelector('i').style.left = pct + '%';
sliderEl.onclick = (e) => {
e.stopPropagation();
const rect = sliderEl.getBoundingClientRect();
const v = Math.round(Math.min(255, Math.max(0, (e.clientX - rect.left) / rect.width)) * 255);
makeSliderDraggable(sliderEl, (ratio) => {
const v = Math.round(ratio * 255);
bridge.call('setSystemBrightness', v).then(() => renderSlider(v)).catch(() => {});
};
});
item.appendChild(sliderEl);
};
@@ -237,3 +233,22 @@ function setupCheckUpdate(page) {
});
};
}
// 通用滑块拖动:touch 拖动实时回调(保留 click 兼容桌面调试)
// Generic draggable slider: touch-drag with live callback (click kept for desktop)
function makeSliderDraggable(slider, onRatio) {
let dragging = false;
const ratioAt = (clientX) => {
const rect = slider.getBoundingClientRect();
return Math.min(1, Math.max(0, (clientX - rect.left) / rect.width));
};
slider.addEventListener('touchstart', (e) => {
dragging = true;
onRatio(ratioAt(e.touches[0].clientX));
}, { passive: true });
slider.addEventListener('touchmove', (e) => {
if (dragging) onRatio(ratioAt(e.touches[0].clientX));
}, { passive: true });
slider.addEventListener('touchend', () => { dragging = false; }, { passive: true });
slider.addEventListener('click', (e) => onRatio(ratioAt(e.clientX)));
}
@@ -154,6 +154,13 @@ class MediaSessionSource(context: Context) {
Log.d(TAG, "playPause: index=$index playing=$playing")
}
// 拖动进度条 seek 到指定位置(毫秒)
// Seek to the given position (ms) when dragging the progress bar
fun seekTo(index: Int, position: Long) {
controllers.getOrNull(index)?.transportControls?.seekTo(position)
Log.d(TAG, "seekTo: index=$index position=$position")
}
// 跳转到指定会话的播放界面:优先 sessionActivityPendingIntent),
// 失败则回退到启动该 App
// Jump to the session's playback UI: prefer sessionActivity (PendingIntent),
@@ -355,6 +355,13 @@ class JsBridge(
mediaSourceRef?.next(index)
}
// 拖动进度条 seek 到指定位置(毫秒)
// Seek to the given position (ms) when dragging the progress bar
@android.webkit.JavascriptInterface
fun mediaSeekTo(index: Int, position: Long) {
mediaSourceRef?.seekTo(index, position)
}
// 跳转到指定会话的播放界面(封面点击)
// Jump to the session's playback UI (cover tap)
@android.webkit.JavascriptInterface