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;
}