Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
2 changes: 1 addition & 1 deletion src/version.js
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const VERSION = '0.4.1';
export const VERSION = '0.4.2';
33 changes: 20 additions & 13 deletions web/public/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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 {
Expand Down
48 changes: 46 additions & 2 deletions web/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {});
}
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -1538,6 +1548,7 @@ function openSetup(first = true) {
</form>
<p class="note">${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 <a href="https://github.com/profullstack/hdtilt.com#install" target="_blank" rel="noopener">desktop app or CLI</a>.' : 'Running locally: nothing leaves this machine.'} Open source: <a href="https://github.com/profullstack/hdtilt.com" target="_blank" rel="noopener">github.com/profullstack/hdtilt.com</a></p>
</div>`;
dockFooter();
let tab = 'm3u';
sheet.querySelectorAll('[data-tab]').forEach((b) => {
b.onclick = () => {
Expand Down Expand Up @@ -1633,6 +1644,7 @@ function openSettings() {
<p class="sub">↑ ↓ 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</p>
<p class="note">hdtilt ${esc(api.serverInfo.version)} · open source, MIT · <a href="https://github.com/profullstack/hdtilt.com" target="_blank" rel="noopener">source</a> · CLI, TUI, MCP and API included</p>
</div>`;
dockFooter();
$('s-add').onclick = () => go('/settings/add');
$('s-close').onclick = () => back();
$('s-copy')?.addEventListener('click', () => navigator.clipboard?.writeText(S.playing.url));
Expand Down Expand Up @@ -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) {
Expand All @@ -1789,6 +1831,7 @@ function sheet(screen, html) {
delete sh.dataset.ad;
sh.hidden = false;
sh.innerHTML = `<div class="card">${html}</div>`;
dockFooter();
syncNav();
setTimeout(() => sh.querySelector('input')?.focus(), 50);
return sh;
Expand Down Expand Up @@ -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)) {
Expand Down
Loading