Add sort and exclusion filters to the media grid

This commit is contained in:
Fisto
2026-09-17 11:01:50 +02:00
parent cc14140f34
commit 515af01e84
13 changed files with 561 additions and 96 deletions
+216 -3
View File
@@ -251,6 +251,12 @@
renderScrapes();
}
const SIZE_UNITS = { B: 1, KB: 1024, MB: 1024 ** 2, GB: 1024 ** 3 };
function sizeUnit(unit) {
return SIZE_UNITS[unit] ?? 1;
}
function filterQuery() {
const kind = state.kind === "all" ? "" : `kind=${state.kind}&`;
const source = state.filters.source ? `source=${state.filters.source}&` : "";
@@ -264,11 +270,34 @@
const excludeHosts = state.filters.excludeHosts.length
? `excludeHosts=${state.filters.excludeHosts.join(",")}&`
: "";
const sort = state.filters.sort && state.filters.sort !== "newest" ? `sort=${state.filters.sort}&` : "";
const excludeProviders = state.filters.excludeProviders.length
? `excludeProviders=${state.filters.excludeProviders.join(",")}&`
: "";
const excludeSources = state.filters.excludeSources.length
? `excludeSources=${state.filters.excludeSources.join(",")}&`
: "";
const excludeExts = state.filters.excludeExts.length
? `excludeExt=${state.filters.excludeExts.join(",")}&`
: "";
const amount = (value, unit) => {
const number = Number(value);
if (value === "" || !Number.isFinite(number) || number < 0) return "";
return String(Math.round(number * (unit ? sizeUnit(unit) : 1)));
};
const sizeLt = state.filters.sizeLt !== "" ? `sizeLt=${amount(state.filters.sizeLt, state.filters.sizeLtUnit)}&` : "";
const sizeGt = state.filters.sizeGt !== "" ? `sizeGt=${amount(state.filters.sizeGt, state.filters.sizeGtUnit)}&` : "";
const lenLt = state.filters.lenLt !== "" ? `lenLt=${amount(state.filters.lenLt)}&` : "";
const lenGt = state.filters.lenGt !== "" ? `lenGt=${amount(state.filters.lenGt)}&` : "";
const aspect = state.filters.aspect ? `aspect=${state.filters.aspect}&` : "";
const resolution = state.filters.resolution ? `resolution=${state.filters.resolution}&` : "";
const after = state.filters.after ? `after=${state.filters.after}T00:00:00.000Z&` : "";
const before = state.filters.before ? `before=${state.filters.before}T23:59:59.999Z&` : "";
return `${kind}${source}${channel}${authors}${origin}${cached}${exts}${excludeHosts}${aspect}${resolution}${after}${before}`;
return (
`${kind}${source}${channel}${authors}${origin}${cached}${exts}${excludeHosts}` +
`${sort}${excludeProviders}${excludeSources}${excludeExts}${sizeLt}${sizeGt}${lenLt}${lenGt}` +
`${aspect}${resolution}${after}${before}`
);
}
function kindTotals(field) {
@@ -1137,6 +1166,16 @@
cachedOnly: false,
exts: [],
excludeHosts: ["4chan.org", "i.4cdn.org"],
sort: "newest",
excludeProviders: [],
excludeSources: [],
excludeExts: [],
sizeLt: "",
sizeGt: "",
sizeLtUnit: "MB",
sizeGtUnit: "MB",
lenLt: "",
lenGt: "",
aspect: "",
resolution: "",
after: "",
@@ -1146,7 +1185,16 @@
function ensureDraft() {
if (!state.draft) {
state.draft = { kind: state.kind, ...state.filters, authorIds: [...state.filters.authorIds] };
state.draft = {
...state.filters,
kind: state.kind,
authorIds: [...state.filters.authorIds],
exts: [...state.filters.exts],
excludeHosts: [...state.filters.excludeHosts],
excludeProviders: [...state.filters.excludeProviders],
excludeSources: [...state.filters.excludeSources],
excludeExts: [...state.filters.excludeExts]
};
}
}
@@ -1160,6 +1208,16 @@
cachedOnly: state.draft.cachedOnly,
exts: [...state.draft.exts],
excludeHosts: [...state.draft.excludeHosts],
sort: state.draft.sort,
excludeProviders: [...state.draft.excludeProviders],
excludeSources: [...state.draft.excludeSources],
excludeExts: [...state.draft.excludeExts],
sizeLt: state.draft.sizeLt,
sizeGt: state.draft.sizeGt,
sizeLtUnit: state.draft.sizeLtUnit,
sizeGtUnit: state.draft.sizeGtUnit,
lenLt: state.draft.lenLt,
lenGt: state.draft.lenGt,
aspect: state.draft.aspect,
resolution: state.draft.resolution,
after: state.draft.after,
@@ -1291,6 +1349,19 @@
const filters = section("Filters");
filters.append(
filterSelect(
[
["newest", "Newest first"],
["oldest", "Oldest first"],
["user", "User (A\u2192Z)"],
["size", "Largest size"],
["length", "Longest length"]
],
draft.sort,
value => {
draft.sort = value;
}
),
filterSelect(
[
["", "Any ratio"],
@@ -1403,6 +1474,146 @@
});
deadRow.append(deadBox, document.createTextNode(" Hide 4chan links (usually dead)"));
filters.append(deadRow);
// "Do not include": nested collapsibles, so exclusions stay out of the way.
const exclude = document.createElement("details");
exclude.className = "vk-media-exclude";
exclude.append(el("summary", "vk-media-exclude-title", "Do not include"));
const group = title => {
const detailsNode = document.createElement("details");
detailsNode.className = "vk-media-exclude-group";
detailsNode.append(el("summary", "vk-media-status", title));
return detailsNode;
};
const checkRow = (label, checked, onToggle) => {
const row = el("label", "vk-media-check");
const box = document.createElement("input");
box.type = "checkbox";
box.checked = checked;
box.addEventListener("change", () => {
onToggle(box.checked);
renderFilterDrawer();
});
row.append(box, document.createTextNode(` ${label}`));
return row;
};
const toggle = (list, value, on) =>
on ? [...new Set([...list, value])] : list.filter(entry => entry !== value);
const sourceGroup = group("Sources");
for (const [value, label] of [
["twitch", "Twitch"],
["youtube", "YouTube"],
["discord", "Discord"],
["tenor", "Tenor"],
["giphy", "Giphy"]
]) {
sourceGroup.append(
checkRow(label, draft.excludeProviders.includes(value), on => {
draft.excludeProviders = toggle(draft.excludeProviders, value, on);
})
);
}
for (const [value, label] of [
["embed", "Embeds"],
["attachment", "Attachments"],
["sticker", "Stickers"]
]) {
sourceGroup.append(
checkRow(label, draft.excludeSources.includes(value), on => {
draft.excludeSources = toggle(draft.excludeSources, value, on);
})
);
}
exclude.append(sourceGroup);
const extGroup = group("File types");
const extRow = el("div", "vk-media-toolbar");
for (const extension of [
"mp4",
"webm",
"mov",
"mkv",
"png",
"jpg",
"gif",
"webp",
"avif",
"mp3",
"ogg",
"wav",
"m4a",
"flac"
]) {
const chip = el(
"button",
`vk-chip ${draft.excludeExts.includes(extension) ? "vk-chip-off vk-chip-strike" : "vk-chip-on"}`,
extension
);
chip.type = "button";
chip.title = draft.excludeExts.includes(extension)
? `Excluding ${extension}`
: `${extension} included`;
chip.addEventListener("click", () => {
draft.excludeExts = toggle(draft.excludeExts, extension, !draft.excludeExts.includes(extension));
renderFilterDrawer();
});
extRow.append(chip);
}
extGroup.append(extRow);
exclude.append(extGroup);
const amountRow = (label, key, unitKey) => {
const row = el("div", "vk-media-toolbar");
row.append(el("span", "vk-media-status", label));
const input = document.createElement("input");
input.type = "number";
input.min = "0";
input.step = "any";
input.className = "vk-media-input vk-media-small-input";
input.value = draft[key];
input.addEventListener("input", () => {
draft[key] = input.value;
});
row.append(input);
if (unitKey) {
const unit = document.createElement("select");
unit.className = "vk-chip vk-chip-off";
for (const value of Object.keys(SIZE_UNITS)) {
const option = document.createElement("option");
option.value = value;
option.textContent = value;
unit.append(option);
}
unit.value = draft[unitKey];
unit.addEventListener("change", () => {
draft[unitKey] = unit.value;
});
row.append(unit);
} else {
row.append(el("span", "vk-media-status", "seconds"));
}
return row;
};
const sizeGroup = group("Size");
sizeGroup.append(
amountRow("Less than", "sizeLt", "sizeLtUnit"),
amountRow("Greater than", "sizeGt", "sizeGtUnit")
);
exclude.append(sizeGroup);
const lengthGroup = group("Length");
lengthGroup.append(amountRow("Less than", "lenLt"), amountRow("Greater than", "lenGt"));
exclude.append(lengthGroup);
filters.append(exclude);
filterDrawer.append(filters);
// Zoom applies live; it is not part of the filter draft.
@@ -1480,7 +1691,9 @@
function render() {
title.textContent = state.view === "bot" ? "Slopbot" : "Media archive";
toolbar.hidden = false;
// The filter bar and its drawer belong to the Media tab only.
toolbar.hidden = state.view === "bot";
if (state.view === "bot") filterDrawer.hidden = true;
grid.hidden = state.view === "bot";
botPanel.hidden = state.view !== "bot";
scrapeList.hidden = state.view === "bot";