better multiselect
This commit is contained in:
101
frontend/app.js
101
frontend/app.js
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user