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.channel}${durationText}
+ + ${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;