Fix model view for archives with long paths; preview inside archive-only models
- Long file names inside archives (deep folder paths) widened the details column until the viewer was crushed to a sliver and content ran off the modal. Both columns now keep their share (minmax(0, …)), long file names wrap, and viewer tabs truncate. - Archive entries show the file name first with its folder dimmed after. - A model whose only file is an archive opens the most useful file inside it (a slicer project, then other 3D files, then pictures/PDFs) instead of "No 3D preview for .none files". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
ea2afeac0e
commit
fbf1b706bf
3 changed files with 34 additions and 7 deletions
|
|
@ -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 = `<div class="entry-head">${data.entries.length} file${data.entries.length === 1 ? '' : 's'} inside</div>`
|
||||
+ data.entries.map((e) => `
|
||||
+ data.entries.map((e) => {
|
||||
const name = e.path.split('/').pop();
|
||||
const folder = e.path.replace(/^\.\//, '').slice(0, -name.length);
|
||||
return `
|
||||
<div class="entry-row">
|
||||
<span title="${escapeHtml(e.path)}">${escapeHtml(e.path.replace(/^\.\//, ''))} <em>${formatFileSize(e.size)}</em></span>
|
||||
<span title="${escapeHtml(e.path)}">${escapeHtml(name)}${folder ? `<span class="entry-folder">${escapeHtml(folder)}</span>` : ''} <em>${formatFileSize(e.size)}</em></span>
|
||||
<span class="row-actions">
|
||||
${PREVIEWABLE.includes(e.format) && e.format !== 'mtl' ? `<button type="button" class="link-btn" data-view-entry="${e.id}" data-archive="${fileId}">view</button>` : ''}
|
||||
<a href="/api/entries/${e.id}/raw" download>download</a>
|
||||
</span>
|
||||
</div>`).join('');
|
||||
</div>`;
|
||||
}).join('');
|
||||
if (!detailItems.length) previewFirstEntry(fileId, data.entries);
|
||||
} catch (err) {
|
||||
if (container()) container().innerHTML = `<div class="entry-head">Couldn't list this archive: ${escapeHtml(err.message)}</div>`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue