window.App = window.App || {}; App.storage = App.storage || {}; App.session = App.session || {}; (function() { const { FAVORITES_KEY, FAVORITES_VISIBILITY_KEY, PREFERRED_QUALITY_KEY } = App.constants; // Basic localStorage helpers. App.storage.getConfig = function() { return JSON.parse(localStorage.getItem('config')) || { servers: [] }; }; App.storage.setConfig = function(nextConfig) { localStorage.setItem('config', JSON.stringify(nextConfig)); }; App.storage.getSession = function() { return JSON.parse(localStorage.getItem('session')) || null; }; App.storage.setSession = function(nextSession) { localStorage.setItem('session', JSON.stringify(nextSession)); }; App.storage.getPreferences = function() { return JSON.parse(localStorage.getItem('preferences')) || {}; }; App.storage.setPreferences = function(nextPreferences) { localStorage.setItem('preferences', JSON.stringify(nextPreferences)); }; App.storage.getPreferredQuality = function() { return localStorage.getItem(PREFERRED_QUALITY_KEY) || '1080'; }; App.storage.setPreferredQuality = function(nextQuality) { localStorage.setItem(PREFERRED_QUALITY_KEY, nextQuality); }; App.storage.getServerEntries = function() { const config = App.storage.getConfig(); if (!config.servers || !Array.isArray(config.servers)) return []; return config.servers.map((serverObj) => { const server = Object.keys(serverObj)[0]; return { url: server, data: serverObj[server] || null }; }); }; // Options/session helpers that power channel selection and filters. App.session.serializeOptions = function(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; }; App.session.hydrateOptions = function(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; }; // Builds a pseudo-channel representing a whole channel group, used so the // rest of the app (session, filters, video loading) can treat a selected // group the same way it treats a single channel. App.session.buildGroupChannel = function(group, channels) { if (!group) return null; const knownIds = new Set((channels || []).map((channel) => channel.id)); const channelIds = Array.isArray(group.channelIds) ? group.channelIds.filter((id) => knownIds.has(id)) : []; return { id: `group:${group.id}`, name: group.title || group.id, isGroup: true, groupId: group.id, channelIds: channelIds }; }; // Resolves a stored channel id (which may reference a single channel or a // "group:" pseudo-channel) against a server's status payload. App.session.resolveChannelById = function(serverData, channelId) { if (!serverData || !channelId) return null; const channels = Array.isArray(serverData.channels) ? serverData.channels : []; if (channelId.startsWith('group:')) { const groupId = channelId.slice('group:'.length); const groups = Array.isArray(serverData.channelGroups) ? serverData.channelGroups : []; const group = groups.find((g) => g.id === groupId); return App.session.buildGroupChannel(group, channels); } return channels.find((channel) => channel.id === channelId) || null; }; App.session.savePreference = function(session) { if (!session || !session.server || !session.channel) return; const prefs = App.storage.getPreferences(); const serverPrefs = prefs[session.server] || {}; serverPrefs.channelId = session.channel.id; serverPrefs.optionsByChannel = serverPrefs.optionsByChannel || {}; serverPrefs.optionsByChannel[session.channel.id] = App.session.serializeOptions(session.options); prefs[session.server] = serverPrefs; App.storage.setPreferences(prefs); }; App.session.buildDefaultOptions = function(channel) { const selected = {}; if (!channel || !Array.isArray(channel.options)) return selected; channel.options.forEach((optionGroup) => { if (!optionGroup.options || optionGroup.options.length === 0) return; if (optionGroup.multiSelect) { selected[optionGroup.id] = [optionGroup.options[0]]; } else { selected[optionGroup.id] = optionGroup.options[0]; } }); return selected; }; // Ensures defaults exist and refreshes server status. App.storage.ensureDefaults = async function() { if (!localStorage.getItem('config')) { localStorage.setItem('config', JSON.stringify({ servers: [ { "https://getfigleaf.com": {} }, { "https://hottubapp.io": {} }, { "https://hottub.spacemoehre.de": {} } ] })); } if (!localStorage.getItem('theme')) { localStorage.setItem('theme', 'dark'); } if (!localStorage.getItem(PREFERRED_QUALITY_KEY)) { localStorage.setItem(PREFERRED_QUALITY_KEY, '1080'); } if (!localStorage.getItem(FAVORITES_KEY)) { localStorage.setItem(FAVORITES_KEY, JSON.stringify([])); } if (!localStorage.getItem(FAVORITES_VISIBILITY_KEY)) { localStorage.setItem(FAVORITES_VISIBILITY_KEY, 'true'); } await App.storage.initializeServerStatus(); }; // Fetches server status and keeps the session pointing to a valid channel/options. App.storage.initializeServerStatus = async function() { const config = JSON.parse(localStorage.getItem('config')); if (!config || !config.servers) return; const statusPromises = config.servers.map(async (serverObj) => { const server = Object.keys(serverObj)[0]; try { const response = await fetch(`/api/status`, { method: "POST", body: JSON.stringify({ server: server }), headers: { "Content-Type": "application/json" }, }); const status = await response.json(); serverObj[server] = status; } catch (err) { serverObj[server] = { online: false, channels: [] }; } }); await Promise.all(statusPromises); localStorage.setItem('config', JSON.stringify(config)); const existingSession = App.storage.getSession(); const serverKeys = config.servers.map((serverObj) => Object.keys(serverObj)[0]); if (serverKeys.length === 0) return; const selectedServerKey = existingSession && serverKeys.includes(existingSession.server) ? existingSession.server : serverKeys[0]; const serverEntry = config.servers.find((serverObj) => Object.keys(serverObj)[0] === selectedServerKey); const serverData = serverEntry ? serverEntry[selectedServerKey] : null; if (serverData && serverData.channels && serverData.channels.length > 0) { const prefs = App.storage.getPreferences(); const serverPrefs = prefs[selectedServerKey] || {}; const preferredChannelId = serverPrefs.channelId; const channel = App.session.resolveChannelById(serverData, preferredChannelId) || serverData.channels[0]; const savedOptions = serverPrefs.optionsByChannel ? serverPrefs.optionsByChannel[channel.id] : null; const options = savedOptions ? App.session.hydrateOptions(channel, savedOptions) : App.session.buildDefaultOptions(channel); const sessionData = { server: selectedServerKey, channel: channel, options: options, }; App.storage.setSession(sessionData); App.session.savePreference(sessionData); } }; })();