clone

a desktop tv and program guide for over-the-air broadcasts: what's on air, what your tuner gets, live tv, recordings

main
airwaves / frontend / dist / js / touch.js
touch.js 5.2K
1'use strict';23// Touch: a phone or a tablet, the web app installed or in a browser.45// ---------- touch ----------6// Taps are clicks, which every view takes already; this adds what a finger7// needs besides. On TV a tap brings up the controls and puts them away, a8// swipe left or right skips as Left and Right do (back 10 s, ahead 30 s),9// up or down changes channel as Up and Down do. In the guide a swipe left10// or right moves the hours shown, a tap picks a program, a tap on the one11// picked watches it or shows what's to come's actions, and holding one12// shows its actions (record, favorite, hide). In Recordings and Podcasts a13// tap on what's picked plays it. A phone held upright is asked to turn14// sideways (style.css).15const touch = { x: 0, y: 0, on: false, held: 0, longDone: false, at: -Infinity };16const TOUCH_SWIPE = 48; // CSS pixels a finger travels for a swipe17const TOUCH_HOLD_MS = 550; // a press this long is a hold18const TOUCH_MOUSE_MS = 1000; // a finger's stand-in mouse events follow it within this1920// A finger's or a pen's pointer: its gestures are a finger's in any21// layout, as on a laptop's touch screen.22const touching = (e) => e.pointerType !== 'mouse';2324// A finger also sends a mouse's events after it, for pages made for one.25// Here they'd act twice: its move is a pointer brought to the screen, which26// brings up the dock and the controls (wireMouse), and two quick taps are a27// double click, which plays what the second tap already played. What a28// finger does is handled here, from its pointer events.29const fromFinger = () => performance.now() - touch.at < TOUCH_MOUSE_MS;30const fingerTap = () => platform === 'touch' || fromFinger();3132function wireTouch() {33 const lastFinger = (e) => { if (touching(e)) touch.at = performance.now(); };34 document.addEventListener('pointerdown', (e) => {35 lastFinger(e);36 if (!touching(e)) return;37 Object.assign(touch, { x: e.clientX, y: e.clientY, on: true, longDone: false });38 clearTimeout(touch.held);39 const cell = e.target.closest && e.target.closest('#guide .g-rows [data-row]');40 if (cell) touch.held = setTimeout(() => { touch.longDone = true; holdCell(cell); }, TOUCH_HOLD_MS);41 }, { capture: true, passive: true });42 document.addEventListener('pointermove', (e) => {43 if (touch.on && Math.hypot(e.clientX - touch.x, e.clientY - touch.y) > 12) clearTimeout(touch.held);44 }, { capture: true, passive: true });45 document.addEventListener('pointercancel', () => { touch.on = false; clearTimeout(touch.held); }, true);46 document.addEventListener('pointerup', (e) => {47 lastFinger(e);48 if (!touch.on || !touching(e)) return;49 touch.on = false;50 clearTimeout(touch.held);51 const dx = e.clientX - touch.x;52 const dy = e.clientY - touch.y;53 if (Math.max(Math.abs(dx), Math.abs(dy)) < TOUCH_SWIPE) return;54 swipe(e.target, Math.abs(dx) > Math.abs(dy) ? (dx > 0 ? 'right' : 'left') : (dy > 0 ? 'down' : 'up'));55 }, { capture: true, passive: true });56 for (const type of ['mousemove', 'dblclick']) {57 document.addEventListener(type, (e) => { if (fromFinger()) e.stopImmediatePropagation(); }, true);58 }59 // A hold's lift isn't also a tap.60 document.addEventListener('click', (e) => {61 if (touch.longDone) {62 touch.longDone = false;63 e.stopPropagation();64 e.preventDefault();65 }66 }, true);67 // On TV a tap on the picture, or on the banner over it, brings up the68 // controls; with them up, a tap anywhere but on a control puts them69 // away, the banner too, as a phone's video players do. Listened for on70 // the picture and the banner themselves, as an iPhone sends a tap's71 // click only to what listens for one, and first (capture), so a tap on72 // the picture elsewhere, which goes back to TV (wireDock), isn't one.73 const tapTV = (e) => {74 if (!fingerTap() || state.view !== 'tv' || document.body.classList.contains('booting')) return;75 if (e.target.closest('button, input, a, .tl, [data-act]')) return;76 if (osd.open) hideBanner(); else openOSD(null);77 };78 for (const el of [stage, $('#banner')]) el.addEventListener('click', tapTV, true);79}8081// swipe acts on a swipe over el: on TV, over the picture or the banner,82// the arrows; in the guide, the hours shown.83function swipe(el, dir) {84 if (state.view === 'tv' && !el.closest('#dock')) {85 if (dir === 'left' || dir === 'right') {86 skip(dir === 'right' ? 30 : -10);87 return openOSD('timeline');88 }89 return step(dir === 'up' ? -1 : 1);90 }91 if (state.view === 'guide' && el.closest('.g-rows') && (dir === 'left' || dir === 'right')) {92 const earliest = floor30(Date.now()) - 30 * MIN;93 state.gStart = Math.max(earliest, state.gStart + (dir === 'left' ? 60 : -60) * MIN);94 state.gTime = Math.max(state.gTime, state.gStart);95 if (state.gTime >= state.gStart + (state.gSpan - 30) * MIN) state.gTime = state.gStart;96 renderGuide();97 }98 return undefined;99}100101// holdCell shows a guide program's actions, picking it first.102function holdCell(cell) {103 state.gRow = Number(cell.dataset.row);104 if (cell.dataset.t) state.gTime = Number(cell.dataset.t);105 renderGuide();106 guideOpenActions();107}108109// tappedAgain reports whether a tap lands on what was already picked: the110// guide's, Recordings' and Podcasts' second tap does it.111const tappedAgain = (was, now) => fingerTap() && was === now;