feat: replace model details image gallery with draggable horizontal strip

This commit is contained in:
Dariusz L
2026-06-08 01:06:04 +02:00
parent 511cf4ff1d
commit 9bebb52395
2 changed files with 115 additions and 39 deletions
+39 -13
View File
@@ -3785,25 +3785,47 @@
}
.mr-model-details-gallery {
position: relative;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 18px;
align-items: stretch;
min-height: 420px;
height: min(640px, calc(100vh - 190px));
min-height: 440px;
height: min(660px, calc(100vh - 190px));
margin-top: 2px;
overflow: hidden;
}
.mr-model-details-gallery.is-single {
grid-template-columns: minmax(280px, 520px);
justify-content: center;
max-width: 560px;
margin-left: auto;
margin-right: auto;
}
.mr-model-details-gallery-strip {
display: flex;
align-items: center;
gap: 16px;
width: 100%;
height: 100%;
min-width: 0;
overflow-x: auto;
overflow-y: hidden;
scroll-behavior: smooth;
scrollbar-width: none;
cursor: grab;
user-select: none;
-webkit-user-select: none;
padding: 0 2px;
}
.mr-model-details-gallery-strip::-webkit-scrollbar {
display: none;
}
.mr-model-details-gallery-strip.is-dragging {
cursor: grabbing;
scroll-behavior: auto;
}
.mr-model-details-image {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
flex: 0 0 min(var(--mr-image-width, 330px), 82vw);
width: min(var(--mr-image-width, 330px), 82vw);
height: min(var(--mr-image-height, 430px), 100%);
min-width: 0;
min-height: 0;
margin: 0;
@@ -3813,6 +3835,10 @@
background: #101114;
overflow: hidden;
cursor: zoom-in;
aspect-ratio: var(--mr-image-ratio, 0.72);
}
.mr-model-details-gallery-strip.is-dragging .mr-model-details-image {
cursor: grabbing;
}
.mr-model-details-image img {
display: block;
@@ -4015,12 +4041,12 @@
font-size: 18px;
}
.mr-model-details-gallery {
grid-template-columns: 1fr;
min-height: 360px;
height: min(560px, calc(100vh - 170px));
}
.mr-model-details-gallery .mr-model-details-image + .mr-model-details-image {
display: none;
.mr-model-details-image {
flex-basis: min(var(--mr-image-width, 330px), 88vw);
width: min(var(--mr-image-width, 330px), 88vw);
}
.mr-model-details-file {
grid-template-columns: 1fr;
+76 -26
View File
@@ -1191,13 +1191,6 @@ export const modelInfoMethods = {
const selectedVersionId = String(data.selected_version?.id || data.version_id || versions[0]?.id || '');
const selectedVersion = versions.find(version => String(version.id) === selectedVersionId) || data.selected_version || versions[0] || {};
const images = (Array.isArray(selectedVersion.images) && selectedVersion.images.length ? selectedVersion.images : data.images || []).filter(img => img?.url).slice(0, 12);
const imageOffset = Math.max(0, Math.min(Number(data._detailsImageOffset || 0), Math.max(0, images.length - 1)));
const visibleImages = images.length <= 2
? images.map((image, index) => ({ image, index }))
: [0, 1].map(step => {
const index = (imageOffset + step) % images.length;
return { image: images[index], index };
});
const stats = data.stats || {};
const versionStats = selectedVersion.stats || {};
const tags = Array.isArray(data.tags) ? data.tags.slice(0, 12) : [];
@@ -1237,15 +1230,17 @@ export const modelInfoMethods = {
</button>
`).join('')}
</div>
<div class="mr-model-details-gallery ${visibleImages.length === 1 ? 'is-single' : ''}">
${images.length > 2 ? `<button type="button" class="mr-model-details-gallery-nav is-left" data-gallery-direction="-1" aria-label="Previous images">&lsaquo;</button>` : ''}
${images.length ? visibleImages.map(({ image, index }) => `
<button type="button" class="mr-model-details-image" data-image-index="${index}">
<img src="${this.escapeHtml(image.url)}" alt="Example image" loading="lazy">
${this.renderSourceModelImageMeta(image)}
</button>
`).join('') : '<div class="mr-model-details-empty">No example images available.</div>'}
${images.length > 2 ? `<button type="button" class="mr-model-details-gallery-nav is-right" data-gallery-direction="1" aria-label="Next images">&rsaquo;</button>` : ''}
<div class="mr-model-details-gallery ${images.length === 1 ? 'is-single' : ''}">
${images.length > 1 ? `<button type="button" class="mr-model-details-gallery-nav is-left" data-gallery-direction="-1" aria-label="Previous images">&lsaquo;</button>` : ''}
<div class="mr-model-details-gallery-strip">
${images.length ? images.map((image, index) => `
<button type="button" class="mr-model-details-image" style="${this.getSourceModelImageSizingStyle(image)}" data-image-index="${index}">
<img src="${this.escapeHtml(image.url)}" alt="Example image" loading="lazy" draggable="false">
${this.renderSourceModelImageMeta(image)}
</button>
`).join('') : '<div class="mr-model-details-empty">No example images available.</div>'}
</div>
${images.length > 1 ? `<button type="button" class="mr-model-details-gallery-nav is-right" data-gallery-direction="1" aria-label="Next images">&rsaquo;</button>` : ''}
</div>
<div class="mr-model-details-description">
<h3>About</h3>
@@ -1336,6 +1331,28 @@ export const modelInfoMethods = {
`;
},
getSourceModelImageSizingStyle(image = {}) {
const width = Number(image.width || image.metadata?.width || 0);
const height = Number(image.height || image.metadata?.height || 0);
const ratio = width > 0 && height > 0 ? width / height : 0.72;
const clampedRatio = Math.max(0.48, Math.min(1.85, ratio));
const preferredHeight = Math.max(
320,
Math.min(
560,
Math.round(430 + Math.max(0, 1 - clampedRatio) * 150 - Math.max(0, clampedRatio - 1) * 70)
)
);
let preferredWidth = Math.round(preferredHeight * clampedRatio);
if (preferredWidth < 220) {
preferredWidth = 220;
} else if (preferredWidth > 640) {
preferredWidth = 640;
}
const adjustedHeight = Math.round(preferredWidth / clampedRatio);
return `--mr-image-ratio:${clampedRatio.toFixed(3)}; --mr-image-width:${preferredWidth}px; --mr-image-height:${adjustedHeight}px;`;
},
bindSourceModelDetailsEvents(details) {
if (!details) return;
if (details.dataset.modelDetailsBound === 'true') return;
@@ -1358,7 +1375,6 @@ export const modelInfoMethods = {
const data = details._detailsData;
if (data) {
data.selected_version = (data.versions || []).find(version => String(version.id) === details._selectedVersionId) || data.selected_version;
data._detailsImageOffset = 0;
details.innerHTML = this.renderSourceModelDetails(data, details._sourceModel || {});
this.bindTooltips(details);
}
@@ -1367,21 +1383,21 @@ export const modelInfoMethods = {
const galleryNav = event.target.closest('.mr-model-details-gallery-nav');
if (galleryNav && details.contains(galleryNav)) {
const data = details._detailsData || {};
const version = data.selected_version || {};
const images = (Array.isArray(version.images) && version.images.length ? version.images : data.images || []).filter(img => img?.url);
if (images.length <= 2) return;
const strip = galleryNav.closest('.mr-model-details-gallery')?.querySelector('.mr-model-details-gallery-strip');
if (!strip) return;
const direction = Number(galleryNav.dataset.galleryDirection || 1);
const current = Number(data._detailsImageOffset || 0);
data._detailsImageOffset = (current + direction + images.length) % images.length;
details.innerHTML = this.renderSourceModelDetails(data, details._sourceModel || {});
this.bindTooltips(details);
strip.scrollBy({
left: direction * Math.max(260, Math.round(strip.clientWidth * 0.82)),
behavior: 'smooth'
});
return;
}
const imageBtn = event.target.closest('.mr-model-details-image');
if (imageBtn && details.contains(imageBtn)) {
if (details._detailsGalleryDraggedUntil && Date.now() < details._detailsGalleryDraggedUntil) {
return;
}
const data = details._detailsData || {};
const version = data.selected_version || {};
const images = (Array.isArray(version.images) && version.images.length ? version.images : data.images || []).filter(img => img?.url);
@@ -1402,6 +1418,40 @@ export const modelInfoMethods = {
}
}
});
details.addEventListener('pointerdown', (event) => {
const strip = event.target.closest?.('.mr-model-details-gallery-strip');
if (!strip || !details.contains(strip) || event.button > 0) return;
const startX = event.clientX;
const startScrollLeft = strip.scrollLeft;
let moved = false;
strip.classList.add('is-dragging');
strip.setPointerCapture?.(event.pointerId);
const onPointerMove = (moveEvent) => {
const delta = moveEvent.clientX - startX;
if (Math.abs(delta) > 4) {
moved = true;
details._detailsGalleryDraggedUntil = Date.now() + 250;
}
strip.scrollLeft = startScrollLeft - delta;
};
const finishDrag = () => {
strip.classList.remove('is-dragging');
strip.removeEventListener('pointermove', onPointerMove);
strip.removeEventListener('pointerup', finishDrag);
strip.removeEventListener('pointercancel', finishDrag);
if (moved) {
details._detailsGalleryDraggedUntil = Date.now() + 250;
}
};
strip.addEventListener('pointermove', onPointerMove);
strip.addEventListener('pointerup', finishDrag, { once: true });
strip.addEventListener('pointercancel', finishDrag, { once: true });
});
},
applySourceModelDetailsSelection(selection = {}, contextModel = {}) {