Add explicit Documents & Texts tab and native Image-to-PDF converter (/api/convert_pdf)

This commit is contained in:
drjones
2026-07-20 21:52:36 -07:00
parent 2d3ef6a06c
commit 031fccb3ff
2 changed files with 154 additions and 0 deletions

View File

@@ -416,6 +416,7 @@ mark{background:rgba(34,211,238,.28);color:#fff;border-radius:2px;padding:0 1px}
<h2>⌕ <b>photo library</b> — search, browse & bulk edit</h2>
<div class="view-tabs" id="viewTabs">
<button class="view-btn active" data-view="tagged">Tagged</button>
<button class="view-btn" data-view="docs">📑 Documents & Texts</button>
<button class="view-btn" data-view="untagged">Untagged</button>
<button class="view-btn" data-view="failed">Failed</button>
<button class="view-btn" data-view="videos">Videos</button>
@@ -458,6 +459,7 @@ mark{background:rgba(34,211,238,.28);color:#fff;border-radius:2px;padding:0 1px}
<select id="bulkCatSelect"></select>
<button id="btnBulkApply" class="btn-primary">Apply category to selected</button>
<button id="btnDownloadSelected" class="btn-primary">⬇ Download selected (ZIP)</button>
<button id="btnDownloadSelectedPdf" class="btn-primary">📄 Convert & Download Selected (PDF ZIP)</button>
<button id="btnExportSelected">Export selected (JSON)</button>
<button id="btnExportAllCsv">Export all matches (CSV)</button>
<button id="btnDownloadAllZip">⬇ Download all matches (ZIP)</button>
@@ -471,6 +473,13 @@ mark{background:rgba(34,211,238,.28);color:#fff;border-radius:2px;padding:0 1px}
</div>
</div>
<!-- DOCUMENTS & TEXTS -->
<div id="view-docs" class="lib-view">
<div class="search-toolbar"><span class="search-count" id="docsCount">—</span></div>
<div class="search-results-grid" id="docsGrid"></div>
<div class="load-more-row"><button id="btnLoadMoreDocs" style="display:none">Load more ▾</button></div>
</div>
<!-- UNTAGGED -->
<div id="view-untagged" class="lib-view">
<div class="search-toolbar"><span class="search-count" id="untaggedCount">—</span></div>
@@ -747,6 +756,7 @@ mark{background:rgba(34,211,238,.28);color:#fff;border-radius:2px;padding:0 1px}
<label>file path</label>
<div class="editor-path" id="lbPath">—</div>
<button id="lbCopyPath" style="margin-top:6px">⧉ Copy path</button>
<button id="lbDownloadPdf" class="btn-primary" style="margin-top:6px;width:100%">📄 Convert & Download PDF</button>
<label>category</label>
<select id="lbCat"></select>
@@ -1470,6 +1480,7 @@ function switchView(view){
document.querySelectorAll(".lib-view").forEach(el=>el.classList.remove("active"));
$("view-"+view).classList.add("active");
if(view === "tagged") doSearch(true);
else if(view === "docs") loadDocuments(true);
else if(view === "untagged") loadUntagged(true);
else if(view === "failed") loadFailed();
else if(view === "videos") loadVideos(true);
@@ -1739,6 +1750,60 @@ $("btnRetryAllFailed").onclick = async ()=>{
if(j.ok) addLog({level:"info", msg:"Retrying all failed photos…", ts:new Date().toTimeString().slice(0,8)});
};
/* ---- documents & text view ---- */
let docItems = [], docTotal = 0, docLoading = false;
async function loadDocuments(reset){
if(docLoading) return;
if(reset) docItems = [];
docLoading = true;
const j = await api("/api/documents?limit=60&offset=" + docItems.length);
docLoading = false;
if(!j.results) return;
docItems.push(...j.results);
docTotal = j.total || 0;
$("docsCount").textContent = docTotal + " document & text item" + (docTotal===1?"":"s") + " found";
const grid = $("docsGrid");
grid.innerHTML = docItems.length ? "" : `<div class="search-empty">No documents, receipts, or text screenshots found.</div>`;
docItems.forEach((rec, i)=>{
const card = document.createElement("div");
card.className = "result-card2";
card.onclick = (e)=>{ if(!e.target.closest(".btn-pdf")) openQuickView(rec); };
card.innerHTML = `<div class="result-thumb"><img loading="lazy" src="/api/thumbnail?path=${encodeURIComponent(rec.path)}">
<span class="result-cat-badge">${escapeHtml(rec.category || "Documents")}</span></div>
<div class="result-body">
<div class="result-name" title="${rec.name}">${escapeHtml(rec.name)}</div>
<div class="result-desc2">${escapeHtml(rec.desc || "")}</div>
<button class="btn-pdf btn-primary" style="margin-top:6px;width:100%;font-size:9px">📄 Convert & Download PDF</button>
</div>`;
card.querySelector(".btn-pdf").onclick = (e)=>{
e.stopPropagation();
showToast("Converting " + os_basename(rec.path) + " to PDF…", "info");
window.location.href = "/api/convert_pdf?path=" + encodeURIComponent(rec.path);
};
grid.appendChild(card);
});
if($("btnLoadMoreDocs")) $("btnLoadMoreDocs").style.display = docItems.length < docTotal ? "block" : "none";
}
if($("btnLoadMoreDocs")) $("btnLoadMoreDocs").onclick = ()=>loadDocuments(false);
if($("lbDownloadPdf")){
$("lbDownloadPdf").onclick = ()=>{
const rec = currentLbRec();
if(!rec) return;
showToast("Converting " + os_basename(rec.path) + " to PDF…", "info");
window.location.href = "/api/convert_pdf?path=" + encodeURIComponent(rec.path);
};
}
if($("btnDownloadSelectedPdf")){
$("btnDownloadSelectedPdf").onclick = ()=>{
if(selectedPaths.size === 0) return;
showToast(`Converting ${selectedPaths.size} image(s) to PDF ZIP…`, "info");
const params = new URLSearchParams({paths: [...selectedPaths].join(",")});
window.location.href = "/api/convert_pdf?" + params.toString();
};
}
/* ---- videos view ---- */
let videoItems = [], videoTotal = 0, videoLoading = false;
async function loadVideos(reset){