From 80852374a8c175b6fb8d933e4de9ee6df14c66e1 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Wed, 23 Sep 2026 11:42:21 -0400 Subject: [PATCH] fix: positionnement lecteur media (mobile/desktop) et drag libre de la mini-video #110 --- CHANGELOG.md | 19 ++++- README.fr.md | 6 +- README.md | 6 +- VERSION | 2 +- desktop/Cargo.lock | 2 +- desktop/Cargo.toml | 2 +- desktop/tauri.conf.json | 2 +- docs/ROADMAP.md | 2 +- docs/features/media-viewers-109.md | 11 ++- frontend/js/now-playing.js | 114 ++++++++++++++++----------- frontend/style.css | 49 ++++++++++-- package.json | 2 +- tests/e2e/media-viewer.spec.js | 26 ++++++ tests/frontend/media-viewer.test.mjs | 12 +++ 14 files changed, 185 insertions(+), 70 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39341ac..70ae52b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Format basé sur [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/), et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). > **En cours de développement** : les changements à venir sont listés dans la section -> [Unreleased](#unreleased). La dernière version livrée est **2.19.0**. +> [Unreleased](#unreleased). La dernière version livrée est **2.19.1**. --- @@ -14,6 +14,23 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). --- +## [2.19.1] — 2026-09-23 + +### Corrigé + +- **#110 — lisibilité et positionnement du lecteur média** : en mode mobile, la + barre audio passe en grille (barre de progression pleine largeur sur sa propre + ligne) et les actions secondaires (précédent/suivant/agrandir/volume) sont + masquées pour éviter tout chevauchement ; le volume est aussi masqué sur les + écrans intermédiaires en desktop, et `overflow: hidden` empêche le débordement + hors de la pilule. La mini-fenêtre vidéo n'est plus ré-aimantée sur les bords : + elle se déplace et se redimensionne **librement** (position absolue mémorisée, + centre autorisé), le glisser fonctionnant désormais depuis n'importe quel point + de la fenêtre (boutons/curseurs/poignée exclus, seuil de 4 px pour préserver + les contrôles natifs de la vidéo). + +--- + ## [2.19.0] — 2026-09-23 ### Ajouté diff --git a/README.fr.md b/README.fr.md index 9b1352a..5e12fc4 100644 --- a/README.fr.md +++ b/README.fr.md @@ -4,7 +4,7 @@ **Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive. -[![Version](https://img.shields.io/badge/Version-2.19.0-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.19.1-blue.svg)]() [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/) [![Python](https://img.shields.io/badge/Python-3.11+-green.svg)](https://www.python.org/) @@ -966,8 +966,8 @@ Ce projet est sous licence **MIT** — voir le fichier [LICENSE](LICENSE) pour l ## 📝 Changelog -Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.19.0). +Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.19.1). --- -*Projet : ObsiGate | Version : 2.19.0 | Dernière mise à jour : Septembre 2026* +*Projet : ObsiGate | Version : 2.19.1 | Dernière mise à jour : Septembre 2026* diff --git a/README.md b/README.md index ce82a8e..fddcb47 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **Ultra-light web gateway for your Obsidian vaults** — Access, browse, and search all your Obsidian notes from any device via a modern, responsive web interface. -[![Version](https://img.shields.io/badge/Version-2.19.0-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.19.1-blue.svg)]() [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/) [![Python](https://img.shields.io/badge/Python-3.11+-green.svg)](https://www.python.org/) @@ -1142,8 +1142,8 @@ This project is licensed under the **MIT License** - see the [LICENSE](LICENSE) ## 📝 Changelog -See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.19.0). +See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.19.1). --- -*Project: ObsiGate | Version: 2.19.0 | Last updated: September 2026* +*Project: ObsiGate | Version: 2.19.1 | Last updated: September 2026* diff --git a/VERSION b/VERSION index ef0f38a..b8e248f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.19.0 +2.19.1 diff --git a/desktop/Cargo.lock b/desktop/Cargo.lock index ee0d420..a83c8aa 100644 --- a/desktop/Cargo.lock +++ b/desktop/Cargo.lock @@ -2626,7 +2626,7 @@ dependencies = [ [[package]] name = "obsigate-desktop" -version = "2.19.0" +version = "2.19.1" dependencies = [ "chrono", "env_logger", diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index 998df8e..839642d 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "obsigate-desktop" -version = "2.19.0" +version = "2.19.1" description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian" authors = ["Bruno Charest"] edition = "2021" diff --git a/desktop/tauri.conf.json b/desktop/tauri.conf.json index 18102ff..b484aa8 100644 --- a/desktop/tauri.conf.json +++ b/desktop/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json", "productName": "ObsiGate", - "version": "2.19.0", + "version": "2.19.1", "identifier": "com.obsigate.desktop", "build": { "frontendDist": "../frontend", diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 165deb7..7aa270a 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,6 +1,6 @@ # ObsiGate — Roadmap -> **Version :** 2.19.0 | **Dernière mise à jour :** 2026-09-23 +> **Version :** 2.19.1 | **Dernière mise à jour :** 2026-09-23 > **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact > vers les fonctionnalités livrées. > - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)** diff --git a/docs/features/media-viewers-109.md b/docs/features/media-viewers-109.md index 53c5a1d..6f0040a 100644 --- a/docs/features/media-viewers-109.md +++ b/docs/features/media-viewers-109.md @@ -134,12 +134,15 @@ global `window.NowPlaying`. - **Panneau étendu** : carte centrale (bottom-sheet sur mobile) avec artwork, scrub large, volume, vitesse 0,5–2×, précédent/suivant et actions ouvrir/télécharger/fermer. -- **Mini-vidéo flottante** (`.np-dock--video`) : déplaçable (aimantation au coin - le plus proche) et redimensionnable, géométrie persistée ; sur mobile elle se - fixe au-dessus de la barre d'outils. +- **Mini-vidéo flottante** (`.np-dock--video`) : déplaçable **librement** depuis + n'importe quel point de la fenêtre (position absolue mémorisée, centre autorisé + — aucune aimantation aux bords) et redimensionnable, géométrie persistée ; sur + mobile elle se fixe au-dessus de la barre d'outils. - **Mobile** : mini-player au-dessus de la barre 64 px (`bottom: calc(64px + env(safe-area-inset-bottom))`), `viewport-fit=cover` - ajouté, et `body.np-active` ajoute le décalage du contenu. + ajouté, et `body.np-active` ajoute le décalage du contenu. La barre audio passe + en grille (progression sur sa propre ligne) et masque les actions secondaires + pour éviter tout chevauchement. ### Comportements diff --git a/frontend/js/now-playing.js b/frontend/js/now-playing.js index 54f075e..5ce1b31 100644 --- a/frontend/js/now-playing.js +++ b/frontend/js/now-playing.js @@ -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; diff --git a/frontend/style.css b/frontend/style.css index 9cf2c43..baf2ff2 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -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 { diff --git a/package.json b/package.json index 0202920..c91e173 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsigate", - "version": "2.19.0", + "version": "2.19.1", "description": "**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.", "main": "patch.js", "directories": { diff --git a/tests/e2e/media-viewer.spec.js b/tests/e2e/media-viewer.spec.js index 04b47bc..18baa71 100644 --- a/tests/e2e/media-viewer.spec.js +++ b/tests/e2e/media-viewer.spec.js @@ -134,4 +134,30 @@ test.describe('Now Playing — lecture persistante (#110)', () => { await expect(mini).toBeVisible({ timeout: 10000 }); await expect(mini.locator('video.np-media--video')).toBeVisible(); }); + + test('la mini-fenêtre vidéo peut être déplacée librement (centre)', async ({ page }) => { + await login(page); + await openFile(page, 'TestVault', 'sample-video.webm'); + await expect(page.locator('#content-area video.np-media--video')).toBeVisible({ timeout: 10000 }); + await openFile(page, 'TestVault', 'note1.md'); + + const mini = page.locator('#now-playing-host .np-dock--video'); + await expect(mini).toBeVisible({ timeout: 10000 }); + const before = await mini.evaluate((el) => ({ left: parseFloat(el.style.left), top: parseFloat(el.style.top) })); + + const box = await mini.boundingBox(); + await page.mouse.move(box.x + box.width / 2, box.y + 12); + await page.mouse.down(); + await page.mouse.move(640, 360, { steps: 12 }); + await page.mouse.up(); + + const after = await mini.evaluate((el) => ({ left: parseFloat(el.style.left), top: parseFloat(el.style.top) })); + expect(after.left).not.toBe(before.left); + expect(after.top).not.toBe(before.top); + // Doit pouvoir rester au centre (pas de re-aimantation sur les bords). + expect(after.left).toBeGreaterThan(50); + expect(after.left).toBeLessThan(900); + expect(after.top).toBeGreaterThan(20); + expect(after.top).toBeLessThan(600); + }); }); diff --git a/tests/frontend/media-viewer.test.mjs b/tests/frontend/media-viewer.test.mjs index 1c0cb65..fb8f6f9 100644 --- a/tests/frontend/media-viewer.test.mjs +++ b/tests/frontend/media-viewer.test.mjs @@ -121,6 +121,13 @@ test("now-playing exposes the public controller API and is initialised at boot", assert.match(app, /initNowPlaying\(\)/); }); +test("video mini-window is freely positioned (no corner snapping)", () => { + assert.doesNotMatch(np, /nearestCorner/); + assert.match(np, /function clampToViewport/); + assert.match(np, /left: Math\.round\(r\.left\),\s*\n\s*top: Math\.round\(r\.top\)/); + assert.match(np, /bindVideoDrag\(dock\);/); +}); + // ── Static checks: CSS ───────────────────────────────────────────────────── test("style.css defines the media viewers and the Now Playing dock", () => { assert.match(css, /\.audio-viewer-container/); @@ -131,6 +138,11 @@ test("style.css defines the media viewers and the Now Playing dock", () => { assert.match(css, /body\.np-active \.content-area/); }); +test("mobile dock uses a grid so actions never overlap the progress bar", () => { + assert.match(css, /grid-template-areas:\s*\n\s*"progress progress progress progress"/); + assert.match(css, /\.np-dock--audio \[data-np="expand"\]/); +}); + test("style.css no longer references undefined --surface1 / --text-dim vars", () => { assert.doesNotMatch(css, /var\(--surface1\)/); assert.doesNotMatch(css, /var\(--text-dim\)/);