broke up monolithic structure
This commit is contained in:
437
frontend/js/ui.js
Normal file
437
frontend/js/ui.js
Normal file
@@ -0,0 +1,437 @@
|
||||
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);
|
||||
};
|
||||
|
||||
// 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();
|
||||
});
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user