diff --git a/public/app.js b/public/app.js index 4675d2d..70e3cb8 100644 --- a/public/app.js +++ b/public/app.js @@ -993,8 +993,13 @@ function openModelDetail(modelId) { if (detailItems.length) { loadViewer(detailItems[0]); } else { + // A model that's only archives previews a file from inside once they're listed. + const hasArchive = model.files.some((f) => ARCHIVE_FORMATS.includes(f.format)); document.getElementById('regenThumbBtn').style.display = 'none'; - window.ModelViewer.render(document.getElementById('detailViewer'), '', 'none', { fallbackImage: currentDetailThumb }); + window.ModelViewer.render(document.getElementById('detailViewer'), '', 'none', { + fallbackImage: currentDetailThumb, + message: hasArchive ? 'Opening a file from the archive…' : 'Nothing here can be previewed yet — add files below.', + }); } } @@ -1038,6 +1043,20 @@ function resolverFor(model, archiveEntries = null) { return (name) => byName.get(key(name)) || null; } +// For a model with nothing to preview directly: open the most useful file +// inside its archive — a slicer project, then other 3D files, then pictures/PDFs. +const ENTRY_PREVIEW_ORDER = ['3mf', 'stl', 'obj', 'step', 'stp', 'fbx', '3dm', 'jpg', 'jpeg', 'png', 'webp', 'gif', 'pdf']; +function previewFirstEntry(archiveId, entries) { + const rank = (e) => (ENTRY_PREVIEW_ORDER.includes(e.format) ? ENTRY_PREVIEW_ORDER.indexOf(e.format) : Infinity); + const best = [...entries].sort((a, b) => rank(a) - rank(b))[0]; + if (!best || rank(best) === Infinity) { + window.ModelViewer.render(document.getElementById('detailViewer'), '', 'none', + { fallbackImage: currentDetailThumb, message: 'Nothing inside this archive can be previewed.' }); + return; + } + openViewerItem({ kind: 'entry', id: best.id, format: best.format, name: best.path.split('/').pop(), archiveId }); +} + async function loadArchiveEntries(modelId, fileId) { const container = () => (fileDetailModal.dataset.currentModelId === String(modelId) ? document.querySelector(`[data-entries-for="${fileId}"]`) : null); @@ -1057,14 +1076,19 @@ async function loadArchiveEntries(modelId, fileId) { archiveEntryCache.set(fileId, data.entries); const { PREVIEWABLE } = window.ModelViewer; container().innerHTML = `
${data.entries.length} file${data.entries.length === 1 ? '' : 's'} inside
` - + data.entries.map((e) => ` + + data.entries.map((e) => { + const name = e.path.split('/').pop(); + const folder = e.path.replace(/^\.\//, '').slice(0, -name.length); + return `
- ${escapeHtml(e.path.replace(/^\.\//, ''))} ${formatFileSize(e.size)} + ${escapeHtml(name)}${folder ? `${escapeHtml(folder)}` : ''} ${formatFileSize(e.size)} ${PREVIEWABLE.includes(e.format) && e.format !== 'mtl' ? `` : ''} download -
`).join(''); + `; + }).join(''); + if (!detailItems.length) previewFirstEntry(fileId, data.entries); } catch (err) { if (container()) container().innerHTML = `
Couldn't list this archive: ${escapeHtml(err.message)}
`; } diff --git a/public/styles.css b/public/styles.css index f7680f3..4058bc2 100644 --- a/public/styles.css +++ b/public/styles.css @@ -205,7 +205,8 @@ label { display: block; font-size: 13px; font-weight: 500; color: var(--text-mut .form-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 22px; } /* ---- model detail ---- */ -.file-detail-content { display: grid; grid-template-columns: 1.1fr 1fr; gap: 26px; } +/* minmax(0, …) keeps a long file name from widening its column and crushing the viewer */ +.file-detail-content { display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr); gap: 26px; } .file-detail-preview { display: flex; flex-direction: column; gap: 10px; } .model-viewer { width: 100%; height: 380px; background: radial-gradient(circle at 50% 40%, #232730 0%, #131519 100%); border: 1px solid var(--border); border-radius: var(--radius); display: flex; align-items: center; justify-content: center; } .model-viewer canvas { display: block; border-radius: var(--radius); } @@ -224,6 +225,7 @@ label { display: block; font-size: 13px; font-weight: 500; color: var(--text-mut .info-section .entry-row { display: flex; justify-content: space-between; align-items: center; gap: 10px; padding: 3px 0; font-size: 13px; color: var(--text-muted); } .entry-row > span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; } .entry-row em { color: var(--text-dim); font-style: normal; font-family: var(--mono); font-size: 11px; margin-left: 4px; } +.entry-row .entry-folder { color: var(--text-dim); margin-left: 4px; } .row-actions { display: flex; gap: 12px; flex-shrink: 0; align-items: center; } .link-btn { background: none; border: none; color: var(--accent); font: inherit; cursor: pointer; padding: 0; } .link-btn:hover { text-decoration: underline; } @@ -240,7 +242,7 @@ label { display: block; font-size: 13px; font-weight: 500; color: var(--text-mut .swatch { width: 16px; height: 16px; border-radius: 50%; border: 1px solid rgba(255, 255, 255, 0.25); flex-shrink: 0; } .file-tabs { display: flex; flex-wrap: wrap; gap: 6px; } -.file-tab { border: 1px solid var(--border); background: var(--surface-2); color: var(--text-muted); padding: 5px 11px; border-radius: var(--radius-sm); font-size: 12px; cursor: pointer; font-family: inherit; } +.file-tab { border: 1px solid var(--border); background: var(--surface-2); color: var(--text-muted); padding: 5px 11px; border-radius: var(--radius-sm); font-size: 12px; cursor: pointer; font-family: inherit; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .file-tab:hover { color: var(--text); } .file-tab.active { background: var(--accent); color: var(--accent-text); border-color: var(--accent); } @@ -253,6 +255,7 @@ label { display: block; font-size: 13px; font-weight: 500; color: var(--text-mut .file-row { display: flex; justify-content: space-between; align-items: center; gap: 10px; padding: 7px 0; border-bottom: 1px solid var(--border); font-size: 14px; color: var(--text-muted); } .file-row:last-child { border-bottom: none; } .file-row em { color: var(--text-dim); font-style: normal; font-family: var(--mono); font-size: 12px; } +.file-row > span:first-child { min-width: 0; overflow-wrap: anywhere; } /* ---- import / settings ---- */ .import-status { margin-top: 14px; color: var(--text-muted); font-size: 14px; min-height: 20px; } diff --git a/public/viewer.js b/public/viewer.js index c4c0251..b677797 100644 --- a/public/viewer.js +++ b/public/viewer.js @@ -193,7 +193,7 @@ async function render(container, url, format, opts = {}) { return; } if (!MODEL_FORMATS.includes(format)) { - showFallback(container, opts.fallbackImage, `No 3D preview for .${format} files.`); + showFallback(container, opts.fallbackImage, opts.message || `No 3D preview for .${format} files.`); return; }