Add custom download destinations, confirmations, and the bot error feed
This commit is contained in:
+189
-21
@@ -22,6 +22,7 @@
|
||||
dms: [],
|
||||
loadedChannels: false,
|
||||
authors: null,
|
||||
errors: [],
|
||||
filters: {
|
||||
source: "",
|
||||
channelId: "",
|
||||
@@ -92,6 +93,13 @@
|
||||
document.querySelector("#scope-bot")?.classList.toggle("active", view === "bot");
|
||||
document.querySelector("#scope-guild")?.classList.remove("active");
|
||||
document.querySelector("#scope-dm")?.classList.remove("active");
|
||||
|
||||
if (view === "bot") {
|
||||
localStorage.setItem("vk-media-errors-seen", String(errorCount));
|
||||
updateErrorBadge();
|
||||
void refreshErrors();
|
||||
}
|
||||
|
||||
render();
|
||||
}
|
||||
|
||||
@@ -191,7 +199,7 @@
|
||||
renderGrid();
|
||||
}
|
||||
|
||||
async function startScrape(channelId, guildId, mode) {
|
||||
async function startScrape(channelId, guildId, mode, downloadDir) {
|
||||
if (!channelId) {
|
||||
setStatus("no channel selected");
|
||||
return;
|
||||
@@ -202,7 +210,8 @@
|
||||
channelId,
|
||||
guildId: guildId ?? null,
|
||||
mode,
|
||||
backend: guildId ? "auto" : "puppet"
|
||||
backend: guildId ? "auto" : "puppet",
|
||||
downloadDir: downloadDir ?? null
|
||||
});
|
||||
setStatus(`started ${scrape.id} (${mode})`);
|
||||
scrapeModal.hidden = true;
|
||||
@@ -219,8 +228,8 @@
|
||||
function renderScrapes() {
|
||||
scrapeList.replaceChildren();
|
||||
const active = state.scrapes.filter(scrape => ["queued", "running", "paused"].includes(scrape.state));
|
||||
const rows = active.length ? active : state.scrapes.slice(0, 5);
|
||||
for (const scrape of rows) {
|
||||
|
||||
for (const scrape of active) {
|
||||
const row = el("div", "vk-media-scrape");
|
||||
const media = scrape.media ?? {};
|
||||
|
||||
@@ -239,21 +248,79 @@
|
||||
)
|
||||
);
|
||||
|
||||
if (active.includes(scrape)) {
|
||||
row.append(
|
||||
button("Stop", async () => {
|
||||
await window.VK.api.post("/api/media/scrape/stop", { id: scrape.id });
|
||||
void loadScrapes();
|
||||
})
|
||||
);
|
||||
} else {
|
||||
row.append(el("span", "vk-media-status", durationLabel(scrape)));
|
||||
}
|
||||
row.append(
|
||||
button("Stop", async () => {
|
||||
await window.VK.api.post("/api/media/scrape/stop", { id: scrape.id });
|
||||
void loadScrapes();
|
||||
})
|
||||
);
|
||||
|
||||
scrapeList.append(row);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Bot errors ----
|
||||
|
||||
let errorCount = 0;
|
||||
|
||||
function seenErrorCount() {
|
||||
return Number(localStorage.getItem("vk-media-errors-seen") ?? 0);
|
||||
}
|
||||
|
||||
function updateErrorBadge() {
|
||||
const badge = document.querySelector("#scope-bot-badge");
|
||||
if (!badge) return;
|
||||
|
||||
const unseen = Math.max(0, errorCount - seenErrorCount());
|
||||
badge.textContent = String(unseen);
|
||||
badge.hidden = unseen === 0;
|
||||
}
|
||||
|
||||
function renderErrors() {
|
||||
scrapeList.replaceChildren();
|
||||
|
||||
const head = el("div", "vk-media-scrape");
|
||||
head.append(el("span", null, `${(state.errors ?? []).length} error(s)`));
|
||||
head.append(
|
||||
button("Clear errors", async () => {
|
||||
try {
|
||||
await window.VK.api.post("/api/media/errors/clear", {});
|
||||
} catch {
|
||||
// the list stays until the next refresh
|
||||
}
|
||||
state.errors = [];
|
||||
errorCount = 0;
|
||||
localStorage.setItem("vk-media-errors-seen", "0");
|
||||
updateErrorBadge();
|
||||
renderErrors();
|
||||
})
|
||||
);
|
||||
scrapeList.append(head);
|
||||
|
||||
for (const entry of state.errors ?? []) {
|
||||
scrapeList.append(
|
||||
el(
|
||||
"div",
|
||||
"vk-media-scrape vk-media-error",
|
||||
`${new Date(entry.at).toLocaleTimeString()} · ${entry.source} · ${entry.message}`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!(state.errors ?? []).length) scrapeList.append(el("div", "vk-media-scrape", "No errors."));
|
||||
}
|
||||
|
||||
async function refreshErrors() {
|
||||
try {
|
||||
state.errors = await window.VK.api.get("/api/media/errors");
|
||||
} catch {
|
||||
state.errors = [];
|
||||
}
|
||||
errorCount = (state.errors ?? []).length;
|
||||
updateErrorBadge();
|
||||
if (state.view === "bot") renderErrors();
|
||||
}
|
||||
|
||||
function renderGrid() {
|
||||
grid.replaceChildren();
|
||||
for (const item of state.items) {
|
||||
@@ -319,8 +386,7 @@
|
||||
toolbar.append(chip);
|
||||
}
|
||||
|
||||
toolbar.append(button("New scrape…", openScrapeModal));
|
||||
toolbar.append(button("History", openHistoryModal));
|
||||
|
||||
|
||||
const sourceSelect = document.createElement("select");
|
||||
sourceSelect.className = "vk-chip vk-chip-off";
|
||||
@@ -428,6 +494,10 @@
|
||||
void loadMedia(true);
|
||||
})
|
||||
);
|
||||
|
||||
toolbar.append(el("span", "vk-media-spacer"));
|
||||
toolbar.append(button("History", openHistoryModal));
|
||||
toolbar.append(button("New scrape…", openScrapeModal));
|
||||
} else {
|
||||
toolbar.append(el("span", null, "Slopbot is the dedicated bot core. Setup: docs/slopbot.md."));
|
||||
}
|
||||
@@ -444,12 +514,14 @@
|
||||
statusLine.textContent = bot?.connected
|
||||
? `connected as ${bot.user} in ${bot.guilds.length} guild(s)`
|
||||
: "slopbot is disabled or not logged in (VESKSLOP_SLOPBOT_ENABLED=1 and a token)";
|
||||
void loadScrapes().then(renderScrapes);
|
||||
void refreshErrors();
|
||||
} else {
|
||||
void loadChannels().then(renderToolbar);
|
||||
void loadAuthors().then(renderToolbar);
|
||||
void loadMedia(true);
|
||||
void loadScrapes();
|
||||
}
|
||||
void loadScrapes();
|
||||
}
|
||||
|
||||
overlay.querySelector("#media-close").addEventListener("click", hide);
|
||||
@@ -465,6 +537,16 @@
|
||||
document.querySelector("#scope-guild")?.addEventListener("click", hide);
|
||||
document.querySelector("#scope-dm")?.addEventListener("click", hide);
|
||||
|
||||
const errorBadge = el("span", "vk-media-badge");
|
||||
errorBadge.id = "scope-bot-badge";
|
||||
errorBadge.hidden = true;
|
||||
scopeBot?.append(errorBadge);
|
||||
|
||||
void refreshErrors();
|
||||
setInterval(() => {
|
||||
if (state.view !== "bot") void refreshErrors();
|
||||
}, 15000);
|
||||
|
||||
// ---- Scrape popup: server or group-chat trees ----
|
||||
|
||||
const scrapeModal = document.createElement("div");
|
||||
@@ -472,7 +554,11 @@
|
||||
scrapeModal.hidden = true;
|
||||
scrapeModal.innerHTML = `
|
||||
<div class="vk-media-modal-box vk-media-wide">
|
||||
<strong>New scrape</strong>
|
||||
<div class="vk-media-head">
|
||||
<strong>New scrape</strong>
|
||||
<span class="vk-media-spacer"></span>
|
||||
<button id="scrape-x" class="btn btn-secondary vk-small-btn" type="button">X</button>
|
||||
</div>
|
||||
<div class="vk-media-toolbar">
|
||||
<button id="scrape-tab-server" class="vk-chip vk-chip-on" type="button">Server</button>
|
||||
<button id="scrape-tab-group" class="vk-chip vk-chip-off" type="button">Group chat</button>
|
||||
@@ -491,8 +577,8 @@
|
||||
function channelRow(channelId, guildId, label) {
|
||||
const row = el("div", "vk-media-channel");
|
||||
row.append(el("span", "vk-media-channel-name", label));
|
||||
row.append(button("Links", () => void startScrape(channelId, guildId, "links")));
|
||||
row.append(button("Download", () => void startScrape(channelId, guildId, "download")));
|
||||
row.append(button("Links", () => askScrape(channelId, guildId, "links", label)));
|
||||
row.append(button("Download", () => askScrape(channelId, guildId, "download", label)));
|
||||
return row;
|
||||
}
|
||||
|
||||
@@ -558,6 +644,78 @@
|
||||
scrapeModal.querySelector("#scrape-cancel").addEventListener("click", () => {
|
||||
scrapeModal.hidden = true;
|
||||
});
|
||||
scrapeModal.querySelector("#scrape-x").addEventListener("click", () => {
|
||||
scrapeModal.hidden = true;
|
||||
});
|
||||
scrapeModal.addEventListener("click", event => {
|
||||
if (event.target === scrapeModal) scrapeModal.hidden = true;
|
||||
});
|
||||
|
||||
// ---- Confirmation popup for Links / Download ----
|
||||
|
||||
const confirmModal = document.createElement("div");
|
||||
confirmModal.className = "vk-media-modal";
|
||||
confirmModal.hidden = true;
|
||||
confirmModal.innerHTML = `
|
||||
<div class="vk-media-modal-box">
|
||||
<strong id="confirm-title">Start scrape?</strong>
|
||||
<div id="confirm-note" class="vk-media-status"></div>
|
||||
<label id="confirm-dest-row" class="vk-media-check" hidden>
|
||||
<input id="confirm-custom-dest" type="checkbox" /> Custom download destination
|
||||
</label>
|
||||
<input
|
||||
id="confirm-dest"
|
||||
class="vk-media-input vk-media-dest"
|
||||
type="text"
|
||||
placeholder="/path/to/download/"
|
||||
hidden
|
||||
/>
|
||||
<div class="vk-media-toolbar">
|
||||
<button id="confirm-cancel" class="btn btn-secondary vk-small-btn" type="button">Cancel</button>
|
||||
<button id="confirm-start" class="btn btn-primary vk-small-btn" type="button">Start</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.append(confirmModal);
|
||||
|
||||
let confirmAction = null;
|
||||
|
||||
function askScrape(channelId, guildId, mode, label) {
|
||||
const isDownload = mode === "download";
|
||||
const destRow = confirmModal.querySelector("#confirm-dest-row");
|
||||
const destInput = confirmModal.querySelector("#confirm-dest");
|
||||
const checkbox = confirmModal.querySelector("#confirm-custom-dest");
|
||||
|
||||
confirmModal.querySelector("#confirm-title").textContent =
|
||||
isDownload ? "Download scrape?" : "Links scrape?";
|
||||
confirmModal.querySelector("#confirm-note").textContent = isDownload
|
||||
? `${label} — downloads files${guildId ? "" : " with the puppet"}.`
|
||||
: `${label} — indexes links and caches previews${guildId ? "" : " with the puppet"}.`;
|
||||
|
||||
destRow.hidden = !isDownload;
|
||||
destInput.hidden = true;
|
||||
destInput.value = "";
|
||||
checkbox.checked = false;
|
||||
checkbox.onchange = () => {
|
||||
destInput.hidden = !checkbox.checked;
|
||||
};
|
||||
|
||||
confirmAction = () =>
|
||||
startScrape(channelId, guildId, mode, isDownload && checkbox.checked ? destInput.value.trim() : null);
|
||||
confirmModal.hidden = false;
|
||||
}
|
||||
|
||||
confirmModal.querySelector("#confirm-cancel").addEventListener("click", () => {
|
||||
confirmModal.hidden = true;
|
||||
});
|
||||
confirmModal.querySelector("#confirm-start").addEventListener("click", () => {
|
||||
const action = confirmAction;
|
||||
confirmModal.hidden = true;
|
||||
void action?.();
|
||||
});
|
||||
confirmModal.addEventListener("click", event => {
|
||||
if (event.target === confirmModal) confirmModal.hidden = true;
|
||||
});
|
||||
|
||||
// ---- History popup ----
|
||||
|
||||
@@ -566,7 +724,11 @@
|
||||
historyModal.hidden = true;
|
||||
historyModal.innerHTML = `
|
||||
<div class="vk-media-modal-box vk-media-wide">
|
||||
<strong>Scrape history</strong>
|
||||
<div class="vk-media-head">
|
||||
<strong>Scrape history</strong>
|
||||
<span class="vk-media-spacer"></span>
|
||||
<button id="history-x" class="btn btn-secondary vk-small-btn" type="button">X</button>
|
||||
</div>
|
||||
<div id="history-list" class="vk-media-modal-list"></div>
|
||||
<div class="vk-media-toolbar">
|
||||
<button id="history-close" class="btn btn-secondary vk-small-btn" type="button">Close</button>
|
||||
@@ -630,6 +792,12 @@
|
||||
historyModal.querySelector("#history-close").addEventListener("click", () => {
|
||||
historyModal.hidden = true;
|
||||
});
|
||||
historyModal.querySelector("#history-x").addEventListener("click", () => {
|
||||
historyModal.hidden = true;
|
||||
});
|
||||
historyModal.addEventListener("click", event => {
|
||||
if (event.target === historyModal) historyModal.hidden = true;
|
||||
});
|
||||
|
||||
// ---- Settings: media storage ----
|
||||
|
||||
|
||||
Reference in New Issue
Block a user