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

View File

@@ -404,6 +404,26 @@ body.theme-light .input-row input:focus {
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 {
outline: none;
border-color: var(--accent);