Refine resource loader status grouping and PDF import handling
All checks were successful
Build Org Website / build (push) Successful in 39s

This commit is contained in:
gitea-actions
2026-08-19 15:36:00 +01:00
parent b8bb1944dd
commit 8dbeca7570
11 changed files with 173 additions and 91 deletions

View File

@@ -23,6 +23,15 @@
pdfUrls: new Map(),
thumbnailUrls: new Map(),
};
let pdfJsPromise;
const STATUS_GROUPS = [
{ id: "active", label: "In progress", description: "Resources you are actively reading or watching." },
{ id: "backlog", label: "Backlog", description: "Resources you intend to start later." },
{ id: "shelf", label: "On the shelf", description: "Resources kept close at hand, but not currently active." },
{ id: "completed", label: "Completed", description: "Resources you have finished." },
{ id: "archived", label: "Archived", description: "Resources retained outside your current library flow." },
];
const els = {
vaultSelect: $("#rl-vault-select"),
@@ -217,7 +226,7 @@
const progress = Model.progressFor(resource);
const initial = (resource.title || "?").trim().charAt(0).toUpperCase();
const creators = (resource.creators || []).join(", ") || "Unknown creator";
return `<article class="rl-resource-card" tabindex="0" role="button" data-resource-id="${escapeHtml(resource.id)}" aria-label="Open ${escapeHtml(resource.title)}">
return `<article class="rl-resource-card rl-resource-card--${escapeHtml(resource.status)}" tabindex="0" role="button" data-resource-id="${escapeHtml(resource.id)}" aria-label="Open ${escapeHtml(resource.title)}">
<span class="rl-resource-cover" data-thumb="${escapeHtml(resource.thumbnailId || "")}">${escapeHtml(initial)}</span>
<span class="rl-resource-title"><strong>${escapeHtml(resource.title)}</strong><small>${escapeHtml(creators)}</small><small>${escapeHtml((resource.tags || []).slice(0, 3).join(" · "))}</small></span>
<span class="rl-badges"><span class="rl-badge">${escapeHtml(resource.type)}</span><span class="rl-badge">${escapeHtml(resource.status)}</span><span class="rl-badge">${escapeHtml(resource.format)}</span></span>
@@ -226,6 +235,19 @@
</article>`;
}
function statusGroupMarkup(group, resources) {
const headingId = `rl-status-${group.id}`;
const countLabel = `${resources.length} ${resources.length === 1 ? "resource" : "resources"}`;
return `<section class="rl-status-group rl-status-group--${group.id}" aria-labelledby="${headingId}">
<header class="rl-status-group__head">
<span class="rl-status-group__marker" aria-hidden="true"></span>
<span class="rl-status-group__title"><strong id="${headingId}">${escapeHtml(group.label)}</strong><small>${escapeHtml(group.description)}</small></span>
<span class="rl-status-group__count" aria-label="${countLabel}">${resources.length}</span>
</header>
<div class="rl-status-resources">${resources.map(cardMarkup).join("")}</div>
</section>`;
}
async function hydrateThumbnails(container) {
const targets = Array.from(container.querySelectorAll("[data-thumb]")).filter((item) => item.dataset.thumb);
await Promise.all(targets.map(async (target) => {
@@ -260,7 +282,10 @@
els.resourceList.querySelector("[data-empty-add]").addEventListener("click", () => openResourceDialog());
return;
}
els.resourceList.innerHTML = resources.map(cardMarkup).join("");
els.resourceList.innerHTML = STATUS_GROUPS.map((group) => {
const matching = resources.filter((resource) => resource.status === group.id);
return matching.length ? statusGroupMarkup(group, matching) : "";
}).join("");
bindResourceCards();
hydrateThumbnails(els.resourceList);
}
@@ -291,6 +316,20 @@
$("#rl-cover-preview").innerHTML = "<span>Cover</span>";
}
function updateResourceFormForType() {
const type = $("#rl-field-type").value;
const isBook = type === "book";
els.resourceDialog.classList.toggle("is-compact", !isBook);
els.resourceDialog.querySelectorAll("[data-resource-types]").forEach((element) => {
element.hidden = !element.dataset.resourceTypes.split(/\s+/).includes(type);
});
$("#rl-creators-label").textContent = type === "video" ? "Presenter / creator" : "Author(s)";
$("#rl-publisher-label").textContent = type === "video" ? "Channel" : "Publisher";
if (!isBook) $("#rl-field-format").value = "web";
if (!isBook) $("#rl-field-pages").value = "";
if (type !== "video") $("#rl-field-duration").value = "";
}
async function showCover(id) {
resetCover();
const url = await thumbnailUrl(id);
@@ -341,6 +380,7 @@
setValue("rl-field-tags", item && (item.tags || []).join(", "));
setValue("rl-field-description", item && item.description);
setValue("rl-field-notes", item && item.notes);
updateResourceFormForType();
populateFolderField(item && item.folderId ? item.folderId : (!["all", "unfiled"].includes(state.folderId) ? state.folderId : ""));
$("#rl-delete-resource").hidden = !resource;
$("#rl-open-resource").hidden = !(item && (item.url || item.attachmentId));
@@ -597,6 +637,20 @@
return new Promise((resolve) => canvas.toBlob(resolve, "image/jpeg", .78));
}
function loadPdfJs() {
if (globalThis.pdfjsLib) return Promise.resolve(globalThis.pdfjsLib);
if (pdfJsPromise) return pdfJsPromise;
pdfJsPromise = new Promise((resolve, reject) => {
const script = document.createElement("script");
script.src = "/assets/scripts/vendor/pdf.min.js";
script.async = true;
script.onload = () => globalThis.pdfjsLib ? resolve(globalThis.pdfjsLib) : reject(new Error("PDF.js loaded without exposing its API."));
script.onerror = () => reject(new Error("The local PDF.js library could not be loaded."));
document.head.appendChild(script);
});
return pdfJsPromise;
}
async function importPdf(file) {
if (!file || (file.type && file.type !== "application/pdf") || !file.name.toLowerCase().endsWith(".pdf")) {
toast("Choose a PDF file.", true); return;
@@ -605,8 +659,8 @@
let attachmentId;
let thumbnailId;
try {
const pdfjs = await import("/assets/scripts/vendor/pdf.min.mjs");
pdfjs.GlobalWorkerOptions.workerSrc = "/assets/scripts/vendor/pdf.worker.min.mjs";
const pdfjs = await loadPdfJs();
pdfjs.GlobalWorkerOptions.workerSrc = "/assets/scripts/vendor/pdf.worker.min.js";
const bytes = new Uint8Array(await file.arrayBuffer());
const documentTask = pdfjs.getDocument({ data: bytes });
const pdf = await documentTask.promise;
@@ -638,7 +692,8 @@
if (attachmentId) await deleteAttachment(attachmentId).catch(() => {});
if (thumbnailId) await deleteAttachment(thumbnailId).catch(() => {});
const reason = error && error.name === "PasswordException" ? "The PDF is password protected." : error && error.name === "QuotaExceededError" ? "Browser storage is full." : "The PDF could not be read.";
toast(`${reason} No resource was created.`, true);
const detail = error && error.message ? ` ${error.message}` : "";
toast(`${reason}${detail} No resource was created.`, true);
console.error(error);
} finally {
els.pdfInput.value = "";
@@ -687,6 +742,7 @@
$("#rl-delete-resource").addEventListener("click", removeResource);
$("#rl-open-resource").addEventListener("click", openLinkedResource);
$("#rl-add-session").addEventListener("click", openSessionDialog);
$("#rl-field-type").addEventListener("change", updateResourceFormForType);
$("#rl-session-form").addEventListener("submit", saveSession);
$("#rl-name-form").addEventListener("submit", submitName);
$("#rl-name-delete").addEventListener("click", deleteNamedFolder);