From f71d8e3ee1f31e06cf167f585bc61801fa0e71fb Mon Sep 17 00:00:00 2001 From: Simon Date: Sun, 8 Feb 2026 15:48:45 +0000 Subject: [PATCH] visual upgrade --- frontend/app.js | 151 ++++++++++++++++++++++++++++++++++++--------- frontend/style.css | 13 +++- 2 files changed, 132 insertions(+), 32 deletions(-) diff --git a/frontend/app.js b/frontend/app.js index 96fd7c1..542275b 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -58,31 +58,29 @@ async function InitializeServerStatus() { await Promise.all(statusPromises); localStorage.setItem('config', JSON.stringify(config)); - const firstServerKey = Object.keys(config.servers[0])[0]; - const serverData = config.servers[0][firstServerKey]; + const existingSession = getSession(); + const serverKeys = config.servers.map((serverObj) => Object.keys(serverObj)[0]); + const selectedServerKey = existingSession && serverKeys.includes(existingSession.server) + ? existingSession.server + : serverKeys[0]; + const serverData = config.servers.find((serverObj) => Object.keys(serverObj)[0] === selectedServerKey)[selectedServerKey]; if (serverData.channels && serverData.channels.length > 0) { - const channel = serverData.channels[0]; - let options = {}; - - if (channel.options) { - channel.options.forEach(element => { - // Ensure the options structure matches your API expectations - if (element.multiSelect) { - options[element.id] = element.options.length > 0 ? [element.options[0]] : []; - } else { - options[element.id] = element.options[0]; - } - }); - } + const prefs = getPreferences(); + const serverPrefs = prefs[selectedServerKey] || {}; + const preferredChannelId = serverPrefs.channelId; + const channel = serverData.channels.find((ch) => ch.id === preferredChannelId) || serverData.channels[0]; + const savedOptions = serverPrefs.optionsByChannel ? serverPrefs.optionsByChannel[channel.id] : null; + const options = savedOptions ? hydrateOptions(channel, savedOptions) : buildDefaultOptions(channel); const sessionData = { - server: firstServerKey, + server: selectedServerKey, channel: channel, options: options, }; - localStorage.setItem('session', JSON.stringify(sessionData)); + setSession(sessionData); + savePreference(sessionData); } } @@ -140,14 +138,15 @@ function renderVideos(videos) { const items = videos && Array.isArray(videos.items) ? videos.items : []; items.forEach(v => { if (renderedVideoIds.has(v.id)) return; - + const card = document.createElement('div'); card.className = 'video-card'; - const durationText = v.duration === 0 ? '' : ` • ${v.duration}s`; + const durationText = formatDuration(v.duration); card.innerHTML = ` ${v.title}

${v.title}

-

${v.channel}${durationText}

+

${v.channel}

+ ${durationText ? `

${durationText}

` : ''} `; card.onclick = () => openPlayer(v.url); grid.appendChild(card); @@ -155,6 +154,17 @@ function renderVideos(videos) { }); } +function formatDuration(seconds) { + if (!seconds || seconds <= 0) return ''; + const totalSeconds = Math.floor(seconds); + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + if (hours > 0) { + return `${hours}h ${String(minutes).padStart(2, '0')}m`; + } + return `${minutes}m`; +} + // 4. Initialization (Run this last) async function initApp() { // Clear old data if you want a fresh start every refresh @@ -270,6 +280,14 @@ function setSession(nextSession) { localStorage.setItem('session', JSON.stringify(nextSession)); } +function getPreferences() { + return JSON.parse(localStorage.getItem('preferences')) || {}; +} + +function setPreferences(nextPreferences) { + localStorage.setItem('preferences', JSON.stringify(nextPreferences)); +} + function getServerEntries() { const config = getConfig(); if (!config.servers || !Array.isArray(config.servers)) return []; @@ -291,6 +309,48 @@ async function refreshServerStatus() { renderMenu(); } +function serializeOptions(options) { + const serialized = {}; + Object.entries(options || {}).forEach(([key, value]) => { + if (Array.isArray(value)) { + serialized[key] = value.map((entry) => entry.id); + } else if (value && value.id) { + serialized[key] = value.id; + } + }); + return serialized; +} + +function hydrateOptions(channel, savedOptions) { + const hydrated = {}; + if (!channel || !Array.isArray(channel.options)) return hydrated; + const saved = savedOptions || {}; + channel.options.forEach((optionGroup) => { + const allOptions = optionGroup.options || []; + const savedValue = saved[optionGroup.id]; + if (optionGroup.multiSelect) { + const selectedIds = Array.isArray(savedValue) ? savedValue : []; + const selected = allOptions.filter((opt) => selectedIds.includes(opt.id)); + hydrated[optionGroup.id] = selected.length > 0 ? selected : allOptions.slice(0, 1); + } else { + const selected = allOptions.find((opt) => opt.id === savedValue) || allOptions[0]; + if (selected) hydrated[optionGroup.id] = selected; + } + }); + return hydrated; +} + +function savePreference(session) { + if (!session || !session.server || !session.channel) return; + const prefs = getPreferences(); + const serverPrefs = prefs[session.server] || {}; + serverPrefs.channelId = session.channel.id; + serverPrefs.optionsByChannel = serverPrefs.optionsByChannel || {}; + serverPrefs.optionsByChannel[session.channel.id] = serializeOptions(session.options); + prefs[session.server] = serverPrefs; + setPreferences(prefs); +} + function buildDefaultOptions(channel) { const selected = {}; if (!channel || !Array.isArray(channel.options)) return selected; @@ -355,21 +415,33 @@ function renderMenu() { const channels = selectedServer && selectedServer.data && selectedServer.data.channels ? selectedServer.data.channels : []; - const nextChannel = channels.length > 0 ? channels[0] : null; + const prefs = getPreferences(); + const serverPrefs = prefs[selectedServerUrl] || {}; + const preferredChannel = channels.find((channel) => channel.id === serverPrefs.channelId) || null; + const nextChannel = preferredChannel || (channels.length > 0 ? channels[0] : null); + const savedOptions = nextChannel && serverPrefs.optionsByChannel ? + serverPrefs.optionsByChannel[nextChannel.id] : + null; const nextSession = { server: selectedServerUrl, channel: nextChannel, - options: nextChannel ? buildDefaultOptions(nextChannel) : {} + options: nextChannel ? (savedOptions ? hydrateOptions(nextChannel, savedOptions) : buildDefaultOptions(nextChannel)) : {} }; setSession(nextSession); + savePreference(nextSession); renderMenu(); resetAndReload(); }; const activeServer = serverEntries.find((entry) => entry.url === (session && session.server)); const availableChannels = activeServer && activeServer.data && activeServer.data.channels ? - activeServer.data.channels : + [...activeServer.data.channels] : []; + availableChannels.sort((a, b) => { + const nameA = (a.name || a.id || '').toLowerCase(); + const nameB = (b.name || b.id || '').toLowerCase(); + return nameA.localeCompare(nameB); + }); channelSelect.innerHTML = ""; availableChannels.forEach((channel) => { @@ -386,12 +458,18 @@ function renderMenu() { channelSelect.onchange = () => { const selectedId = channelSelect.value; const nextChannel = availableChannels.find((channel) => channel.id === selectedId) || null; + const prefs = getPreferences(); + const serverPrefs = prefs[session.server] || {}; + const savedOptions = nextChannel && serverPrefs.optionsByChannel ? + serverPrefs.optionsByChannel[nextChannel.id] : + null; const nextSession = { server: session.server, channel: nextChannel, - options: nextChannel ? buildDefaultOptions(nextChannel) : {} + options: nextChannel ? (savedOptions ? hydrateOptions(nextChannel, savedOptions) : buildDefaultOptions(nextChannel)) : {} }; setSession(nextSession); + savePreference(nextSession); renderMenu(); resetAndReload(); }; @@ -426,6 +504,11 @@ function renderMenu() { return key !== entry.url; }); setConfig(config); + const prefs = getPreferences(); + if (prefs[entry.url]) { + delete prefs[entry.url]; + setPreferences(prefs); + } const remaining = getServerEntries(); if (remaining.length === 0) { @@ -433,12 +516,17 @@ function renderMenu() { } else { const nextServerUrl = remaining[0].url; const nextServer = remaining[0]; - const nextChannel = nextServer.data && nextServer.data.channels ? nextServer.data.channels[0] : null; - setSession({ + const serverPrefs = prefs[nextServerUrl] || {}; + const channels = nextServer.data && nextServer.data.channels ? nextServer.data.channels : []; + const nextChannel = channels.find((channel) => channel.id === serverPrefs.channelId) || channels[0] || null; + const savedOptions = nextChannel && serverPrefs.optionsByChannel ? serverPrefs.optionsByChannel[nextChannel.id] : null; + const nextSession = { server: nextServerUrl, channel: nextChannel, - options: nextChannel ? buildDefaultOptions(nextChannel) : {} - }); + options: nextChannel ? (savedOptions ? hydrateOptions(nextChannel, savedOptions) : buildDefaultOptions(nextChannel)) : {} + }; + setSession(nextSession); + savePreference(nextSession); } await refreshServerStatus(); @@ -475,11 +563,13 @@ function renderMenu() { const nextChannel = addedEntry && addedEntry.data && addedEntry.data.channels ? addedEntry.data.channels[0] : null; - setSession({ + const nextSession = { server: normalized, channel: nextChannel, options: nextChannel ? buildDefaultOptions(nextChannel) : {} - }); + }; + setSession(nextSession); + savePreference(nextSession); } renderMenu(); resetAndReload(); @@ -550,6 +640,7 @@ function renderFilters(container, session) { } setSession(nextSession); + savePreference(nextSession); resetAndReload(); }; diff --git a/frontend/style.css b/frontend/style.css index 8e631e8..a07c732 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -487,9 +487,9 @@ body.theme-light .setting-item select option { .video-card img { width: 100%; - aspect-ratio: 16 / 9; - object-fit: cover; + height: auto; display: block; + background: var(--bg-tertiary); } .video-card h4 { @@ -513,6 +513,15 @@ body.theme-light .setting-item select option { margin: 0; } +.video-meta { + padding-bottom: 4px; +} + +.video-duration { + color: var(--text-primary); + opacity: 0.8; +} + /* Modal */ .modal { display: none;