On a phone held sideways the controls covered about 70% of the picture, so a tap aimed at the picture landed on the banner: it put the controls away but left the banner up with no timer. Now, on a short touch screen, the controls draw tighter (what's next is left to the guide) and cover about half. A tap anywhere off a button puts the banner away with them, as a phone's video players do. The tap is listened for on the picture and the banner themselves, since an iPhone sends a tap's click only to what listens for one. A finger's stand-in mouse events no longer act a second time. Two quick taps in a list were also a double click, playing again what the second tap had already played. A tap's move was also a pointer brought to the screen. The first tap unmutes a picture that started muted on the tap's click, not when the finger lands, which browsers don't count as the user's yet.
- commit
- f6cf4475353c7a4ce39af19307372557698e221a
- parent
- 114b5948464065e2b8c9804bfd3d2dd85de51dba
- author
- FelineStateMachine <226808055+FelineStateMachine@users.noreply.github.com>
- kind
- fix risk score 2.1 of 3, Jev's estimate
| M | README.md | 5 | +++-- |
| M | frontend/ | 6 | +++-- |
| M | frontend/ | 35 | ++++++++++++++++++++++-------- |
| M | frontend/ | 11 | +++++++++ |
@@ -196,11 +196,12 @@ desktop app's guess; `?platform=` does the same at `/tv/`), and Settings, Displa
196196 can change it on one device. Console and TV start at 125%, sized for across a room. A197197 controller or a keyboard used on any of them still brings its own hints.198198 199-On touch, a tap on TV brings up the controls (and puts them away); a swipe left or right199+On touch, a tap on TV brings up the controls, and a tap anywhere off a button puts them200+away, as a phone's video players do; a swipe left or right200201 skips back 10 seconds or ahead 30, up or down changes channel. In the guide a swipe moves201202 the hours shown, a tap picks a program and a second tap watches it, and holding one shows202203 its actions; in Recordings and Podcasts a second tap plays. A phone is used on its side,203-with a compact banner and guide; held upright it asks to be turned.204+with a compact banner, controls and guide; held upright it asks to be turned.204205 205206 The web app is installable (`manifest.webmanifest`, and `sw.js`, which keeps nothing206207 offline): from a secure address, such as a [Tailscale Service](https://tailscale.com/kb/1552/tailscale-services)@@ -128,9 +128,11 @@ function attempt(how, src) {
128128 if (err && err.name === 'NotAllowedError') {129129 video.muted = true;130130 video.play().catch(() => {});131- // A touch screen's next touch, which lets sound play, turns it on.131+ // A touch screen's next tap, which lets sound play, turns it on: once132+ // it's a tap. As a finger lands the browser doesn't yet count it as133+ // the user's, and unmuted then, the picture would stop.132134 if (platform === 'touch') {133- document.addEventListener('pointerdown', () => { if (!state.userMuted) video.muted = false; }, { once: true, capture: true });135+ document.addEventListener('click', () => { if (!state.userMuted) video.muted = false; }, { once: true, capture: true });134136 return toast('Started muted. Tap for sound.', 3500);135137 }136138 const m = keyHint('m', 'M');@@ -12,14 +12,24 @@
1212 // shows its actions (record, favorite, hide). In Recordings and Podcasts a1313 // tap on what's picked plays it. A phone held upright is asked to turn1414 // sideways (style.css).15-const touch = { x: 0, y: 0, on: false, held: 0, longDone: false };15+const touch = { x: 0, y: 0, on: false, held: 0, longDone: false, at: -Infinity };1616 const TOUCH_SWIPE = 48; // CSS pixels a finger travels for a swipe1717 const TOUCH_HOLD_MS = 550; // a press this long is a hold18+const TOUCH_MOUSE_MS = 1000; // a finger's stand-in mouse events follow it within this1819 1920 const touching = (e) => platform === 'touch' && e.pointerType !== 'mouse';2021 22+// A finger also sends a mouse's events after it, for pages made for one.23+// Here they'd act twice: its move is a pointer brought to the screen, which24+// brings up the dock and the controls (wireMouse), and two quick taps are a25+// double click, which plays what the second tap already played. What a26+// finger does is handled here, from its pointer events.27+const fromFinger = () => performance.now() - touch.at < TOUCH_MOUSE_MS;28+2129 function wireTouch() {30+ const lastFinger = (e) => { if (e.pointerType !== 'mouse') touch.at = performance.now(); };2231 document.addEventListener('pointerdown', (e) => {32+ lastFinger(e);2333 if (!touching(e)) return;2434 Object.assign(touch, { x: e.clientX, y: e.clientY, on: true, longDone: false });2535 clearTimeout(touch.held);@@ -31,6 +41,7 @@ function wireTouch() {
3141 }, { capture: true, passive: true });3242 document.addEventListener('pointercancel', () => { touch.on = false; clearTimeout(touch.held); }, true);3343 document.addEventListener('pointerup', (e) => {44+ lastFinger(e);3445 if (!touch.on || !touching(e)) return;3546 touch.on = false;3647 clearTimeout(touch.held);@@ -39,6 +50,9 @@ function wireTouch() {
3950 if (Math.max(Math.abs(dx), Math.abs(dy)) < TOUCH_SWIPE) return;4051 swipe(e.target, Math.abs(dx) > Math.abs(dy) ? (dx > 0 ? 'right' : 'left') : (dy > 0 ? 'down' : 'up'));4152 }, { capture: true, passive: true });53+ for (const type of ['mousemove', 'dblclick']) {54+ document.addEventListener(type, (e) => { if (fromFinger()) e.stopImmediatePropagation(); }, true);55+ }4256 // A hold's lift isn't also a tap.4357 document.addEventListener('click', (e) => {4458 if (touch.longDone) {@@ -47,15 +61,18 @@ function wireTouch() {
4761 e.preventDefault();4862 }4963 }, true);50- // On TV a tap that isn't on a control, on the picture or the banner over51- // it, brings up the controls or puts them away: seen first (capture), so52- // a tap on the picture elsewhere, which goes back to TV (wireDock), isn't53- // one.54- document.addEventListener('click', (e) => {64+ // On TV a tap on the picture, or on the banner over it, brings up the65+ // controls; with them up, a tap anywhere but on a control puts them66+ // away, the banner too, as a phone's video players do. Listened for on67+ // the picture and the banner themselves, as an iPhone sends a tap's68+ // click only to what listens for one, and first (capture), so a tap on69+ // the picture elsewhere, which goes back to TV (wireDock), isn't one.70+ const tapTV = (e) => {5571 if (platform !== 'touch' || state.view !== 'tv' || document.body.classList.contains('booting')) return;56- if (e.target.closest('button, input, a, .ob, .tl, #dock, [data-act]')) return;57- if (osd.open) closeOSD(); else openOSD(null);58- }, true);72+ if (e.target.closest('button, input, a, .tl, [data-act]')) return;73+ if (osd.open) hideBanner(); else openOSD(null);74+ };75+ for (const el of [stage, $('#banner')]) el.addEventListener('click', tapTV, true);5976 }6077 6178 // swipe acts on a swipe over el: on TV, the arrows; in the guide, the@@ -789,6 +789,17 @@ body.pf-touch .s-main { grid-template-rows: minmax(0, 1fr); }
789789 body.pf-touch .bn-title { font-size: 30px; margin-top: 6px; }790790 body.pf-touch .bn-desc, body.pf-touch .bn-cast, body.pf-touch .bn-logo { display: none; }791791 body.pf-touch .bn-next { margin-top: 6px; }792+ /* The controls leave most of the picture showing, there to tap: what's793+ next is in the guide, and the timeline and the buttons draw tighter. */794+ body.pf-touch #banner.osd { padding: 12px 20px 8px; gap: 4px 20px; }795+ body.pf-touch #banner.osd .bn-num { font-size: 48px; }796+ body.pf-touch #banner.osd .bn-call { margin-top: 8px; }797+ body.pf-touch #banner.osd .bn-next { display: none; }798+ body.pf-touch .bn-ep { min-height: 0; }799+ body.pf-touch #banner .tl { padding: 6px 12px; }800+ body.pf-touch .osd-bar { padding-top: 4px; }801+ body.pf-touch .ob { padding: 8px 4px 6px; gap: 5px; }802+ body.pf-touch .ob svg { width: 24px; height: 24px; }792803 /* The guide's top clears the dock, and leaves the grid more rows. */793804 body.pf-touch { --gtop: calc(40 * var(--vh)); --pv-h: calc(var(--gtop) - 62px - 14px); --pv-w: calc(var(--pv-h) * 16 / 9); }794805 body.pf-touch .g-top { padding-top: 62px; padding-bottom: 14px; }