521 lines
22 KiB
JavaScript
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();
|
|
});
|
|
}
|
|
};
|
|
})();
|