From b9e3fa717b39c028bbc5b29acd3f7f1d7e1a135f Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Mon, 26 Oct 2026 01:12:56 +0000 Subject: [PATCH] 0.4.2: on a TV the footer no longer covers the sign-in form On a 960x540 TV screen the footer dock (webring row, badge and the network row, about 190px) was pinned over the bottom of the sheet with only 6rem reserved, so the password field and the Sign in / Create account button sat under it and clicks landed on the footer. - The footer moves into the sheet after the card and scrolls with it. - The sheet centres the card with auto margins instead of place-items, so a card taller than the screen scrolls from its top instead of losing it. - Up/Down on the sign-in and settings sheets step through every control, fields included (a remote has no Tab), and scroll it into view; Up from the first goes to the nav. - Fullscreen-on-first-key no longer fires from a text field or the account screens, where it could close a TV's on-screen keyboard mid-password. Co-Authored-By: Claude Opus 5.5 --- package.json | 2 +- src/version.js | 2 +- web/public/app.css | 33 ++++++++++++++++++------------- web/src/app.js | 48 ++++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 68 insertions(+), 17 deletions(-) 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)) {