Files
jacuzzi/frontend/js/ui.js
2026-02-09 14:50:17 +00:00

521 lines
22 KiB
JavaScript

window.App = window.App || {};
App.ui = App.ui || {};
(function() {
const state = App.state;
App.ui.applyTheme = function() {
const theme = localStorage.getItem('theme') || 'dark';
document.body.classList.toggle('theme-light', theme === 'light');
const select = document.getElementById('theme-select');
if (select) select.value = theme;
};
// Toast helper for playback + network errors.
App.ui.showError = function(message) {
const toast = document.getElementById('error-toast');
const text = document.getElementById('error-toast-text');
if (!toast || !text) return;
text.textContent = message;
toast.classList.add('show');
if (state.errorToastTimer) {
clearTimeout(state.errorToastTimer);
}
state.errorToastTimer = setTimeout(() => {
toast.classList.remove('show');
}, 4000);
};
App.ui.showInfo = function(video) {
const modal = document.getElementById('info-modal');
if (!modal) return;
const title = document.getElementById('info-title');
const list = document.getElementById('info-list');
const empty = document.getElementById('info-empty');
const data = video && video.meta ? video.meta : video;
const titleText = data && data.title ? data.title : 'Video Info';
if (title) title.textContent = titleText;
if (list) {
list.innerHTML = "";
}
let hasRows = false;
if (data && typeof data === 'object') {
Object.entries(data).forEach(([key, value]) => {
if (!list) return;
const row = document.createElement('div');
row.className = 'info-row';
const label = document.createElement('span');
label.className = 'info-label';
label.textContent = key;
let valueNode;
if (value && typeof value === 'object') {
valueNode = document.createElement('pre');
valueNode.className = 'info-json';
valueNode.textContent = JSON.stringify(value, null, 2);
} else {
valueNode = document.createElement('span');
valueNode.className = 'info-value';
valueNode.textContent = value === undefined || value === null || value === '' ? '—' : String(value);
}
row.appendChild(label);
row.appendChild(valueNode);
list.appendChild(row);
hasRows = true;
});
}
if (empty) {
empty.style.display = hasRows ? 'none' : 'block';
}
modal.classList.add('open');
modal.setAttribute('aria-hidden', 'false');
};
App.ui.closeInfo = function() {
const modal = document.getElementById('info-modal');
if (!modal) return;
modal.classList.remove('open');
modal.setAttribute('aria-hidden', 'true');
};
// Drawer controls shared by the inline HTML handlers.
App.ui.closeDrawers = function() {
const menuDrawer = document.getElementById('drawer-menu');
const settingsDrawer = document.getElementById('drawer-settings');
const overlay = document.getElementById('overlay');
const menuBtn = document.querySelector('.menu-toggle');
const settingsBtn = document.querySelector('.settings-toggle');
if (menuDrawer) menuDrawer.classList.remove('open');
if (settingsDrawer) settingsDrawer.classList.remove('open');
if (overlay) overlay.classList.remove('open');
if (menuBtn) menuBtn.classList.remove('active');
if (settingsBtn) settingsBtn.classList.remove('active');
document.body.classList.remove('drawer-open');
};
App.ui.toggleDrawer = function(type) {
const menuDrawer = document.getElementById('drawer-menu');
const settingsDrawer = document.getElementById('drawer-settings');
const overlay = document.getElementById('overlay');
const menuBtn = document.querySelector('.menu-toggle');
const settingsBtn = document.querySelector('.settings-toggle');
const isMenu = type === 'menu';
const targetDrawer = isMenu ? menuDrawer : settingsDrawer;
const otherDrawer = isMenu ? settingsDrawer : menuDrawer;
const targetBtn = isMenu ? menuBtn : settingsBtn;
const otherBtn = isMenu ? settingsBtn : menuBtn;
if (!targetDrawer || !overlay) return;
const willOpen = !targetDrawer.classList.contains('open');
if (otherDrawer) otherDrawer.classList.remove('open');
if (otherBtn) otherBtn.classList.remove('active');
if (willOpen) {
targetDrawer.classList.add('open');
if (targetBtn) targetBtn.classList.add('active');
overlay.classList.add('open');
document.body.classList.add('drawer-open');
} else {
App.ui.closeDrawers();
}
};
// Settings + menu rendering.
App.ui.renderMenu = function() {
const session = App.storage.getSession();
const serverEntries = App.storage.getServerEntries();
const sourceSelect = document.getElementById('source-select');
const channelSelect = document.getElementById('channel-select');
const filtersContainer = document.getElementById('filters-container');
const sourcesList = document.getElementById('sources-list');
const addSourceBtn = document.getElementById('add-source-btn');
const sourceInput = document.getElementById('source-input');
const reloadChannelBtn = document.getElementById('reload-channel-btn');
const favoritesToggle = document.getElementById('favorites-toggle');
if (!sourceSelect || !channelSelect || !filtersContainer) return;
sourceSelect.innerHTML = "";
serverEntries.forEach((entry) => {
const option = document.createElement('option');
option.value = entry.url;
option.textContent = entry.url;
sourceSelect.appendChild(option);
});
if (session && session.server) {
sourceSelect.value = session.server;
}
sourceSelect.onchange = () => {
const selectedServerUrl = sourceSelect.value;
const selectedServer = serverEntries.find((entry) => entry.url === selectedServerUrl);
const channels = selectedServer && selectedServer.data && selectedServer.data.channels ?
selectedServer.data.channels :
[];
const prefs = App.storage.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 ? (savedOptions ? App.session.hydrateOptions(nextChannel, savedOptions) : App.session.buildDefaultOptions(nextChannel)) : {}
};
App.storage.setSession(nextSession);
App.session.savePreference(nextSession);
App.ui.renderMenu();
App.videos.resetAndReload();
};
const activeServer = serverEntries.find((entry) => entry.url === (session && session.server));
const availableChannels = activeServer && activeServer.data && 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) => {
const option = document.createElement('option');
option.value = channel.id;
option.textContent = channel.name || channel.id;
channelSelect.appendChild(option);
});
if (session && session.channel) {
channelSelect.value = session.channel.id;
}
channelSelect.onchange = () => {
const selectedId = channelSelect.value;
const nextChannel = availableChannels.find((channel) => channel.id === selectedId) || null;
const prefs = App.storage.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 ? (savedOptions ? App.session.hydrateOptions(nextChannel, savedOptions) : App.session.buildDefaultOptions(nextChannel)) : {}
};
App.storage.setSession(nextSession);
App.session.savePreference(nextSession);
App.ui.renderMenu();
App.videos.resetAndReload();
};
App.ui.renderFilters(filtersContainer, session);
const themeSelect = document.getElementById('theme-select');
if (themeSelect) {
themeSelect.onchange = () => {
const nextTheme = themeSelect.value === 'light' ? 'light' : 'dark';
localStorage.setItem('theme', nextTheme);
App.ui.applyTheme();
};
}
if (favoritesToggle) {
favoritesToggle.checked = App.favorites.isVisible();
favoritesToggle.onchange = () => {
App.favorites.setVisible(favoritesToggle.checked);
App.favorites.renderBar();
};
}
if (sourcesList) {
sourcesList.innerHTML = "";
serverEntries.forEach((entry) => {
const row = document.createElement('div');
row.className = 'source-item';
const text = document.createElement('span');
text.textContent = entry.url;
const removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.textContent = 'Remove';
removeBtn.onclick = async () => {
const config = App.storage.getConfig();
config.servers = (config.servers || []).filter((serverObj) => {
const key = Object.keys(serverObj)[0];
return key !== entry.url;
});
App.storage.setConfig(config);
const prefs = App.storage.getPreferences();
if (prefs[entry.url]) {
delete prefs[entry.url];
App.storage.setPreferences(prefs);
}
const remaining = App.storage.getServerEntries();
if (remaining.length === 0) {
localStorage.removeItem('session');
} else {
const nextServerUrl = remaining[0].url;
const nextServer = remaining[0];
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 ? (savedOptions ? App.session.hydrateOptions(nextChannel, savedOptions) : App.session.buildDefaultOptions(nextChannel)) : {}
};
App.storage.setSession(nextSession);
App.session.savePreference(nextSession);
}
await App.storage.initializeServerStatus();
App.videos.resetAndReload();
App.ui.renderMenu();
};
row.appendChild(text);
row.appendChild(removeBtn);
sourcesList.appendChild(row);
});
}
if (addSourceBtn && sourceInput) {
addSourceBtn.onclick = async () => {
const raw = sourceInput.value.trim();
if (!raw) return;
const normalized = raw.endsWith('/') ? raw.slice(0, -1) : raw;
const config = App.storage.getConfig();
const exists = (config.servers || []).some((serverObj) => Object.keys(serverObj)[0] === normalized);
if (!exists) {
config.servers = config.servers || [];
config.servers.push({
[normalized]: {}
});
App.storage.setConfig(config);
sourceInput.value = '';
await App.storage.initializeServerStatus();
const session = App.storage.getSession();
if (!session || session.server !== normalized) {
const entries = App.storage.getServerEntries();
const addedEntry = entries.find((entry) => entry.url === normalized);
const nextChannel = addedEntry && addedEntry.data && addedEntry.data.channels ?
addedEntry.data.channels[0] :
null;
const nextSession = {
server: normalized,
channel: nextChannel,
options: nextChannel ? App.session.buildDefaultOptions(nextChannel) : {}
};
App.storage.setSession(nextSession);
App.session.savePreference(nextSession);
}
App.ui.renderMenu();
App.videos.resetAndReload();
}
};
}
if (reloadChannelBtn) {
reloadChannelBtn.onclick = () => {
App.videos.resetAndReload();
};
}
};
App.ui.renderFilters = function(container, session) {
container.innerHTML = "";
if (!session || !session.channel || !Array.isArray(session.channel.options)) {
const empty = document.createElement('div');
empty.className = 'filters-empty';
empty.textContent = 'No filters available for this channel.';
container.appendChild(empty);
return;
}
session.channel.options.forEach((optionGroup) => {
const wrapper = document.createElement('div');
wrapper.className = 'setting-item';
const labelRow = document.createElement('div');
labelRow.className = 'setting-label-row';
const label = document.createElement('label');
label.textContent = optionGroup.title || optionGroup.id;
labelRow.appendChild(label);
const options = optionGroup.options || [];
const currentSelection = session.options ? session.options[optionGroup.id] : null;
if (optionGroup.multiSelect) {
const actionBtn = document.createElement('button');
actionBtn.type = 'button';
actionBtn.className = 'btn-link';
const list = document.createElement('div');
list.className = 'multi-select';
const selectedIds = new Set(
Array.isArray(currentSelection)
? currentSelection.map((item) => item.id)
: []
);
const updateActionLabel = () => {
const allChecked = options.length > 0 &&
Array.from(list.querySelectorAll('input[type="checkbox"]'))
.every((cb) => cb.checked);
actionBtn.textContent = allChecked ? 'Deselect all' : 'Select all';
actionBtn.disabled = options.length === 0;
};
options.forEach((opt) => {
const item = document.createElement('label');
item.className = 'multi-select-item';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.value = opt.id;
checkbox.checked = selectedIds.has(opt.id);
const text = document.createElement('span');
text.textContent = opt.title || opt.id;
checkbox.onchange = () => {
const nextSession = App.storage.getSession();
if (!nextSession || !nextSession.channel) return;
const selected = [];
list.querySelectorAll('input[type="checkbox"]').forEach((cb) => {
if (cb.checked) {
const found = options.find((item) => item.id === cb.value);
if (found) selected.push(found);
}
});
nextSession.options[optionGroup.id] = selected;
App.storage.setSession(nextSession);
App.session.savePreference(nextSession);
App.videos.resetAndReload();
updateActionLabel();
};
item.appendChild(checkbox);
item.appendChild(text);
list.appendChild(item);
});
updateActionLabel();
actionBtn.onclick = () => {
const checkboxes = Array.from(list.querySelectorAll('input[type="checkbox"]'));
const allChecked = checkboxes.length > 0 && checkboxes.every((cb) => cb.checked);
checkboxes.forEach((cb) => {
cb.checked = !allChecked;
});
const nextSession = App.storage.getSession();
if (!nextSession || !nextSession.channel) return;
const selected = [];
if (!allChecked) {
options.forEach((opt) => selected.push(opt));
}
nextSession.options[optionGroup.id] = selected;
App.storage.setSession(nextSession);
App.session.savePreference(nextSession);
App.videos.resetAndReload();
updateActionLabel();
};
labelRow.appendChild(actionBtn);
wrapper.appendChild(labelRow);
wrapper.appendChild(list);
container.appendChild(wrapper);
return;
}
const select = document.createElement('select');
options.forEach((opt) => {
const option = document.createElement('option');
option.value = opt.id;
option.textContent = opt.title || opt.id;
select.appendChild(option);
});
if (currentSelection && currentSelection.id) {
select.value = currentSelection.id;
}
select.onchange = () => {
const nextSession = App.storage.getSession();
if (!nextSession || !nextSession.channel) return;
const selected = options.find((item) => item.id === select.value);
if (selected) {
nextSession.options[optionGroup.id] = selected;
}
App.storage.setSession(nextSession);
App.session.savePreference(nextSession);
App.videos.resetAndReload();
};
wrapper.appendChild(labelRow);
wrapper.appendChild(select);
container.appendChild(wrapper);
});
};
// Expose inline handlers + keyboard shortcuts.
App.ui.bindGlobalHandlers = function() {
window.toggleDrawer = App.ui.toggleDrawer;
window.closeDrawers = App.ui.closeDrawers;
window.closePlayer = App.player.close;
window.handleSearch = App.videos.handleSearch;
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
App.ui.closeDrawers();
App.ui.closeInfo();
App.videos.closeAllMenus();
}
});
document.addEventListener('click', () => {
App.videos.closeAllMenus();
});
const infoModal = document.getElementById('info-modal');
if (infoModal) {
infoModal.addEventListener('click', (event) => {
if (event.target === infoModal) {
App.ui.closeInfo();
}
});
}
const infoClose = document.getElementById('info-close');
if (infoClose) {
infoClose.addEventListener('click', () => {
App.ui.closeInfo();
});
}
};
})();