diff --git a/package.json b/package.json index cec7aca..d2c889e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hdtilt", - "version": "0.4.1", + "version": "0.4.2", "description": "Open-source IPTV player: M3U, Xtream Codes and XMLTV, with a TV guide. Web/PWA, desktop, CLI, TUI, MCP and API.", "license": "MIT", "author": { diff --git a/src/version.js b/src/version.js index 9a797a4..89aaa4e 100644 --- a/src/version.js +++ b/src/version.js @@ -1 +1 @@ -export const VERSION = '0.4.1'; +export const VERSION = '0.4.2'; diff --git a/web/public/app.css b/web/public/app.css index 69454d4..1a690f1 100644 --- a/web/public/app.css +++ b/web/public/app.css @@ -604,18 +604,29 @@ body.nav-on .clock { } /* ---- sheet (setup + settings) ------------------------------------------- */ +/* A column that scrolls from the top: the card is centred by auto margins, + which give way when it is taller than the screen (place-items: center cut + the top of a tall card off where scrolling cannot reach), and the footer + follows it in the flow instead of covering it. */ .sheet { position: absolute; inset: 0; - display: grid; - place-items: center; + display: flex; + flex-direction: column; + align-items: center; background: rgba(5, 8, 12, 0.82); backdrop-filter: blur(6px); z-index: 10; overflow-y: auto; padding: 1.6rem 16px; } +/* Room around a control the remote scrolls to, focus ring included. */ +.sheet :is(input, button, a) { + scroll-margin-block: 1.2rem; +} .card { + flex: none; + margin-block: auto; width: min(100%, 34rem); background: var(--panel-2); border: 1px solid var(--line); @@ -1085,21 +1096,17 @@ body.gated .nav-items button:not(#nav-account) { min-width: 0; } -/* Footer (@profullstack/footer + the pit.world network footer): only while the - sheet is up, docked under it. */ +/* Footer (@profullstack/footer + the pit.world network footer): app.js moves + it into the sheet, after the card, so it scrolls under it and never sits on + the form. Hidden anywhere else. */ .ring-dock { display: none; } -#sheet:not([hidden]) { - padding-bottom: 6rem; -} -#sheet:not([hidden]) ~ .ring-dock { - position: absolute; - left: 0; - right: 0; - bottom: 0; - z-index: 11; +.sheet > .ring-dock { + flex: none; display: block; + align-self: stretch; + margin: 1.6rem -16px -1.6rem; color: var(--text); } .ring-dock .pfs-footer { diff --git a/web/src/app.js b/web/src/app.js index f23486d..5480a90 100644 --- a/web/src/app.js +++ b/web/src/app.js @@ -40,8 +40,12 @@ const isTv = isTvBrowser(navigator.userAgent); } const fsAuto = store.prefs.read('fullscreen', isTv ? 'on' : 'off') === 'on'; let fsDeclined = false; -function autoFullscreen() { +function autoFullscreen(e) { if (!fsAuto || fsDeclined || document.fullscreenElement || !document.fullscreenEnabled) return; + // Not while typing, nor on the account screens: the jump to fullscreen can + // close a TV's on-screen keyboard mid-password, and there is no picture yet. + if (e?.target?.closest?.('input, textarea, select')) return; + if (S.mode === 'sheet' && $('sheet').dataset.screen === 'account') return; if (navigator.userActivation && !navigator.userActivation.isActive) return; document.documentElement.requestFullscreen?.().catch(() => {}); } @@ -784,7 +788,7 @@ function soundOn(e) { } for (const ev of ['pointerdown', 'keydown']) addEventListener(ev, soundOn, true); -addEventListener('keydown', (e) => e.key !== 'Escape' && autoFullscreen(), true); +addEventListener('keydown', (e) => e.key !== 'Escape' && autoFullscreen(e), true); addEventListener('pointerdown', autoFullscreen, true); // Back or Esc on a remote leaves fullscreen without asking us: that counts as a no. document.addEventListener('fullscreenchange', () => { @@ -1511,6 +1515,12 @@ $('guide-grid').addEventListener( /* ------------------------------------------------------- setup + settings */ +// The footer scrolls with the sheet, under the card, rather than sitting on +// top of it: on a 540px-tall TV a docked footer covered the sign-in button. +// Every render replaces the sheet's markup, so the node is re-appended after. +const ringDock = document.querySelector('.ring-dock'); +const dockFooter = () => ringDock && $('sheet').append(ringDock); + function openSetup(first = true) { closeAll(); S.mode = 'sheet'; @@ -1538,6 +1548,7 @@ function openSetup(first = true) {

${api.serverInfo.public ? 'Your playlist and login are kept in this browser only. Streams this page cannot reach directly pass through hdtilt.com. Prefer nothing in between? Get the desktop app or CLI.' : 'Running locally: nothing leaves this machine.'} Open source: github.com/profullstack/hdtilt.com

`; + dockFooter(); let tab = 'm3u'; sheet.querySelectorAll('[data-tab]').forEach((b) => { b.onclick = () => { @@ -1633,6 +1644,7 @@ function openSettings() {

↑ ↓ change channel · OK channel list · Space play/pause · + − volume · ◀ groups · G guide · ☆ or F favorite · 0–9 channel number · ⌫ last channel · / or ↑ from the top row: filter, and the guide’s ☆ Favorites only · M mute · Esc back

hdtilt ${esc(api.serverInfo.version)} · open source, MIT · source · CLI, TUI, MCP and API included

`; + dockFooter(); $('s-add').onclick = () => go('/settings/add'); $('s-close').onclick = () => back(); $('s-copy')?.addEventListener('click', () => navigator.clipboard?.writeText(S.playing.url)); @@ -1779,6 +1791,36 @@ function homeKey(e) { return true; } +/** + * Up and Down on every other sheet (sign-in, settings): one control to the + * next, fields included, since a text field keeps the arrows to itself and a + * remote has no Tab. Up from the first control goes to the nav. + */ +function sheetKey(e) { + const k = e.key; + if ((k !== 'ArrowUp' && k !== 'ArrowDown') || onHome()) return false; + const t = e.target; + if (t instanceof HTMLTextAreaElement || t instanceof HTMLSelectElement || t.type === 'range') return false; + const all = [ + ...$('sheet').querySelectorAll( + 'input:not([type=hidden]):not([disabled]), button:not([disabled]), a[href], select, textarea', + ), + ].filter((x) => x.getClientRects().length); + if (!all.length) return false; + const i = all.indexOf(t); + if (k === 'ArrowUp' && i <= 0) { + if (t instanceof HTMLInputElement) t.blur(); + focusNav(); + } else { + const next = all[k === 'ArrowDown' ? Math.min(all.length - 1, i + 1) : i - 1]; + next.focus(); + // focus() leaves a half-visible control where it is. + next.scrollIntoView({ block: 'nearest' }); + } + e.preventDefault(); + return true; +} + /* ------------------------------------------------------------- account */ function sheet(screen, html) { @@ -1789,6 +1831,7 @@ function sheet(screen, html) { delete sh.dataset.ad; sh.hidden = false; sh.innerHTML = `
${html}
`; + dockFooter(); syncNav(); setTimeout(() => sh.querySelector('input')?.focus(), 50); return sh; @@ -2415,6 +2458,7 @@ addEventListener('keydown', (e) => { } if (S.mode === 'sheet') { if ($('sheet').dataset.screen === 'home' && homeKey(e)) return; + if (sheetKey(e)) return; if (e.key === 'ArrowUp' && !(e.target instanceof HTMLInputElement) && e.target.closest?.('.card') == null) return focusNav(); if (e.key === 'Escape' || e.key === 'GoBack' || BACK_CODES.has(e.keyCode)) {