The classifier (bulk ZDDC rename) workflow runs as an embedded' + + ' iframe and requires the page be served by zddc-server.
' + + 'If you opened this file directly (file://), open the standalone'
+ + ' classifier.html tool instead — it provides the same'
+ + ' workflow against a local folder you pick from the file system.
The classifier (bulk ZDDC rename) workflow auto-serves at'
+ + ' working/, staging/, and'
+ + ' incoming/ URLs. The current page'
+ + ' (' + escapeHtml(window.location.pathname) + ') isn\'t'
+ + ' inside any of those, so classifier isn\'t available here.
Navigate browse into a working/ or staging/ folder, then' + + ' switch to Grid.
' + + ' for text; download button for everything else.
+// preview.js — file-preview rendering for the browse tool's right pane.
//
-// Lifecycle: a single popup window is reused across multiple file
-// clicks (state.previewWindow). Subsequent clicks rewrite its
-// contents instead of spawning a new window — same UX as the archive
-// tool.
+// Default flow: showFilePreview(node) renders into the inline preview
+// pane (#previewBody). Popup flow: showFilePreview(node, {popup:true})
+// opens a separate window — kept for users who want previews on a
+// second monitor.
+//
+// Rendering uses shared/preview-lib.js for content types it handles
+// (TIFF, ZIP listing, image-mime detection). PDF / HTML go in iframes;
+// text into a ; markdown into the dedicated markdown plugin
+// (preview-markdown.js); unknown extensions show a download button.
(function () {
'use strict';
@@ -13,9 +16,7 @@
var loader = window.app.modules.loader;
var preview = window.zddc && window.zddc.preview;
if (!preview) {
- // shared/preview-lib.js wasn't concatenated in. Bail loudly so
- // the bug shows up in console rather than mysteriously failing.
- console.error('[browse] zddc.preview not loaded — preview popup disabled.');
+ console.error('[browse] zddc.preview not loaded — preview disabled.');
}
function escapeHtml(s) {
@@ -32,15 +33,22 @@
'zip': 'application/zip',
'txt': 'text/plain', 'md': 'text/markdown', 'json': 'application/json',
'xml': 'application/xml', 'csv': 'text/csv', 'log': 'text/plain',
- 'js': 'text/javascript', 'css': 'text/css'
+ 'js': 'text/javascript', 'css': 'text/css',
+ 'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ 'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+ 'xls': 'application/vnd.ms-excel'
};
- // Pull bytes for a file node. Three sources:
- // - server URL (zddc-server-backed file, including downloads
- // of archived files served at real paths)
- // - FS-API handle (local folder)
- // - JSZip entry (file inside an expanded zip; reads from
- // parent's cached JSZip instance)
+ function getMime(ext) { return MIME[ext] || 'application/octet-stream'; }
+
+ function fmtSize(bytes) {
+ if (bytes == null) return '';
+ if (bytes < 1024) return bytes + ' B';
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
+ if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
+ return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
+ }
+
async function getArrayBuffer(node) {
if (node.zipParentId != null) {
var owner = state.nodes.get(node.zipParentId);
@@ -61,13 +69,6 @@
throw new Error('no source for file');
}
- function getMime(ext) {
- return MIME[ext] || 'application/octet-stream';
- }
-
- // Build a blob URL for the file's bytes. For server-mode regular
- // files (not in a zip), prefer the live URL — relative links and
- // server-side interception (e.g. .archive resolution) work then.
async function getBlobUrl(node) {
if (state.source === 'server' && node.url && node.zipParentId == null) {
return { url: node.url, fromServer: true };
@@ -77,14 +78,134 @@
return { url: URL.createObjectURL(blob), fromServer: false };
}
+ // ── Inline rendering ────────────────────────────────────────────────────
+
+ function renderEmpty(container, msg) {
+ container.innerHTML = '' + escapeHtml(msg) + '';
+ }
+
+ function renderError(container, msg) {
+ container.innerHTML = ''
+ + escapeHtml(msg) + '';
+ }
+
+ async function renderInline(node) {
+ var container = document.getElementById('previewBody');
+ var titleEl = document.getElementById('previewTitle');
+ var metaEl = document.getElementById('previewMeta');
+ var popoutBtn = document.getElementById('previewPopout');
+ if (!container) return;
+
+ if (titleEl) titleEl.textContent = node.name;
+ if (metaEl) {
+ var meta = [];
+ if (!node.isDir && !node.isZip) meta.push(fmtSize(node.size));
+ if (node.ext) meta.push(node.ext.toUpperCase());
+ metaEl.textContent = meta.join(' · ');
+ }
+ if (popoutBtn) popoutBtn.classList.remove('hidden');
+
+ var ext = (node.ext || '').toLowerCase();
+
+ // Markdown plugin (if loaded) takes over for .md / .markdown.
+ if ((ext === 'md' || ext === 'markdown') &&
+ window.app.modules.markdown &&
+ typeof window.app.modules.markdown.render === 'function') {
+ try {
+ await window.app.modules.markdown.render(node, container, { getArrayBuffer: getArrayBuffer });
+ } catch (e) {
+ renderError(container, 'Markdown render failed: ' + (e.message || e));
+ }
+ return;
+ }
+
+ // PDF / HTML → iframe.
+ if (ext === 'pdf' || ext === 'html' || ext === 'htm') {
+ try {
+ var info = await getBlobUrl(node);
+ var sandbox = (ext === 'pdf') ? '' : ' sandbox="allow-same-origin allow-popups allow-popups-to-escape-sandbox"';
+ container.innerHTML = '';
+ } catch (e) {
+ renderError(container, e.message || String(e));
+ }
+ return;
+ }
+
+ // Plain images (jpg/png/gif/webp/svg) →
. TIFF goes through preview-lib.
+ if (preview && preview.isImage(ext) && !preview.isTiff(ext)) {
+ try {
+ var imgInfo = await getBlobUrl(node);
+ container.innerHTML = '
';
+ } catch (e) {
+ renderError(container, e.message || String(e));
+ }
+ return;
+ }
+
+ if (preview && preview.isTiff(ext)) {
+ try {
+ var tiffBuf = await getArrayBuffer(node);
+ container.innerHTML = '';
+ await preview.renderTiff(document, container, tiffBuf, { fileName: node.name });
+ } catch (e) {
+ renderError(container, 'Failed to render TIFF: ' + (e.message || e));
+ }
+ return;
+ }
+
+ if (preview && preview.isZip(ext)) {
+ try {
+ var zipBuf = await getArrayBuffer(node);
+ container.innerHTML = '';
+ await preview.renderZipListing(document, container, zipBuf, { fileName: node.name });
+ } catch (e) {
+ renderError(container, 'Failed to read ZIP: ' + (e.message || e));
+ }
+ return;
+ }
+
+ if (preview && preview.isText(ext)) {
+ try {
+ var txtBuf = await getArrayBuffer(node);
+ var text = new TextDecoder('utf-8', { fatal: false }).decode(txtBuf);
+ var MAX = 200000;
+ if (text.length > MAX) {
+ text = text.substring(0, MAX) + '\n\n... (truncated, '
+ + (text.length - MAX) + ' more chars)';
+ }
+ container.innerHTML = '';
+ var pre = document.createElement('pre');
+ pre.className = 'preview-text';
+ pre.textContent = text;
+ container.appendChild(pre);
+ } catch (e) {
+ renderError(container, e.message || String(e));
+ }
+ return;
+ }
+
+ // Unknown type — offer a download link.
+ try {
+ var fallbackInfo = await getBlobUrl(node);
+ container.innerHTML =
+ ''
+ + 'No inline preview for .' + escapeHtml(ext) + '. '
+ + '
'
+ + 'Download ' + escapeHtml(node.name) + ''
+ + '';
+ } catch (e) {
+ renderError(container, 'No source for ' + node.name);
+ }
+ }
+
+ // ── Popup window (kept for "Pop out" button) ────────────────────────────
+
function popupShell(node, primaryUrl) {
var safeName = escapeHtml(node.name);
var safeHref = escapeHtml(primaryUrl);
var ext = (node.ext || '').toLowerCase();
- // Inline PDF and HTML previews load in iframes. HTML uses
- // sandbox="allow-same-origin allow-popups
- // allow-popups-to-escape-sandbox" — same posture as archive's
- // preview: links navigate, scripts blocked, popups allowed.
var contentHtml;
if (ext === 'pdf') {
contentHtml = '';
@@ -128,64 +249,47 @@
+ '' + 'script>