better multiselect

This commit is contained in:
Simon
2026-02-08 16:18:02 +00:00
parent 395b7e2c6d
commit 8ebbaeab1c
2 changed files with 96 additions and 25 deletions

View File

@@ -5,6 +5,7 @@ const renderedVideoIds = new Set();
let hasNextPage = true; let hasNextPage = true;
let isLoading = false; let isLoading = false;
let hlsPlayer = null; let hlsPlayer = null;
let currentLoadController = null;
// 2. Observer Definition (Must be defined before initApp uses it) // 2. Observer Definition (Must be defined before initApp uses it)
const observer = new IntersectionObserver((entries) => { const observer = new IntersectionObserver((entries) => {
@@ -115,12 +116,14 @@ async function loadVideos() {
try { try {
isLoading = true; isLoading = true;
updateLoadMoreState(); updateLoadMoreState();
currentLoadController = new AbortController();
const response = await fetch('/api/videos', { const response = await fetch('/api/videos', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify(body) body: JSON.stringify(body),
signal: currentLoadController.signal
}); });
const videos = await response.json(); const videos = await response.json();
renderVideos(videos); renderVideos(videos);
@@ -128,9 +131,12 @@ async function loadVideos() {
currentPage++; currentPage++;
ensureViewportFilled(); ensureViewportFilled();
} catch (err) { } catch (err) {
console.error("Failed to load videos:", err); if (err.name !== 'AbortError') {
console.error("Failed to load videos:", err);
}
} finally { } finally {
isLoading = false; isLoading = false;
currentLoadController = null;
updateLoadMoreState(); updateLoadMoreState();
} }
} }
@@ -156,6 +162,8 @@ function renderVideos(videos) {
grid.appendChild(card); grid.appendChild(card);
renderedVideoIds.add(v.id); renderedVideoIds.add(v.id);
}); });
ensureViewportFilled();
} }
function formatDuration(seconds) { function formatDuration(seconds) {
@@ -190,6 +198,10 @@ async function initApp() {
}; };
} }
window.addEventListener('resize', () => {
ensureViewportFilled();
});
await loadVideos(); await loadVideos();
} }
@@ -378,6 +390,11 @@ function buildDefaultOptions(channel) {
} }
function resetAndReload() { function resetAndReload() {
if (currentLoadController) {
currentLoadController.abort();
currentLoadController = null;
isLoading = false;
}
currentPage = 1; currentPage = 1;
hasNextPage = true; hasNextPage = true;
renderedVideoIds.clear(); renderedVideoIds.clear();
@@ -391,8 +408,8 @@ function ensureViewportFilled() {
if (!hasNextPage || isLoading) return; if (!hasNextPage || isLoading) return;
const grid = document.getElementById('video-grid'); const grid = document.getElementById('video-grid');
if (!grid) return; if (!grid) return;
const contentHeight = grid.getBoundingClientRect().bottom; const docHeight = document.documentElement.scrollHeight;
if (contentHeight < window.innerHeight + 120) { if (docHeight <= window.innerHeight + 120) {
window.setTimeout(() => loadVideos(), 0); window.setTimeout(() => loadVideos(), 0);
} }
} }
@@ -621,42 +638,76 @@ function renderFilters(container, session) {
const label = document.createElement('label'); const label = document.createElement('label');
label.textContent = optionGroup.title || optionGroup.id; label.textContent = optionGroup.title || optionGroup.id;
const options = optionGroup.options || [];
const currentSelection = session.options ? session.options[optionGroup.id] : null;
if (optionGroup.multiSelect) {
const list = document.createElement('div');
list.className = 'multi-select';
const selectedIds = new Set(
Array.isArray(currentSelection)
? currentSelection.map((item) => item.id)
: []
);
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 = 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;
setSession(nextSession);
savePreference(nextSession);
resetAndReload();
};
item.appendChild(checkbox);
item.appendChild(text);
list.appendChild(item);
});
wrapper.appendChild(label);
wrapper.appendChild(list);
container.appendChild(wrapper);
return;
}
const select = document.createElement('select'); const select = document.createElement('select');
select.multiple = Boolean(optionGroup.multiSelect); options.forEach((opt) => {
(optionGroup.options || []).forEach((opt) => {
const option = document.createElement('option'); const option = document.createElement('option');
option.value = opt.id; option.value = opt.id;
option.textContent = opt.title || opt.id; option.textContent = opt.title || opt.id;
select.appendChild(option); select.appendChild(option);
}); });
const currentSelection = session.options ? session.options[optionGroup.id] : null; if (currentSelection && currentSelection.id) {
if (Array.isArray(currentSelection)) {
const ids = new Set(currentSelection.map((item) => item.id));
Array.from(select.options).forEach((opt) => {
opt.selected = ids.has(opt.value);
});
} else if (currentSelection && currentSelection.id) {
select.value = currentSelection.id; select.value = currentSelection.id;
} }
select.onchange = () => { select.onchange = () => {
const nextSession = getSession(); const nextSession = getSession();
if (!nextSession || !nextSession.channel) return; if (!nextSession || !nextSession.channel) return;
const selected = options.find((item) => item.id === select.value);
const selectedOptions = optionGroup.options || []; if (selected) {
if (optionGroup.multiSelect) {
const selected = Array.from(select.selectedOptions).map((opt) =>
selectedOptions.find((item) => item.id === opt.value)
).filter(Boolean);
nextSession.options[optionGroup.id] = selected; nextSession.options[optionGroup.id] = selected;
} else {
const selected = selectedOptions.find((item) => item.id === select.value);
if (selected) {
nextSession.options[optionGroup.id] = selected;
}
} }
setSession(nextSession); setSession(nextSession);

View File

@@ -404,6 +404,26 @@ body.theme-light .input-row input:focus {
background-repeat: no-repeat; background-repeat: no-repeat;
} }
.multi-select {
display: flex;
flex-direction: column;
gap: 8px;
}
.multi-select-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--text-primary);
}
.multi-select-item input[type="checkbox"] {
width: 16px;
height: 16px;
accent-color: var(--accent);
}
.setting-item select:focus { .setting-item select:focus {
outline: none; outline: none;
border-color: var(--accent); border-color: var(--accent);