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 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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user