Add the Bot settings group with limit, usage bar, and clean modal

This commit is contained in:
Fisto
2026-09-16 08:50:53 +02:00
parent 8f3f153c55
commit af07facbb5
6 changed files with 314 additions and 5 deletions
+172 -1
View File
@@ -252,5 +252,176 @@
document.querySelector("#scope-guild")?.addEventListener("click", hide);
document.querySelector("#scope-dm")?.addEventListener("click", hide);
window.VK.media = { show, hide };
// ---- Settings: media storage ----
function formatBytes(bytes) {
if (bytes >= 1024 ** 3) return `${(bytes / 1024 ** 3).toFixed(2)} GiB`;
if (bytes >= 1024 ** 2) return `${Math.round(bytes / 1024 ** 2)} MiB`;
return `${bytes} B`;
}
function renderUsageBar(status) {
const bar = document.querySelector("#media-usage-bar");
if (!bar || !status) return;
bar.replaceChildren();
const limit = Math.max(status.limitBytes, 1);
const measured =
(status.byKind?.image ?? 0) +
(status.byKind?.gif ?? 0) +
(status.byKind?.video ?? 0) +
(status.byKind?.audio ?? 0);
const other = Math.max(0, status.usedBytes - measured);
for (const [kind, bytes] of [
["image", status.byKind?.image ?? 0],
["gif", status.byKind?.gif ?? 0],
["video", status.byKind?.video ?? 0],
["audio", status.byKind?.audio ?? 0],
["other", other]
]) {
if (bytes <= 0) continue;
const segment = el("div", `vk-media-usage-seg vk-seg-${kind}`);
segment.style.width = `${Math.min(100, (bytes / limit) * 100)}%`;
segment.title = `${kind}: ${formatBytes(bytes)}`;
bar.append(segment);
}
}
async function loadMediaSettings() {
try {
const [storage, status] = await Promise.all([
window.VK.api.get("/api/media/storage"),
window.VK.api.get("/api/media/status")
]);
state.storage = storage;
const bot = status?.slopbot;
const slopbotRow = document.querySelector("#media-slopbot");
if (slopbotRow) slopbotRow.textContent = bot?.connected ? `connected as ${bot.user}` : "disabled";
const limit = document.querySelector("#media-limit");
if (limit && document.activeElement !== limit) {
limit.value = (storage.limitBytes / 1024 ** 3).toFixed(2);
}
const used = document.querySelector("#media-used");
if (used) {
used.textContent = `${formatBytes(storage.usedBytes)} of ${formatBytes(storage.limitBytes)}${
storage.nearLimit ? " (near limit)" : ""
}`;
}
renderUsageBar(storage);
} catch (error) {
const used = document.querySelector("#media-used");
if (used) used.textContent = `unavailable: ${error.message}`;
}
}
document.querySelector("#media-save")?.addEventListener("click", async () => {
const input = document.querySelector("#media-limit");
const gib = Number(input?.value);
if (!Number.isFinite(gib) || gib <= 0) return;
try {
await window.VK.api.post("/api/preferences", { media: { limitBytes: Math.round(gib * 1024 ** 3) } });
setStatus("media limit saved");
const save = document.querySelector("#media-save");
if (save) save.disabled = true;
} catch (error) {
setStatus(`save failed: ${error.message}`);
}
void loadMediaSettings();
});
document.querySelector("#media-limit")?.addEventListener("input", () => {
const save = document.querySelector("#media-save");
if (save) save.disabled = false;
});
const cleanModal = document.createElement("div");
cleanModal.className = "vk-media-modal";
cleanModal.hidden = true;
cleanModal.innerHTML = `
<div class="vk-media-modal-box">
<strong>Delete media scrapes?</strong>
<div id="clean-note" class="vk-media-status"></div>
<div id="clean-list" class="vk-media-modal-list"></div>
<div class="vk-media-toolbar">
<button id="clean-cancel" class="btn btn-secondary vk-small-btn" type="button">Cancel</button>
<button id="clean-confirm" class="btn btn-primary vk-small-btn" type="button" disabled>Delete</button>
</div>
</div>
`;
document.body.append(cleanModal);
let cleanTimer = null;
function openCleanModal() {
const scrapes = state.storage?.scrapes ?? [];
const list = cleanModal.querySelector("#clean-list");
const note = cleanModal.querySelector("#clean-note");
const confirm = cleanModal.querySelector("#clean-confirm");
list.replaceChildren();
for (const scrape of scrapes) {
list.append(el("div", null, `${scrape.id} · ${scrape.state} · ${formatBytes(scrape.bytes)}`));
}
const bytes = scrapes.reduce((sum, scrape) => sum + scrape.bytes, 0);
note.textContent = scrapes.length
? `Everything above will be deleted permanently, including files and databases (${formatBytes(bytes)}). This cannot be undone.`
: "No scrapes to delete.";
confirm.disabled = true;
let left = 5;
confirm.textContent = `Delete (${left})`;
cleanModal.hidden = false;
clearInterval(cleanTimer);
if (!scrapes.length) {
confirm.textContent = "Delete";
return;
}
cleanTimer = setInterval(() => {
left--;
if (left <= 0) {
clearInterval(cleanTimer);
confirm.disabled = false;
confirm.textContent = "Delete";
return;
}
confirm.textContent = `Delete (${left})`;
}, 1000);
}
cleanModal.querySelector("#clean-cancel").addEventListener("click", () => {
clearInterval(cleanTimer);
cleanModal.hidden = true;
});
cleanModal.querySelector("#clean-confirm").addEventListener("click", async () => {
const ids = (state.storage?.scrapes ?? []).map(scrape => scrape.id);
try {
await window.VK.api.post("/api/media/clean", { ids });
} catch (error) {
setStatus(`clean failed: ${error.message}`);
}
cleanModal.hidden = true;
void loadMediaSettings();
void loadScrapes();
});
document.querySelector("#media-clean")?.addEventListener("click", openCleanModal);
const settingsOverlay = document.querySelector("#settings-overlay");
if (settingsOverlay) {
new MutationObserver(() => {
if (!settingsOverlay.hidden) void loadMediaSettings();
}).observe(settingsOverlay, { attributes: true, attributeFilter: ["hidden"] });
}
window.VK.media = { show, hide, loadMediaSettings };
})();