add(ui/ux): show file formats in the scan and filter by them

This commit is contained in:
Steppenstreuner
2026-08-31 08:33:24 +02:00
parent f08417cdc0
commit 2e84bc8aca
8 changed files with 134 additions and 1 deletions
+39
View File
@@ -686,6 +686,7 @@ $("#scan-btn").addEventListener("click", async () => {
importPage = 0;
$("#scan-info").textContent = `${scan.items.length} Kandidat(en) in ${scan.download_dir}`;
$("#import-toolbar").hidden = scanItems.length === 0;
formatOptions();
renderImportTable();
} catch (err) {
$("#scan-info").textContent = "";
@@ -698,6 +699,7 @@ $("#scan-btn").addEventListener("click", async () => {
function applyImportView() {
const text = $("#import-filter-text").value.toLowerCase();
const mode = $("#import-filter-mode").value;
const format = $("#import-filter-format").value;
const sort = $("#import-sort").value;
viewIdx = scanItems
.map((_, i) => i)
@@ -707,12 +709,44 @@ function applyImportView() {
if (mode === "suggested" && !item.suggested_request_id) return false;
if (mode === "unassigned" && item.request_id) return false;
if (mode === "conflict" && !item.conflict) return false;
if (format && !(item.formats || []).includes(format)) return false;
return true;
});
if (sort === "score") viewIdx.sort((a, b) => scanItems[b].score - scanItems[a].score);
if (sort === "name") viewIdx.sort((a, b) => scanItems[a].name.localeCompare(scanItems[b].name));
}
// the split of a folder happens client side, so the extension comes off the name
function extOf(path) {
const ext = (path.split("/").pop().match(/\.([^.]+)$/) || [])[1];
return ext ? [ext.toLowerCase()] : [];
}
function humanSize(bytes) {
if (!bytes) return "";
const units = ["B", "KB", "MB", "GB"];
let n = bytes, u = 0;
while (n >= 1024 && u < units.length - 1) { n /= 1024; u++; }
return `${n.toFixed(n < 10 && u > 0 ? 1 : 0).replace(".", ",")} ${units[u]}`;
}
// a Calibre export ships one book in a dozen formats, so the row has to say
// which one it is before anything can be picked
function formatTag(item) {
const formats = item.formats || [];
if (!formats.length) return "";
const size = humanSize(item.size);
return `<span class="tag fmt">${esc(formats.join(", "))}${size ? " · " + size : ""}</span>`;
}
function formatOptions() {
const all = [...new Set(scanItems.flatMap((it) => it.formats || []))].sort();
const current = $("#import-filter-format").value;
$("#import-filter-format").innerHTML =
'<option value="">Alle Formate</option>' +
all.map((f) => `<option value="${esc(f)}" ${f === current ? "selected" : ""}>.${esc(f)}</option>`).join("");
}
function scoreCell(item) {
if (!item.suggested_request_id) return '<td class="score-none">—</td>';
const cls = item.score >= 80 ? "score-hi" : item.score >= 50 ? "score-mid" : "score-lo";
@@ -757,6 +791,7 @@ function renderImportTable() {
<td><input type="checkbox" data-check="${i}" ${item.checked ? "checked" : ""}></td>
<td class="mono">
${esc(item.name)}${item.is_dir ? " 📁" : ""}
${formatTag(item)}
${item.parts ? `<span class="tag">${item.parts.length} Teile · ${item.files.length} Dateien</span>` : ""}
${!item.parts && item.maybe_separate
? `<span class="tag warn" title="Die ${item.files.length} Dateien tragen verschiedene Titel und sind je groß genug für ein ganzes Buch — mit ✂️ aufteilen">${item.files.length} Titel?</span>`
@@ -876,6 +911,7 @@ $("#import-prev").addEventListener("click", () => { importPage--; renderImportTa
$("#import-next").addEventListener("click", () => { importPage++; renderImportTable(); });
$("#import-filter-text").addEventListener("input", () => { importPage = 0; renderImportTable(); });
$("#import-filter-mode").addEventListener("change", () => { importPage = 0; renderImportTable(); });
$("#import-filter-format").addEventListener("change", () => { importPage = 0; renderImportTable(); });
$("#import-sort").addEventListener("change", () => { importPage = 0; renderImportTable(); });
$("#import-select-suggested").addEventListener("click", () => {
@@ -1053,6 +1089,8 @@ $("#import-merge").addEventListener("click", () => {
media_type: chosen[0].media_type,
is_dir: true,
files: chosen.flatMap((it) => it.files),
formats: [...new Set(chosen.flatMap((it) => it.formats || []))].sort(),
size: chosen.reduce((n, it) => n + (it.size || 0), 0),
checked: true,
request_id: chosen.find((it) => it.request_id)?.request_id ?? null,
suggested_request_id: chosen.find((it) => it.suggested_request_id)?.suggested_request_id ?? null,
@@ -1079,6 +1117,7 @@ function splitItem(i) {
media_type: item.media_type,
is_dir: false,
files: [f],
formats: extOf(f),
checked: false,
request_id: null,
suggested_request_id: null,
+3
View File
@@ -157,6 +157,9 @@
<option value="unassigned">Nur ohne Zuordnung</option>
<option value="conflict">Nur Konflikte</option>
</select>
<select id="import-filter-format" title="Nur Einträge mit diesem Dateiformat zeigen">
<option value="">Alle Formate</option>
</select>
<select id="import-sort">
<option value="none">Reihenfolge: Scan</option>
<option value="score">Score absteigend</option>
+1
View File
@@ -55,6 +55,7 @@ button:disabled { opacity: 0.6; cursor: default; }
.mono { font-family: ui-monospace, monospace; font-size: 0.85rem; word-break: break-all; }
.subpath { color: var(--muted); font-size: 0.75rem; font-family: ui-monospace, monospace; }
.tag.warn { background: #8a6d1f; }
.tag.fmt { background: #3a3f4b; color: #cfd4de; }
/* per-entry import feedback */
/* :not([hidden]): a plain display rule would beat the hidden attribute */