Add explicit Documents & Texts tab and native Image-to-PDF converter (/api/convert_pdf)
This commit is contained in:
65
index.html
65
index.html
@@ -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){
|
||||
|
||||
Reference in New Issue
Block a user