fix: bottom bar no touch seek; lyrics seek on Android; smoother NP animation
- initProgressScrub: pass noTouch=true for the bottom mini-bar so touch events are never registered on it; only mouse events remain (desktop-only). The now-playing scrubber is unchanged and keeps full touch support. - Export seekToMs(ms) from player.js; use it in the lyrics click handler instead of S.audio.currentTime which has no effect when HAS_BRIDGE (native ExoPlayer handles playback, not the HTML audio element). - styles.css: add will-change:transform to .player and .np-overlay to keep them on GPU compositor layers during the swipe gesture; tweak NP open transition to cubic-bezier(.4,0,.2,1) for a snappier feel. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
9d8a5a22bb
commit
4856f0ca57
3 files changed
+40
-25
No files matched your search
+2
-2
@@ -2,7 +2,7 @@ import { S } from './state.js';
|
||||
import { esc } from './utils.js';
|
||||
import { coverUrl } from './api.js';
|
||||
// Circular imports — safe (only used inside function bodies)
|
||||
import { playCurrent, _syncNpDirect, dlSongLocal, npToggleStar, _syncOverlayState, isPlaying } from './player.js';
|
||||
import { playCurrent, _syncNpDirect, dlSongLocal, npToggleStar, _syncOverlayState, isPlaying, seekToMs } from './player.js';
|
||||
import { navigate } from './views.js';
|
||||
import { openPlModal, showSongInfo } from './ui.js';
|
||||
|
||||
@@ -116,7 +116,7 @@ function _renderSyncedLines(content, lines) {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'lyrics-line';
|
||||
el.textContent = line.value;
|
||||
el.onclick = () => { S.audio.currentTime = line.start / 1000; };
|
||||
el.onclick = () => { seekToMs(line.start); };
|
||||
content.appendChild(el);
|
||||
return { start: line.start, el };
|
||||
});
|
||||
|
||||
+35
-21
@@ -656,10 +656,12 @@ export function seekTo(e) {
|
||||
}
|
||||
|
||||
export function initProgressScrub() {
|
||||
// Bottom mini-bar: mouse-only (touch disabled — too easy to accidentally seek on mobile).
|
||||
_initBarScrub(
|
||||
document.getElementById('progress-bar'),
|
||||
document.getElementById('progress-fill'),
|
||||
pos => { document.getElementById('time-cur').textContent = formatDur(pos * _dur()); }
|
||||
pos => { document.getElementById('time-cur').textContent = formatDur(pos * _dur()); },
|
||||
/* noTouch= */ true
|
||||
);
|
||||
_initBarScrub(
|
||||
document.getElementById('np-bar'),
|
||||
@@ -668,7 +670,17 @@ export function initProgressScrub() {
|
||||
);
|
||||
}
|
||||
|
||||
function _initBarScrub(bar, fill, onMove) {
|
||||
// Export for lyrics / other callers that need to seek by ms.
|
||||
export function seekToMs(ms) {
|
||||
if (HAS_BRIDGE) {
|
||||
_setSkipping();
|
||||
_nsBridge('seekTo', Math.round(ms));
|
||||
} else {
|
||||
S.audio.currentTime = ms / 1000;
|
||||
}
|
||||
}
|
||||
|
||||
function _initBarScrub(bar, fill, onMove, noTouch = false) {
|
||||
if(!bar || !fill) return;
|
||||
function getPos(clientX) { const r = bar.getBoundingClientRect(); return Math.max(0, Math.min(1, (clientX-r.left)/r.width)); }
|
||||
function startScrub(pos) { S._scrubbing=true; fill.style.transition='none'; fill.style.width=(pos*100)+'%'; onMove(pos); }
|
||||
@@ -691,25 +703,27 @@ function _initBarScrub(bar, fill, onMove) {
|
||||
fill.style.transition = '';
|
||||
}
|
||||
}
|
||||
// Touch: require ≥8px horizontal drag before committing to scrub.
|
||||
// A plain tap cancels cleanly without seeking.
|
||||
let _touchStartX = 0, _touchMoved = false;
|
||||
bar.addEventListener('touchstart', e => {
|
||||
_touchStartX = e.touches[0].clientX;
|
||||
_touchMoved = false;
|
||||
e.stopPropagation();
|
||||
e.preventDefault(); // block synthetic mousedown/click that would trigger scrub on plain tap
|
||||
});
|
||||
bar.addEventListener('touchmove', e => {
|
||||
const dx = Math.abs(e.touches[0].clientX - _touchStartX);
|
||||
if (!_touchMoved && dx >= 8) { _touchMoved = true; startScrub(getPos(e.touches[0].clientX)); }
|
||||
if (_touchMoved && S._scrubbing) moveScrub(getPos(e.touches[0].clientX));
|
||||
}, {passive:true});
|
||||
bar.addEventListener('touchend', e => {
|
||||
if (_touchMoved && S._scrubbing) endScrub(getPos(e.changedTouches[0].clientX));
|
||||
else S._scrubbing = false;
|
||||
_touchMoved = false;
|
||||
}, {passive:true});
|
||||
if (!noTouch) {
|
||||
// Touch: require ≥8px horizontal drag before committing to scrub.
|
||||
// A plain tap cancels cleanly without seeking.
|
||||
let _touchStartX = 0, _touchMoved = false;
|
||||
bar.addEventListener('touchstart', e => {
|
||||
_touchStartX = e.touches[0].clientX;
|
||||
_touchMoved = false;
|
||||
e.stopPropagation();
|
||||
e.preventDefault(); // block synthetic mousedown/click that would trigger scrub on plain tap
|
||||
});
|
||||
bar.addEventListener('touchmove', e => {
|
||||
const dx = Math.abs(e.touches[0].clientX - _touchStartX);
|
||||
if (!_touchMoved && dx >= 8) { _touchMoved = true; startScrub(getPos(e.touches[0].clientX)); }
|
||||
if (_touchMoved && S._scrubbing) moveScrub(getPos(e.touches[0].clientX));
|
||||
}, {passive:true});
|
||||
bar.addEventListener('touchend', e => {
|
||||
if (_touchMoved && S._scrubbing) endScrub(getPos(e.changedTouches[0].clientX));
|
||||
else S._scrubbing = false;
|
||||
_touchMoved = false;
|
||||
}, {passive:true});
|
||||
}
|
||||
bar.addEventListener('mousedown', e => {
|
||||
startScrub(getPos(e.clientX)); e.preventDefault();
|
||||
const mm = e2 => { if(S._scrubbing) moveScrub(getPos(e2.clientX)); };
|
||||
|
||||
+3
-2
@@ -223,7 +223,7 @@ button:focus{outline:none}
|
||||
.btn-outline.blue:hover{background:var(--blue-dim)}
|
||||
|
||||
/* ── PLAYER ── */
|
||||
.player{position:fixed;bottom:0;left:0;right:0;height:var(--player-h);background:rgba(8,8,20,.93);backdrop-filter:blur(28px);border-top:1px solid var(--border2);display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:0;padding:0 20px;z-index:100}
|
||||
.player{position:fixed;bottom:0;left:0;right:0;height:var(--player-h);background:rgba(8,8,20,.93);backdrop-filter:blur(28px);border-top:1px solid var(--border2);display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:0;padding:0 20px;z-index:100;will-change:transform}
|
||||
.player-drag-handle{position:absolute;top:5px;left:50%;transform:translateX(-50%);width:32px;height:3px;border-radius:2px;background:rgba(255,255,255,.18);transition:background .2s,width .2s;pointer-events:none}
|
||||
.player.dragging .player-drag-handle{background:rgba(255,255,255,.45);width:44px}
|
||||
.player-track{min-width:0;display:flex;align-items:center;gap:10px;overflow:visible}
|
||||
@@ -521,8 +521,9 @@ button:focus{outline:none}
|
||||
display:flex;flex-direction:column;
|
||||
background:var(--bg);
|
||||
transform:translateY(100%);
|
||||
transition:transform .4s cubic-bezier(.32,.72,0,1);
|
||||
transition:transform .35s cubic-bezier(.4,0,.2,1);
|
||||
overflow:hidden;
|
||||
will-change:transform;
|
||||
}
|
||||
.np-overlay.open{transform:translateY(0)}
|
||||
|
||||
|
||||
Reference in new issue
Block a user