fix: positionnement lecteur media (mobile/desktop) et drag libre de la mini-video #110
This commit is contained in:
+67
-47
@@ -365,9 +365,13 @@ function buildAudioDock() {
|
||||
art.addEventListener('click', (e) => { stopEvent(e); toggleExpanded(); });
|
||||
dock.appendChild(art);
|
||||
|
||||
const info = npEl('div', { class: 'np-dock-info' });
|
||||
const info = npEl('div', { class: 'np-dock-info', title: t('player.expand'), role: 'button', tabindex: '0' });
|
||||
info.appendChild(npEl('span', { class: 'np-dock-title' }));
|
||||
info.appendChild(npEl('span', { class: 'np-dock-sub' }));
|
||||
info.addEventListener('click', (e) => { stopEvent(e); toggleExpanded(); });
|
||||
info.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleExpanded(); }
|
||||
});
|
||||
dock.appendChild(info);
|
||||
|
||||
const controls = npEl('div', { class: 'np-dock-controls' });
|
||||
@@ -425,7 +429,7 @@ function buildVideoDock() {
|
||||
dock.appendChild(frame);
|
||||
const resize = npEl('div', { class: 'np-video-resize', title: t('player.resize'), 'aria-hidden': 'true' });
|
||||
dock.appendChild(resize);
|
||||
bindVideoDrag(dock, grip);
|
||||
bindVideoDrag(dock);
|
||||
bindVideoResize(dock, resize);
|
||||
return dock;
|
||||
}
|
||||
@@ -833,75 +837,91 @@ function restore() {
|
||||
notify(t('player.resume'), 'info');
|
||||
}
|
||||
|
||||
// ── Video dock drag & resize ──────────────────────────────────────────────────
|
||||
// ── Video dock drag & resize (free positioning anywhere in the viewport) ──────
|
||||
const VIDEO_MIN = { w: 240, h: 150 };
|
||||
const VIDEO_DEFAULT = { w: 360, h: 220 };
|
||||
|
||||
function clampToViewport(left, top, w, h) {
|
||||
const margin = 8;
|
||||
const maxLeft = Math.max(margin, window.innerWidth - w - margin);
|
||||
const maxTop = Math.max(margin, window.innerHeight - h - margin);
|
||||
return {
|
||||
left: Math.min(Math.max(margin, left), maxLeft),
|
||||
top: Math.min(Math.max(margin, top), maxTop),
|
||||
};
|
||||
}
|
||||
|
||||
function applyVideoGeometry() {
|
||||
if (!S.dock || S.dockKind !== 'video') return;
|
||||
const pos = readPosition();
|
||||
const mobile = isMobile();
|
||||
if (mobile) {
|
||||
if (isMobile()) {
|
||||
S.dock.style.removeProperty('left');
|
||||
S.dock.style.removeProperty('top');
|
||||
S.dock.style.removeProperty('width');
|
||||
S.dock.style.removeProperty('height');
|
||||
return;
|
||||
}
|
||||
const w = pos && pos.w ? pos.w : 360;
|
||||
const h = pos && pos.h ? pos.h : 220;
|
||||
const pos = readPosition() || {};
|
||||
const w = pos.w || VIDEO_DEFAULT.w;
|
||||
const h = pos.h || VIDEO_DEFAULT.h;
|
||||
S.dock.style.width = w + 'px';
|
||||
S.dock.style.height = h + 'px';
|
||||
const vw = window.innerWidth, vh = window.innerHeight;
|
||||
const margin = 16;
|
||||
const corner = (pos && pos.corner) || 'br';
|
||||
let left, top;
|
||||
if (corner.includes('l')) left = margin; else left = vw - w - margin;
|
||||
if (corner.includes('t')) top = margin + 56; else top = vh - h - 24;
|
||||
S.dock.style.left = Math.max(margin, left) + 'px';
|
||||
S.dock.style.top = Math.max(margin, top) + 'px';
|
||||
const left = typeof pos.left === 'number' ? pos.left : window.innerWidth - w - margin;
|
||||
const top = typeof pos.top === 'number' ? pos.top : window.innerHeight - h - 24;
|
||||
const c = clampToViewport(left, top, w, h);
|
||||
S.dock.style.left = c.left + 'px';
|
||||
S.dock.style.top = c.top + 'px';
|
||||
}
|
||||
|
||||
function nearestCorner(left, top, w, h) {
|
||||
const midX = left + w / 2, midY = top + h / 2;
|
||||
const vw = window.innerWidth, vh = window.innerHeight;
|
||||
return `${midY < vh / 2 ? 't' : 'b'}${midX < vw / 2 ? 'l' : 'r'}`;
|
||||
}
|
||||
|
||||
function bindVideoDrag(dock, grip) {
|
||||
if (!grip) return;
|
||||
grip.style.cursor = 'grab';
|
||||
grip.addEventListener('pointerdown', (e) => {
|
||||
if (isMobile()) return;
|
||||
e.preventDefault();
|
||||
// Drag from anywhere on the mini-window (buttons, sliders and the resize handle
|
||||
// excluded). A small movement threshold keeps clicks on the native video
|
||||
// controls working; no pointer capture is used so those controls keep working.
|
||||
function bindVideoDrag(dock) {
|
||||
dock.addEventListener('pointerdown', (e) => {
|
||||
if (isMobile() || e.button !== 0) return;
|
||||
if (e.target.closest('button, input, select, textarea, a, .np-video-resize')) return;
|
||||
const rect = dock.getBoundingClientRect();
|
||||
S._drag = { dx: e.clientX - rect.left, dy: e.clientY - rect.top, w: rect.width, h: rect.height };
|
||||
grip.setPointerCapture?.(e.pointerId);
|
||||
grip.style.cursor = 'grabbing';
|
||||
S._drag = {
|
||||
dx: e.clientX - rect.left,
|
||||
dy: e.clientY - rect.top,
|
||||
startX: e.clientX,
|
||||
startY: e.clientY,
|
||||
moved: false,
|
||||
};
|
||||
document.addEventListener('pointermove', onDragMove);
|
||||
document.addEventListener('pointerup', onDragEnd);
|
||||
});
|
||||
}
|
||||
|
||||
function onDragMove(e) {
|
||||
if (!S._drag || !S.dock) return;
|
||||
const margin = 8;
|
||||
if (!S._drag || S._drag.resize || !S.dock) return;
|
||||
if (!S._drag.moved) {
|
||||
if (Math.abs(e.clientX - S._drag.startX) < 4 && Math.abs(e.clientY - S._drag.startY) < 4) return;
|
||||
S._drag.moved = true;
|
||||
S.dock.classList.add('np-dragging');
|
||||
}
|
||||
const w = S.dock.offsetWidth, h = S.dock.offsetHeight;
|
||||
let left = e.clientX - S._drag.dx;
|
||||
let top = e.clientY - S._drag.dy;
|
||||
left = Math.max(margin, Math.min(window.innerWidth - w - margin, left));
|
||||
top = Math.max(margin, Math.min(window.innerHeight - h - margin, top));
|
||||
S.dock.style.left = left + 'px';
|
||||
S.dock.style.top = top + 'px';
|
||||
const c = clampToViewport(e.clientX - S._drag.dx, e.clientY - S._drag.dy, w, h);
|
||||
S.dock.style.left = c.left + 'px';
|
||||
S.dock.style.top = c.top + 'px';
|
||||
}
|
||||
|
||||
function onDragEnd(e) {
|
||||
function onDragEnd() {
|
||||
document.removeEventListener('pointermove', onDragMove);
|
||||
document.removeEventListener('pointerup', onDragEnd);
|
||||
if (!S.dock) return;
|
||||
const r = S.dock.getBoundingClientRect();
|
||||
const corner = nearestCorner(r.left, r.top, r.width, r.height);
|
||||
writePosition({ corner, w: Math.round(r.width), h: Math.round(r.height) });
|
||||
applyVideoGeometry();
|
||||
void e;
|
||||
if (!S.dock) { S._drag = null; return; }
|
||||
S.dock.classList.remove('np-dragging');
|
||||
if (S._drag && S._drag.moved) {
|
||||
const r = S.dock.getBoundingClientRect();
|
||||
const pos = readPosition() || {};
|
||||
writePosition({
|
||||
...pos,
|
||||
left: Math.round(r.left),
|
||||
top: Math.round(r.top),
|
||||
w: Math.round(r.width),
|
||||
h: Math.round(r.height),
|
||||
});
|
||||
}
|
||||
S._drag = null;
|
||||
}
|
||||
|
||||
@@ -932,8 +952,8 @@ function onResizeEnd() {
|
||||
document.removeEventListener('pointerup', onResizeEnd);
|
||||
if (S.dock) {
|
||||
const r = S.dock.getBoundingClientRect();
|
||||
const pos = readPosition() || {};
|
||||
writePosition({ ...pos, w: Math.round(r.width), h: Math.round(r.height) });
|
||||
const c = clampToViewport(r.left, r.top, r.width, r.height);
|
||||
writePosition({ left: Math.round(c.left), top: Math.round(c.top), w: Math.round(r.width), h: Math.round(r.height) });
|
||||
applyVideoGeometry();
|
||||
}
|
||||
S._drag = null;
|
||||
|
||||
+43
-6
@@ -9913,6 +9913,7 @@ body.desktop-mode .editor-container {
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
|
||||
backdrop-filter: blur(12px);
|
||||
overflow: hidden;
|
||||
}
|
||||
.np-dock--audio .np-media {
|
||||
display: none;
|
||||
@@ -9931,11 +9932,12 @@ body.desktop-mode .editor-container {
|
||||
cursor: pointer;
|
||||
}
|
||||
.np-dock-info {
|
||||
flex: 1 1 auto;
|
||||
flex: 1 1 120px;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.np-dock-title {
|
||||
color: var(--text);
|
||||
@@ -9958,8 +9960,8 @@ body.desktop-mode .editor-container {
|
||||
gap: 2px;
|
||||
}
|
||||
.np-dock-progress {
|
||||
flex: 1 1 180px;
|
||||
min-width: 120px;
|
||||
flex: 1 1 160px;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
@@ -10209,6 +10211,13 @@ body.desktop-mode .editor-container {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* Mid-size screens: drop the volume slider before the row gets cramped. */
|
||||
@media (max-width: 1100px) {
|
||||
.np-dock--audio .np-volume {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.np-dock--audio {
|
||||
left: 8px;
|
||||
@@ -10216,11 +10225,39 @@ body.desktop-mode .editor-container {
|
||||
bottom: calc(64px + env(safe-area-inset-bottom, 0px) + 8px);
|
||||
transform: none;
|
||||
width: auto;
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto auto;
|
||||
grid-template-areas:
|
||||
"progress progress progress progress"
|
||||
"art info controls actions";
|
||||
align-items: center;
|
||||
gap: 6px 8px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.np-dock-progress {
|
||||
flex-basis: 90px;
|
||||
.np-dock--audio .np-dock-art {
|
||||
grid-area: art;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
}
|
||||
.np-volume {
|
||||
.np-dock--audio .np-dock-info {
|
||||
grid-area: info;
|
||||
}
|
||||
.np-dock--audio .np-dock-controls {
|
||||
grid-area: controls;
|
||||
}
|
||||
.np-dock--audio .np-dock-progress {
|
||||
grid-area: progress;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
.np-dock--audio .np-dock-actions {
|
||||
grid-area: actions;
|
||||
}
|
||||
/* Compact mobile: keep play / return / close; expand via the artwork. */
|
||||
.np-dock--audio [data-np="prev"],
|
||||
.np-dock--audio [data-np="next"],
|
||||
.np-dock--audio [data-np="expand"] {
|
||||
display: none;
|
||||
}
|
||||
.np-dock--video {
|
||||
|
||||
Reference in New Issue
Block a user