Compose for TV UI, generic + Dispatcharr sources, device-flow login, favorites, stream profiles, richer remote
- Compose for TV scaffold: D-pad app shell (Live/Einstellungen), channel list with focus handling, fullscreen player with auto-hiding overlay; zapping via channel/D-pad keys (closes #2) - Generic source end-to-end: M3U + XMLTV configurable on the TV, Now/Next with progress in the list, channel cache (closes #3) - OIDC device-flow login: server URL is the only input, issuer/client id come from the fork's status endpoint; QR + user code screen, silent refresh, logout (closes #4) - Dispatcharr source via Bearer API: channels, groups, EPG grid; switchable against the generic source (closes #5) - Per-user favorites: star via long-press, favorites filter, backend-synced (closes #6) - Stream profile selection (Standard/Passthrough/audiofix/720p from the backend profile list), applied per stream URL (closes #7) - Phone remote: Now/Next lines, favorites star + filter over the WebSocket protocol (closes #8) Device verification pending (TV currently in use). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -241,7 +241,7 @@
|
||||
const state = {
|
||||
ws: null, connected: false, authorized: false,
|
||||
status: { state: 'idle', channel: '', group: '', live: false, volume: 0.5 },
|
||||
channels: [], playlistUrl: '',
|
||||
channels: [], playlistUrl: '', extras: { nowNext: [], favorites: [], favoritesSupported: false }, favOnly: false,
|
||||
retryDelay: 1000, volumeDragging: false, searchTerm: '',
|
||||
};
|
||||
|
||||
@@ -316,6 +316,7 @@
|
||||
$('tv-name').textContent = msg.device || 'TV';
|
||||
if (msg.status) { state.status = msg.status; }
|
||||
if (msg.channels) { state.channels = msg.channels; }
|
||||
if (msg.extras) { state.extras = msg.extras; }
|
||||
state.playlistUrl = msg.playlistUrl || '';
|
||||
$('pair-error').textContent = '';
|
||||
showView('view-remote');
|
||||
@@ -329,6 +330,7 @@
|
||||
case 'channels':
|
||||
state.channels = msg.channels || [];
|
||||
state.playlistUrl = msg.playlistUrl || '';
|
||||
if (msg.extras) { state.extras = msg.extras; }
|
||||
renderChannels();
|
||||
break;
|
||||
case 'toast':
|
||||
@@ -383,8 +385,24 @@
|
||||
$('setup').classList.add('open');
|
||||
return;
|
||||
}
|
||||
const favSet = new Set(state.extras.favorites || []);
|
||||
const favOn = state.extras.favoritesSupported && state.favOnly;
|
||||
const shown = favOn ? filtered.filter((c) => favSet.has(c.backendId)) : filtered;
|
||||
if (state.extras.favoritesSupported) {
|
||||
const bar = document.createElement('div');
|
||||
bar.style.cssText = 'display:flex;gap:8px;padding:2px 12px 10px';
|
||||
[['Alle', false], ['★ Favoriten', true]].forEach(([label, val]) => {
|
||||
const chip = document.createElement('button');
|
||||
chip.textContent = label;
|
||||
chip.style.cssText = 'padding:7px 14px;border-radius:999px;font-size:12px;background:' +
|
||||
(state.favOnly === val ? 'rgba(95,212,196,.12);color:#5fd4c4' : '#121418;color:#9aa0a8');
|
||||
chip.addEventListener('click', () => { state.favOnly = val; renderChannels(); });
|
||||
bar.appendChild(chip);
|
||||
});
|
||||
list.appendChild(bar);
|
||||
}
|
||||
const frag = document.createDocumentFragment();
|
||||
filtered.slice(0, 500).forEach((c) => {
|
||||
shown.slice(0, 500).forEach((c) => {
|
||||
const idx = state.channels.indexOf(c);
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'chan' + (c.url === state.status.url || c.name === state.status.channel ? ' current' : '');
|
||||
@@ -403,12 +421,38 @@
|
||||
const name = document.createElement('span');
|
||||
name.className = 'name'; name.textContent = c.name;
|
||||
meta.appendChild(name);
|
||||
if (c.group) {
|
||||
const info = (state.extras.nowNext || [])[idx];
|
||||
if (info && info.now) {
|
||||
const now = document.createElement('span');
|
||||
now.className = 'grp';
|
||||
const pct = info.stop > info.start
|
||||
? Math.round(100 * (Date.now() - info.start) / (info.stop - info.start)) : 0;
|
||||
now.textContent = info.now;
|
||||
meta.appendChild(now);
|
||||
const barWrap = document.createElement('span');
|
||||
barWrap.style.cssText = 'display:block;height:2px;border-radius:1px;background:#1e2126;margin-top:4px;max-width:180px';
|
||||
const bar = document.createElement('span');
|
||||
bar.style.cssText = 'display:block;height:2px;border-radius:1px;background:#5fd4c4;width:' +
|
||||
Math.max(0, Math.min(100, pct)) + '%';
|
||||
barWrap.appendChild(bar);
|
||||
meta.appendChild(barWrap);
|
||||
} else if (c.group) {
|
||||
const grp = document.createElement('span');
|
||||
grp.className = 'grp'; grp.textContent = c.group;
|
||||
meta.appendChild(grp);
|
||||
}
|
||||
btn.appendChild(num); btn.appendChild(meta);
|
||||
if (state.extras.favoritesSupported && c.backendId) {
|
||||
const star = document.createElement('span');
|
||||
const isFav = favSet.has(c.backendId);
|
||||
star.textContent = isFav ? '★' : '☆';
|
||||
star.style.cssText = 'font-size:20px;padding:8px;color:' + (isFav ? '#5fd4c4' : '#6b717a');
|
||||
star.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
send({ type: 'set_favorite', id: c.backendId });
|
||||
});
|
||||
btn.appendChild(star);
|
||||
}
|
||||
btn.addEventListener('click', () => playChannel(c));
|
||||
frag.appendChild(btn);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user