/**
* VerticalListView.js
* Classic vertical example list for the model modal showcase: every example
* rendered full-width and stacked, each with its own metadata panel and media
* controls. Revived from the pre-gallery implementation (removed in v1.2.2),
* adapted to the current shared showcase infrastructure (MediaRenderers,
* MediaUtils) — no legacy scroll/metrics machinery.
*/
import {
initLazyLoading,
initNsfwBlurHandlers,
initMetadataPanelHandlers,
initMediaControlHandlers,
positionAllMediaControls
} from './MediaUtils.js';
import { renderShowcaseMediaItem, findLocalFile } from './MediaRenderers.js';
import { getShowcaseUrl } from '../../../utils/civitaiUtils.js';
import { openMediaViewer } from '../MediaViewer.js';
/**
* Render the vertical list: all examples stacked in a column
* @param {Array} images - Filtered images/videos to show
* @param {Array} exampleFiles - Local example files
* @returns {string} HTML content
*/
export function renderVerticalList(images, exampleFiles = []) {
return `
${images.map((img, index) => renderVerticalMediaItem(img, index, exampleFiles)).join('')}
`;
}
/**
* Render a single vertical-list item. The wrapper is sized by the
* padding-bottom aspect-ratio hack so the column layout is stable before any
* media loads (and lazy loading has a box to intersect).
* @param {Object} img - Image/video metadata
* @param {number} index - Index in the array
* @param {Array} exampleFiles - Local files
* @returns {string} HTML for the media item
*/
function renderVerticalMediaItem(img, index, exampleFiles) {
return renderShowcaseMediaItem(img, index, exampleFiles, {
heightPercent: computeHeightPercent(img),
fullQuality: true,
});
}
/**
* Compute the wrapper height as a padding-bottom percent of the modal width,
* clamped so very tall or very wide media still gets a reasonable box
* (defensive 4:3 fallback for missing dimensions prevents NaN layout)
* @param {Object} img - Image/video metadata
* @returns {number} padding-bottom percent
*/
function computeHeightPercent(img) {
const safeW = img.width || 4;
const safeH = img.height || 3;
const aspectRatio = (safeH / safeW) * 100;
const containerWidth = 800; // modal content maximum width
const minHeightPercent = 40;
const maxHeightPercent = (window.innerHeight * 0.6 / containerWidth) * 100;
return Math.max(minHeightPercent, Math.min(maxHeightPercent, aspectRatio));
}
/**
* Build the item list for the full-size media viewer from the rendered items
* @param {Array} images - Rendered images/videos
* @param {Array} exampleFiles - Local example files
* @returns {Array<{url: string, type: string}>}
*/
function buildViewerItems(images, exampleFiles) {
return images.map((img, index) => {
const localFile = findLocalFile(img, index, exampleFiles);
const originalRemoteUrl = img.url || '';
const isVideo = localFile ? localFile.is_video :
originalRemoteUrl.endsWith('.mp4') || originalRemoteUrl.endsWith('.webm');
return {
url: localFile?.path || getShowcaseUrl(originalRemoteUrl, isVideo ? 'video' : 'image'),
type: isVideo ? 'video' : 'image'
};
});
}
/**
* Initialize all vertical-list interactions: lazy loading, NSFW blur,
* per-item metadata panels and media controls, click-to-view
* @param {HTMLElement} container - The .showcase-vertical root element
* @param {Array} images - Rendered images/videos (aligned with DOM order)
* @param {Array} exampleFiles - Local example files
*/
export function initVerticalList(container, images, exampleFiles = []) {
if (!container) return;
initLazyLoading(container);
initNsfwBlurHandlers(container);
initMetadataPanelHandlers(container);
initMediaControlHandlers(container);
positionAllMediaControls(container);
// Click-to-view: open the full-size media viewer at the clicked item
const viewerItems = buildViewerItems(images, exampleFiles);
container.querySelectorAll('.vertical-list-container .media-wrapper').forEach((wrapper, index) => {
const mediaEl = wrapper.querySelector('img, video');
if (!mediaEl) return;
mediaEl.addEventListener('click', (e) => {
e.stopPropagation();
openMediaViewer(viewerItems, index);
});
});
// Reposition controls once media dimensions are known
container.querySelectorAll('.vertical-list-container img, .vertical-list-container video').forEach(media => {
media.addEventListener('load', () => positionAllMediaControls(container));
if (media.tagName === 'VIDEO') {
media.addEventListener('loadedmetadata', () => positionAllMediaControls(container));
}
});
}