Merge remote-tracking branch 'origin/master' into feature/toast-notifications
# Conflicts: # README.md # docs/architecture.md
This commit is contained in:
commit
acb45c8a1a
5 changed files with 129 additions and 3 deletions
|
|
@ -358,6 +358,9 @@ Then put it behind your existing reverse proxy like everything else
|
||||||
movie-poster tiles — circular artist tiles, square album tiles,
|
movie-poster tiles — circular artist tiles, square album tiles,
|
||||||
a real hero header, and a mobile "now playing" full-screen player
|
a real hero header, and a mobile "now playing" full-screen player
|
||||||
— see docs/architecture.md
|
— see docs/architecture.md
|
||||||
|
- [x] Mobile menu's bottom sheet is now swipe-to-close (a real drag
|
||||||
|
handle, not just a decorative grabber) instead of fighting the
|
||||||
|
browser's own pull-to-refresh gesture — see docs/architecture.md
|
||||||
- [x] Every blocking `alert()` (host handoff, every "couldn't start/join
|
- [x] Every blocking `alert()` (host handoff, every "couldn't start/join
|
||||||
a party" error) replaced with a light, auto-dismissing toast — see
|
a party" error) replaced with a light, auto-dismissing toast — see
|
||||||
docs/architecture.md
|
docs/architecture.md
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,12 @@
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
transition: opacity .2s;
|
transition: opacity .2s;
|
||||||
|
/* A touch starting here has no scrollable content of its own to
|
||||||
|
"contain" (overscroll-behavior wouldn't apply) - this is what
|
||||||
|
stops the browser reading a drag over the dimmed backdrop as a
|
||||||
|
page-level gesture (pull-to-refresh) instead of just dismissing
|
||||||
|
the sheet via the existing tap-to-close below. */
|
||||||
|
touch-action: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobileNavScrim.open { opacity: 1; pointer-events: auto; }
|
.mobileNavScrim.open { opacity: 1; pointer-events: auto; }
|
||||||
|
|
@ -133,6 +139,12 @@
|
||||||
z-index: 21;
|
z-index: 21;
|
||||||
max-height: 75vh;
|
max-height: 75vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
/* Stops a drag that hits the top of the sheet's OWN scroll (its
|
||||||
|
library grid can be taller than 75vh) from chaining past this
|
||||||
|
element to the page underneath and surfacing as pull-to-refresh -
|
||||||
|
complements touch-action:none on the dedicated grabber handle
|
||||||
|
below, which instead covers a drag that never scrolls anything. */
|
||||||
|
overscroll-behavior: contain;
|
||||||
background: var(--bg-header);
|
background: var(--bg-header);
|
||||||
border-radius: 16px 16px 0 0;
|
border-radius: 16px 16px 0 0;
|
||||||
padding: 8px 16px calc(20px + env(safe-area-inset-bottom, 0px));
|
padding: 8px 16px calc(20px + env(safe-area-inset-bottom, 0px));
|
||||||
|
|
@ -143,12 +155,27 @@
|
||||||
|
|
||||||
.mobileNavSheet.open { transform: translateY(0); }
|
.mobileNavSheet.open { transform: translateY(0); }
|
||||||
|
|
||||||
|
/* Dragging (see Shell.ts) suspends the transition above for 1:1
|
||||||
|
tracking under the finger, then this class restores it for the
|
||||||
|
snap-back/close animation - same "instant while dragging, animated
|
||||||
|
on release" split views/player/Player.css's own seek bar uses. */
|
||||||
|
.mobileNavSheet.dragging { transition: none; }
|
||||||
|
|
||||||
|
/* Bigger drag/touch target than the pill it contains (36x4px is far
|
||||||
|
below a real touch-target minimum) - see Shell.html/Shell.ts. */
|
||||||
|
.sheetGrabberHandle {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 14px 0 10px;
|
||||||
|
touch-action: none;
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
.sheetGrabber {
|
.sheetGrabber {
|
||||||
width: 36px;
|
width: 36px;
|
||||||
height: 4px;
|
height: 4px;
|
||||||
border-radius: 99px;
|
border-radius: 99px;
|
||||||
background: var(--muted);
|
background: var(--muted);
|
||||||
margin: 10px auto 6px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Accueil and the (conditional) Listes de lecture row both use this -
|
/* Accueil and the (conditional) Listes de lecture row both use this -
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,14 @@
|
||||||
|
|
||||||
<div class="mobileNavScrim" id="mobileNavScrim"></div>
|
<div class="mobileNavScrim" id="mobileNavScrim"></div>
|
||||||
<div class="mobileNavSheet" id="mobileNavSheet">
|
<div class="mobileNavSheet" id="mobileNavSheet">
|
||||||
<div class="sheetGrabber"></div>
|
<!-- Bigger invisible drag target than the visible pill itself (see
|
||||||
|
.sheetGrabberHandle) - touch-action:none so this app's own
|
||||||
|
pointer handlers (Shell.ts) drive the drag instead of the
|
||||||
|
browser's default touch handling, which otherwise reads a
|
||||||
|
downward drag here as pull-to-refresh (the sheet sits right at
|
||||||
|
the top of the page's own scroll position, same conflict a
|
||||||
|
plain scrim tap-only close used to leave unsolved). -->
|
||||||
|
<div class="sheetGrabberHandle" id="sheetGrabberHandle"><div class="sheetGrabber"></div></div>
|
||||||
<a href="#/home" class="navItem sheetPrimary"><span class="navIcon" data-icon="home"></span>Accueil</a>
|
<a href="#/home" class="navItem sheetPrimary"><span class="navIcon" data-icon="home"></span>Accueil</a>
|
||||||
<div class="navSectionLabel">Bibliothèques</div>
|
<div class="navSectionLabel">Bibliothèques</div>
|
||||||
<div class="sheetGrid" id="mobileLibGrid"></div>
|
<div class="sheetGrid" id="mobileLibGrid"></div>
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@ export async function renderShell(appEl: HTMLElement): Promise<HTMLElement> {
|
||||||
|
|
||||||
// ---- Mobile bottom sheet - see Shell.css's "Mobile navigation". ----
|
// ---- Mobile bottom sheet - see Shell.css's "Mobile navigation". ----
|
||||||
const navTrigger = appEl.querySelector('#mobileNavTrigger') as HTMLButtonElement;
|
const navTrigger = appEl.querySelector('#mobileNavTrigger') as HTMLButtonElement;
|
||||||
const navSheet = appEl.querySelector('#mobileNavSheet')!;
|
const navSheet = appEl.querySelector('#mobileNavSheet') as HTMLElement;
|
||||||
const navScrim = appEl.querySelector('#mobileNavScrim')!;
|
const navScrim = appEl.querySelector('#mobileNavScrim')!;
|
||||||
const closeMobileNav = () => {
|
const closeMobileNav = () => {
|
||||||
navSheet.classList.remove('open');
|
navSheet.classList.remove('open');
|
||||||
|
|
@ -127,5 +127,39 @@ export async function renderShell(appEl: HTMLElement): Promise<HTMLElement> {
|
||||||
});
|
});
|
||||||
window.addEventListener('hashchange', closeMobileNav);
|
window.addEventListener('hashchange', closeMobileNav);
|
||||||
|
|
||||||
|
// Drag-to-dismiss on the grabber handle only - not the whole sheet,
|
||||||
|
// which would fight the library grid's own scroll/tap targets (see
|
||||||
|
// docs/architecture.md's "Mobile navigation, take two"). Reported
|
||||||
|
// directly: without this, a scrim tap was the ONLY way to close the
|
||||||
|
// sheet, and the instinctive swipe-it-down gesture instead fell
|
||||||
|
// through to the browser's own pull-to-refresh (see Shell.css's
|
||||||
|
// touch-action/overscroll-behavior on the scrim/sheet themselves -
|
||||||
|
// this is the other half, giving that swipe something to actually do).
|
||||||
|
const CLOSE_DRAG_THRESHOLD_PX = 90;
|
||||||
|
const grabberHandle = appEl.querySelector('#sheetGrabberHandle') as HTMLElement;
|
||||||
|
let dragStartY: number | null = null;
|
||||||
|
let dragDeltaY = 0;
|
||||||
|
|
||||||
|
grabberHandle.addEventListener('pointerdown', (e) => {
|
||||||
|
dragStartY = e.clientY;
|
||||||
|
dragDeltaY = 0;
|
||||||
|
grabberHandle.setPointerCapture(e.pointerId);
|
||||||
|
navSheet.classList.add('dragging');
|
||||||
|
});
|
||||||
|
grabberHandle.addEventListener('pointermove', (e) => {
|
||||||
|
if (dragStartY === null) return;
|
||||||
|
dragDeltaY = Math.max(0, e.clientY - dragStartY); // only follows a downward drag - closing is the only thing this gesture does
|
||||||
|
navSheet.style.transform = `translateY(${dragDeltaY}px)`;
|
||||||
|
});
|
||||||
|
function endDrag() {
|
||||||
|
if (dragStartY === null) return;
|
||||||
|
dragStartY = null;
|
||||||
|
navSheet.classList.remove('dragging');
|
||||||
|
navSheet.style.transform = ''; // hands back to the .open class's own rule - either translateY(0) (snap back) or the closeMobileNav() call below immediately overrides it anyway
|
||||||
|
if (dragDeltaY > CLOSE_DRAG_THRESHOLD_PX) closeMobileNav();
|
||||||
|
}
|
||||||
|
grabberHandle.addEventListener('pointerup', endDrag);
|
||||||
|
grabberHandle.addEventListener('pointercancel', endDrag);
|
||||||
|
|
||||||
return appEl.querySelector('#mainContent') as HTMLElement;
|
return appEl.querySelector('#mainContent') as HTMLElement;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3775,6 +3775,61 @@ genuinely narrow viewport. **Not verified**: any of this against a
|
||||||
real Emby server's actual artist/album images and names, or on a real
|
real Emby server's actual artist/album images and names, or on a real
|
||||||
phone.
|
phone.
|
||||||
|
|
||||||
|
## Mobile navigation, take two: swipe-to-close was fighting pull-to-refresh
|
||||||
|
|
||||||
|
Reported directly, against a real phone: the bottom sheet (see "Mobile
|
||||||
|
navigation" above) had exactly one way to close it - tap the dimmed
|
||||||
|
scrim - since the grabber was purely decorative. Swiping the sheet
|
||||||
|
down, the instinctive gesture every real bottom sheet supports, fell
|
||||||
|
through to the browser's own default touch handling instead: with
|
||||||
|
nothing telling it otherwise, a downward drag starting on the scrim or
|
||||||
|
the sheet reads to the browser as "the user wants to pull the page
|
||||||
|
down," which on a phone is the built-in pull-to-refresh gesture - not
|
||||||
|
a bug in this app's own logic, but a real gap in it (nothing was
|
||||||
|
`preventDefault`-ing or claiming that gesture for the sheet instead).
|
||||||
|
|
||||||
|
**Two independent parts, not one fix**: containing the browser's own
|
||||||
|
gesture, and giving the swipe something to actually do.
|
||||||
|
|
||||||
|
- `touch-action: none` on `.mobileNavScrim` and the new
|
||||||
|
`.sheetGrabberHandle` - tells the browser not to apply *any* of its
|
||||||
|
own default touch behavior (panning, pull-to-refresh) to a touch
|
||||||
|
starting on either, since this app's own pointer handlers own that
|
||||||
|
gesture now. `overscroll-behavior: contain` on `.mobileNavSheet`
|
||||||
|
itself covers the other half of the same problem: its own library
|
||||||
|
grid can be taller than the sheet's `75vh` cap, so a drag that hits
|
||||||
|
*that* scroll boundary needs to stop there too, not chain past the
|
||||||
|
sheet to the page underneath.
|
||||||
|
- **The grabber is now a real drag handle**, not just a visual hint.
|
||||||
|
`#sheetGrabberHandle` wraps the small visible pill in a much bigger
|
||||||
|
invisible touch target (a real touch target, not a 36x4px sliver)
|
||||||
|
and drives the sheet with the same pointerdown/move/up(/cancel)
|
||||||
|
pattern `views/player/Player.css`'s own seek bar already uses:
|
||||||
|
1:1 tracking under the finger while dragging (`.dragging` suspends
|
||||||
|
the CSS transition for this), then either snapping back open (a
|
||||||
|
drag under 90px) or calling the exact same `closeMobileNav()` the
|
||||||
|
scrim tap already used (past it) - one close path, two ways to
|
||||||
|
reach it, not a second parallel implementation. Deliberately scoped
|
||||||
|
to the grabber alone, not the whole sheet: the sheet's own content
|
||||||
|
(the library grid, the logout row) needs normal taps and its own
|
||||||
|
scroll to keep working, which a whole-sheet drag-to-close would
|
||||||
|
fight.
|
||||||
|
|
||||||
|
Verified: full `typecheck`/`build` (client only). The CSS properties
|
||||||
|
(`touch-action`/`overscroll-behavior`) confirmed applied via
|
||||||
|
`getComputedStyle` against the real compiled `app.css`. The drag logic
|
||||||
|
itself confirmed with dispatched `PointerEvent`s carrying real
|
||||||
|
`clientY`/`pointerId` values against the real handlers copied from
|
||||||
|
`Shell.ts`: a 40px drag snaps back (stays open, `transform` cleared),
|
||||||
|
a 120px drag closes, and the live `transform` mid-drag tracks the
|
||||||
|
pointer 1:1 (confirmed at an intermediate point, not just the start/end
|
||||||
|
state) - not just asserted from reading the threshold constant.
|
||||||
|
Existing behavior re-confirmed unchanged in the same pass: scrim-tap-
|
||||||
|
to-close still closes. **Not verified**: actual touch input (not a
|
||||||
|
mouse or dispatched `PointerEvent`) on a real phone - no device
|
||||||
|
reachable from this sandbox, same standing gap as this project's other
|
||||||
|
touch-specific work (the video/music seek bars' own drag handling).
|
||||||
|
|
||||||
## Toast notifications, replacing every blocking `alert()`
|
## Toast notifications, replacing every blocking `alert()`
|
||||||
|
|
||||||
Asked for directly, prompted by the host-handoff message specifically
|
Asked for directly, prompted by the host-handoff message specifically
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue