import { getModelApiClient } from '../../api/modelApiFactory.js';
import { downloadManager } from '../../managers/DownloadManager.js';
import { modalManager } from '../../managers/ModalManager.js';
import { showToast } from '../../utils/uiHelpers.js';
import { translate } from '../../utils/i18nHelpers.js';
import { state } from '../../state/index.js';
import { formatFileSize } from './utils.js';
const VIDEO_EXTENSIONS = ['.mp4', '.webm', '.mov', '.mkv'];
const PREVIEW_PLACEHOLDER_URL = '/loras_static/images/no-preview.png';
function buildCivitaiVersionUrl(modelId, versionId) {
if (modelId == null || versionId == null) {
return null;
}
const normalizedModelId = String(modelId).trim();
const normalizedVersionId = String(versionId).trim();
if (!normalizedModelId || !normalizedVersionId) {
return null;
}
const encodedModelId = encodeURIComponent(normalizedModelId);
const encodedVersionId = encodeURIComponent(normalizedVersionId);
return `https://civitai.com/models/${encodedModelId}?modelVersionId=${encodedVersionId}`;
}
function escapeHtml(value) {
if (value == null) {
return '';
}
return String(value)
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function extractExtension(value) {
if (value == null) {
return '';
}
const targets = [];
const stringValue = String(value);
if (stringValue) {
targets.push(stringValue);
stringValue.split(/[?&=]/).forEach(fragment => {
if (fragment) {
targets.push(fragment);
}
});
}
for (const target of targets) {
let candidate = target;
try {
candidate = decodeURIComponent(candidate);
} catch (error) {
// ignoring malformed sequences, fallback to raw value
}
const lastDot = candidate.lastIndexOf('.');
if (lastDot === -1) {
continue;
}
const extension = candidate.slice(lastDot).toLowerCase();
if (extension.includes('/') || extension.includes('\\')) {
continue;
}
return extension;
}
return '';
}
function isVideoUrl(url) {
if (!url || typeof url !== 'string') {
return false;
}
const candidates = new Set();
const addCandidate = value => {
if (value == null) {
return;
}
const stringValue = String(value);
if (!stringValue) {
return;
}
candidates.add(stringValue);
};
addCandidate(url);
try {
const parsed = new URL(url, window.location.origin);
addCandidate(parsed.pathname);
parsed.searchParams.forEach(value => addCandidate(value));
} catch (error) {
// ignore parse errors and rely on fallbacks below
}
for (const candidate of candidates) {
const extension = extractExtension(candidate);
if (extension && VIDEO_EXTENSIONS.includes(extension)) {
return true;
}
}
return false;
}
function formatDateLabel(value) {
if (!value) {
return null;
}
const parsed = new Date(value);
if (Number.isNaN(parsed.getTime())) {
return null;
}
return parsed.toLocaleDateString(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric',
});
}
/**
* Format EA end time as smart relative time
* - < 1 day: "in Xh" (hours)
* - 1-7 days: "in Xd" (days)
* - > 7 days: "Jan 15" (short date)
*/
function formatEarlyAccessTime(endsAt) {
if (!endsAt) {
return null;
}
const endDate = new Date(endsAt);
if (Number.isNaN(endDate.getTime())) {
return null;
}
const now = new Date();
const diffMs = endDate.getTime() - now.getTime();
const diffHours = diffMs / (1000 * 60 * 60);
const diffDays = diffHours / 24;
if (diffHours < 1) {
return translate('modals.model.versions.eaTime.endingSoon', {}, 'ending soon');
}
if (diffHours < 24) {
const hours = Math.ceil(diffHours);
return translate(
'modals.model.versions.eaTime.hours',
{ count: hours },
`in ${hours}h`
);
}
if (diffDays <= 7) {
const days = Math.ceil(diffDays);
return translate(
'modals.model.versions.eaTime.days',
{ count: days },
`in ${days}d`
);
}
// More than 7 days: show short date
return endDate.toLocaleDateString(undefined, {
month: 'short',
day: 'numeric',
});
}
function isEarlyAccessActive(version) {
// Two-phase detection:
// 1. Use pre-computed isEarlyAccess flag if available (from backend)
// 2. Otherwise check exact end time if available
if (typeof version.isEarlyAccess === 'boolean') {
return version.isEarlyAccess;
}
if (!version.earlyAccessEndsAt) {
return false;
}
try {
return new Date(version.earlyAccessEndsAt) > new Date();
} catch {
return false;
}
}
function buildMetaMarkup(version, options = {}) {
const segments = [];
if (version.baseModel) {
segments.push(
`${escapeHtml(version.baseModel)} `
);
}
const releaseLabel = formatDateLabel(version.releasedAt);
if (releaseLabel) {
segments.push(escapeHtml(releaseLabel));
}
if (typeof version.sizeBytes === 'number' && version.sizeBytes > 0) {
segments.push(escapeHtml(formatFileSize(version.sizeBytes)));
}
// Add early access info if applicable
if (options.showEarlyAccess && isEarlyAccessActive(version)) {
const eaTime = formatEarlyAccessTime(version.earlyAccessEndsAt);
if (eaTime) {
segments.push(` ${escapeHtml(eaTime)} `);
}
}
if (!segments.length) {
return escapeHtml(
translate('modals.model.versions.labels.noDetails', {}, 'No additional details')
);
}
return segments
.map(segment => `${segment} `)
.join('• ');
}
function buildBadge(label, tone) {
return `${escapeHtml(label)} `;
}
const DISPLAY_FILTER_MODES = Object.freeze({
SAME_BASE: 'same_base',
ANY: 'any',
});
const FILTER_LABEL_KEY = 'modals.model.versions.filters.label';
const FILTER_STATE_KEYS = {
[DISPLAY_FILTER_MODES.SAME_BASE]: 'modals.model.versions.filters.state.showSameBase',
[DISPLAY_FILTER_MODES.ANY]: 'modals.model.versions.filters.state.showAll',
};
const FILTER_TOOLTIP_KEYS = {
[DISPLAY_FILTER_MODES.SAME_BASE]: 'modals.model.versions.filters.tooltip.showAllVersions',
[DISPLAY_FILTER_MODES.ANY]: 'modals.model.versions.filters.tooltip.showSameBaseVersions',
};
function normalizeBaseModelName(value) {
if (typeof value !== 'string') {
return null;
}
const trimmed = value.trim();
if (!trimmed) {
return null;
}
return trimmed.toLowerCase();
}
function getToggleLabelText() {
return translate(FILTER_LABEL_KEY, {}, 'Base filter');
}
function getToggleStateText(mode) {
const key = FILTER_STATE_KEYS[mode] || FILTER_STATE_KEYS[DISPLAY_FILTER_MODES.ANY];
const fallback =
mode === DISPLAY_FILTER_MODES.SAME_BASE ? 'Same base' : 'All versions';
return translate(key, {}, fallback);
}
function getToggleTooltipText(mode) {
const key =
FILTER_TOOLTIP_KEYS[mode] || FILTER_TOOLTIP_KEYS[DISPLAY_FILTER_MODES.ANY];
const fallback =
mode === DISPLAY_FILTER_MODES.SAME_BASE
? 'Switch to showing all versions'
: 'Switch to showing only versions with the current base model';
return translate(key, {}, fallback);
}
function getDefaultDisplayMode() {
const strategy = state?.global?.settings?.update_flag_strategy;
return strategy === DISPLAY_FILTER_MODES.SAME_BASE
? DISPLAY_FILTER_MODES.SAME_BASE
: DISPLAY_FILTER_MODES.ANY;
}
function getCurrentVersionBaseModel(record, versionId) {
if (!record || typeof versionId !== 'number' || !Array.isArray(record.versions)) {
return {
normalized: null,
raw: null,
};
}
const currentVersion = record.versions.find(v => v.versionId === versionId);
if (!currentVersion) {
return {
normalized: null,
raw: null,
};
}
const baseModelRaw = currentVersion.baseModel ?? null;
return {
normalized: normalizeBaseModelName(baseModelRaw),
raw: baseModelRaw,
};
}
function resolveUpdateAvailability(record, baseModel, currentVersionId) {
if (!record) {
return false;
}
const strategy = state?.global?.settings?.update_flag_strategy;
const sameBaseMode = strategy === DISPLAY_FILTER_MODES.SAME_BASE;
const hideEarlyAccess = state?.global?.settings?.hide_early_access_updates;
if (!sameBaseMode) {
return Boolean(record?.hasUpdate);
}
const normalizedBase = normalizeBaseModelName(baseModel);
if (!normalizedBase || !Array.isArray(record.versions)) {
return false;
}
const normalizedCurrentVersionId =
typeof currentVersionId === 'number'
? currentVersionId
: currentVersionId
? Number(currentVersionId)
: null;
let threshold = null;
for (const version of record.versions) {
if (!version.isInLibrary) {
continue;
}
const versionBase = normalizeBaseModelName(version.baseModel);
if (versionBase !== normalizedBase) {
continue;
}
if (threshold === null || version.versionId > threshold) {
threshold = version.versionId;
}
}
if (threshold === null) {
threshold = normalizedCurrentVersionId;
}
if (threshold === null) {
return false;
}
return record.versions.some(version => {
if (version.isInLibrary || version.shouldIgnore) {
return false;
}
if (hideEarlyAccess && isEarlyAccessActive(version)) {
return false;
}
const versionBase = normalizeBaseModelName(version.baseModel);
if (versionBase !== normalizedBase) {
return false;
}
return typeof version.versionId === 'number' && version.versionId > threshold;
});
}
function getAutoplaySetting() {
try {
return Boolean(state?.global?.settings?.autoplay_on_hover);
} catch (error) {
return false;
}
}
function renderMediaMarkup(version) {
if (!version.previewUrl) {
const placeholderText = translate('modals.model.versions.media.placeholder', {}, 'No preview');
return `
${escapeHtml(placeholderText)}
`;
}
if (isVideoUrl(version.previewUrl)) {
const autoplayOnHover = getAutoplaySetting();
return `
`;
}
return `
`;
}
function renderDeletePreview(version, versionName) {
const previewUrl = version?.previewUrl;
if (previewUrl && isVideoUrl(previewUrl)) {
return `
`;
}
const imageUrl = previewUrl || PREVIEW_PLACEHOLDER_URL;
return ` `;
}
function renderRow(version, options) {
const { latestLibraryVersionId, currentVersionId, modelId: parentModelId } = options;
const isCurrent = currentVersionId && version.versionId === currentVersionId;
const isNewer =
typeof latestLibraryVersionId === 'number' &&
version.versionId > latestLibraryVersionId;
const isEarlyAccess = isEarlyAccessActive(version);
const badges = [];
if (isCurrent) {
badges.push(buildBadge(translate('modals.model.versions.badges.current', {}, 'Current Version'), 'current'));
}
if (version.isInLibrary) {
badges.push(buildBadge(translate('modals.model.versions.badges.inLibrary', {}, 'In Library'), 'success'));
} else if (isNewer && !version.shouldIgnore) {
badges.push(buildBadge(translate('modals.model.versions.badges.newer', {}, 'Newer Version'), 'info'));
}
if (isEarlyAccess) {
badges.push(buildBadge(translate('modals.model.versions.badges.earlyAccess', {}, 'Early Access'), 'early-access'));
}
if (version.shouldIgnore) {
badges.push(buildBadge(translate('modals.model.versions.badges.ignored', {}, 'Ignored'), 'muted'));
}
const downloadLabel = translate('modals.model.versions.actions.download', {}, 'Download');
const deleteLabel = translate('modals.model.versions.actions.delete', {}, 'Delete');
const ignoreLabel = translate(
version.shouldIgnore
? 'modals.model.versions.actions.unignore'
: 'modals.model.versions.actions.ignore',
{},
version.shouldIgnore ? 'Unignore' : 'Ignore'
);
const actions = [];
if (!version.isInLibrary) {
// Download button with optional EA bolt icon
const downloadIcon = isEarlyAccess ? ' ' : '';
actions.push(
`${downloadIcon}${escapeHtml(downloadLabel)} `
);
} else if (version.filePath) {
actions.push(
`${escapeHtml(deleteLabel)} `
);
}
actions.push(
`${escapeHtml(ignoreLabel)} `
);
const linkTarget = buildCivitaiVersionUrl(
version.modelId || parentModelId,
version.versionId
);
const civitaiTooltip = translate(
'modals.model.actions.viewOnCivitai',
{},
'View on Civitai'
);
const rowAttributes = [
`class="model-version-row${isCurrent ? ' is-current' : ''}${linkTarget ? ' is-clickable' : ''}${isEarlyAccess ? ' is-early-access' : ''}"`,
`data-version-id="${escapeHtml(version.versionId)}"`,
];
if (linkTarget) {
rowAttributes.push(`data-civitai-url="${escapeHtml(linkTarget)}"`);
rowAttributes.push(`title="${escapeHtml(civitaiTooltip)}"`);
}
return `
${renderMediaMarkup(version)}
${escapeHtml(version.name || translate('modals.model.versions.labels.unnamed', {}, 'Untitled Version'))}
${badges.join('')}
${buildMetaMarkup(version, { showEarlyAccess: true })}
${actions.join('')}
`;
}
function setupMediaHoverInteractions(container) {
const autoplayOnHover = getAutoplaySetting();
if (!autoplayOnHover) {
return;
}
container.querySelectorAll('.version-media video').forEach(video => {
if (video.dataset.autoplayOnHover !== 'true') {
return;
}
const play = () => {
try {
video.currentTime = 0;
const promise = video.play();
if (promise && typeof promise.catch === 'function') {
promise.catch(() => {});
}
} catch (error) {
console.debug('Failed to autoplay preview video:', error);
}
};
const stop = () => {
video.pause();
video.currentTime = 0;
};
video.addEventListener('mouseenter', play);
video.addEventListener('focus', play);
video.addEventListener('mouseleave', stop);
video.addEventListener('blur', stop);
});
}
function getLatestLibraryVersionId(record) {
if (!record || !Array.isArray(record.inLibraryVersionIds) || !record.inLibraryVersionIds.length) {
return null;
}
return Math.max(...record.inLibraryVersionIds);
}
function renderToolbar(record, toolbarState = {}) {
const ignoreText = record.shouldIgnore
? translate('modals.model.versions.actions.resumeModelUpdates', {}, 'Resume updates for this model')
: translate('modals.model.versions.actions.ignoreModelUpdates', {}, 'Ignore updates for this model');
const viewLocalText = translate('modals.model.versions.actions.viewLocalVersions', {}, 'View all local versions');
const infoText = translate(
'modals.model.versions.copy',
{ count: record.versions.length },
'Track and manage every version of this model in one place.'
);
const displayMode = toolbarState.displayMode || DISPLAY_FILTER_MODES.ANY;
const toggleLabel = getToggleLabelText();
const toggleState = getToggleStateText(displayMode);
const toggleTooltip = getToggleTooltipText(displayMode);
const filterActive = toolbarState.isFilteringActive ? 'true' : 'false';
const screenReaderText = [toggleLabel, toggleState].filter(Boolean).join(': ');
return `
`;
}
function renderEmptyState(container) {
const message = translate('modals.model.versions.empty', {}, 'No version history available for this model yet.');
container.innerHTML = `
`;
}
function renderFilteredEmptyState(baseModelLabel) {
const message = translate(
'modals.model.versions.filters.empty',
{ baseModel: baseModelLabel },
'No versions match the current base model filter.'
);
return `
`;
}
function renderErrorState(container, message) {
const fallback = translate('modals.model.versions.error', {}, 'Failed to load versions.');
container.innerHTML = `
${escapeHtml(message || fallback)}
`;
}
export function initVersionsTab({
modalId,
modelType,
modelId,
currentVersionId,
currentBaseModel,
onUpdateStatusChange,
}) {
const pane = document.querySelector(`#${modalId} #versions-tab`);
const container = pane ? pane.querySelector('.model-versions-tab') : null;
const normalizedCurrentVersionId =
typeof currentVersionId === 'number'
? currentVersionId
: currentVersionId
? Number(currentVersionId)
: null;
if (!container) {
return {
async load() {},
async refresh() {},
};
}
let controller = {
isLoading: false,
hasLoaded: false,
record: null,
};
const updateStatusChangeHandler =
typeof onUpdateStatusChange === 'function' ? onUpdateStatusChange : null;
let lastNotifiedUpdateState = null;
let displayMode = getDefaultDisplayMode();
let apiClient;
function ensureClient() {
if (apiClient) {
return apiClient;
}
try {
apiClient = getModelApiClient(modelType);
} catch (error) {
apiClient = getModelApiClient();
}
return apiClient;
}
function showLoading() {
const loadingText = translate('modals.model.loading.versions', {}, 'Loading versions...');
container.innerHTML = `
${escapeHtml(loadingText)}
`;
}
function render(record) {
controller.record = record;
controller.hasLoaded = true;
if (!record || !Array.isArray(record.versions) || record.versions.length === 0) {
renderEmptyState(container);
return;
}
const latestLibraryVersionId = getLatestLibraryVersionId(record);
const { normalized: currentBaseModelNormalized, raw: currentBaseModelLabel } =
getCurrentVersionBaseModel(record, normalizedCurrentVersionId);
const isFilteringActive =
displayMode === DISPLAY_FILTER_MODES.SAME_BASE &&
Boolean(currentBaseModelNormalized);
const sortedVersions = [...record.versions].sort(
(a, b) => Number(b.versionId) - Number(a.versionId)
);
const filteredVersions = sortedVersions.filter(version => {
if (!isFilteringActive) {
return true;
}
return normalizeBaseModelName(version.baseModel) === currentBaseModelNormalized;
});
const dividerThresholdVersionId = (() => {
if (!isFilteringActive) {
return latestLibraryVersionId;
}
const baseLocalVersionIds = record.versions
.filter(
version =>
version.isInLibrary &&
normalizeBaseModelName(version.baseModel) === currentBaseModelNormalized &&
typeof version.versionId === 'number'
)
.map(version => version.versionId);
if (!baseLocalVersionIds.length) {
return null;
}
return Math.max(...baseLocalVersionIds);
})();
let dividerInserted = false;
const rowsMarkup = filteredVersions
.map(version => {
let markup = '';
if (
!dividerInserted &&
typeof dividerThresholdVersionId === 'number' &&
!(version.versionId > dividerThresholdVersionId)
) {
dividerInserted = true;
markup += '
';
}
markup += renderRow(version, {
latestLibraryVersionId: dividerThresholdVersionId,
currentVersionId: normalizedCurrentVersionId,
modelId: record?.modelId ?? modelId,
});
return markup;
})
.join('');
const listContent =
rowsMarkup || renderFilteredEmptyState(currentBaseModelLabel);
container.innerHTML = `
${renderToolbar(record, {
displayMode,
isFilteringActive,
})}
${listContent}
`;
setupMediaHoverInteractions(container);
if (updateStatusChangeHandler) {
const resolvedFlag = resolveUpdateAvailability(
record,
currentBaseModel,
normalizedCurrentVersionId
);
if (resolvedFlag !== lastNotifiedUpdateState) {
lastNotifiedUpdateState = resolvedFlag;
updateStatusChangeHandler(resolvedFlag, record);
}
}
}
async function loadVersions({ forceRefresh = false, eager = false } = {}) {
if (controller.isLoading) {
return;
}
if (!modelId) {
renderErrorState(container, translate('modals.model.versions.missingModelId', {}, 'This model is missing a Civitai model id.'));
return;
}
if (controller.hasLoaded && !forceRefresh) {
return;
}
controller.isLoading = true;
if (!eager) {
showLoading();
}
try {
const client = ensureClient();
const response = await client.fetchModelUpdateVersions(modelId, {
refresh: false,
});
if (!response?.success) {
throw new Error(response?.error || 'Request failed');
}
render(response.record);
} catch (error) {
console.error('Failed to load model versions:', error);
renderErrorState(container, error?.message);
} finally {
controller.isLoading = false;
}
}
async function refresh() {
await loadVersions({ forceRefresh: true });
}
async function handleToggleModelIgnore(button) {
if (!controller.record) {
return;
}
const client = ensureClient();
const nextValue = !controller.record.shouldIgnore;
button.disabled = true;
try {
const response = await client.setModelUpdateIgnore(modelId, nextValue);
if (!response?.success) {
throw new Error(response?.error || 'Request failed');
}
render(response.record);
const toastKey = nextValue
? 'modals.model.versions.toast.modelIgnored'
: 'modals.model.versions.toast.modelResumed';
const toastMessage = translate(
toastKey,
{},
nextValue ? 'Updates ignored for this model' : 'Update tracking resumed'
);
showToast(toastMessage, {}, 'success');
} catch (error) {
console.error('Failed to update model ignore state:', error);
showToast(error?.message || 'Failed to update ignore preference', {}, 'error');
} finally {
button.disabled = false;
}
}
function handleToggleVersionDisplayMode() {
displayMode =
displayMode === DISPLAY_FILTER_MODES.SAME_BASE
? DISPLAY_FILTER_MODES.ANY
: DISPLAY_FILTER_MODES.SAME_BASE;
if (!controller.record) {
return;
}
render(controller.record);
}
async function handleToggleVersionIgnore(button, versionId) {
if (!controller.record) {
return;
}
const client = ensureClient();
const targetVersion = controller.record.versions.find(v => v.versionId === versionId);
const nextValue = targetVersion ? !targetVersion.shouldIgnore : true;
button.disabled = true;
try {
const response = await client.setVersionUpdateIgnore(
modelId,
versionId,
nextValue
);
if (!response?.success) {
throw new Error(response?.error || 'Request failed');
}
render(response.record);
const updatedVersion = response.record.versions.find(v => v.versionId === versionId);
const toastKey = updatedVersion?.shouldIgnore
? 'modals.model.versions.toast.versionIgnored'
: 'modals.model.versions.toast.versionUnignored';
const toastMessage = translate(
toastKey,
{},
updatedVersion?.shouldIgnore ? 'Updates ignored for this version' : 'Version re-enabled'
);
showToast(toastMessage, {}, 'success');
} catch (error) {
console.error('Failed to toggle version ignore state:', error);
showToast(error?.message || 'Failed to update version preference', {}, 'error');
} finally {
button.disabled = false;
}
}
async function performDeleteVersion({
triggerButton,
confirmButton,
closeModal,
version,
}) {
if (!version?.filePath) {
console.warn('Missing file path for deletion.');
return;
}
if (triggerButton) {
triggerButton.disabled = true;
}
let confirmOriginalText = '';
if (confirmButton) {
confirmOriginalText = confirmButton.textContent;
confirmButton.disabled = true;
}
let deletionSucceeded = false;
try {
const client = ensureClient();
await client.deleteModel(version.filePath);
deletionSucceeded = true;
showToast(
translate('modals.model.versions.toast.versionDeleted', {}, 'Version deleted'),
{},
'success'
);
} catch (error) {
console.error('Failed to delete version:', error);
showToast(error?.message || 'Failed to delete version', {}, 'error');
} finally {
if (triggerButton && document.body.contains(triggerButton)) {
triggerButton.disabled = false;
}
if (
confirmButton &&
document.body.contains(confirmButton) &&
!deletionSucceeded
) {
confirmButton.disabled = false;
if (confirmOriginalText) {
confirmButton.textContent = confirmOriginalText;
}
}
}
if (!deletionSucceeded) {
return;
}
if (typeof closeModal === 'function') {
closeModal();
}
await refresh();
}
function showDeleteVersionModal(version, triggerButton) {
const modalRecord = modalManager?.getModal?.('deleteModal');
if (!modalRecord?.element) {
return false;
}
const deleteLabel = translate('modals.model.versions.actions.delete', {}, 'Delete');
const cancelLabel = translate('common.actions.cancel', {}, 'Cancel');
const title = translate('modals.model.versions.actions.delete', {}, 'Delete');
const confirmMessage = translate(
'modals.model.versions.confirm.delete',
{},
'Delete this version from your library?'
);
const versionName =
version.name ||
translate('modals.model.versions.labels.unnamed', {}, 'Untitled Version');
const metaMarkup = buildMetaMarkup(version);
const previewMarkup = renderDeletePreview(version, versionName);
const modalElement = modalRecord.element;
const originalMarkup = modalElement.innerHTML;
const content = `
${escapeHtml(title)}
${escapeHtml(confirmMessage)}
${previewMarkup}
${escapeHtml(versionName)}
${
version.baseModel
? `
${escapeHtml(version.baseModel)}
`
: ''
}
${metaMarkup ? `
${metaMarkup}
` : ''}
${escapeHtml(cancelLabel)}
${escapeHtml(deleteLabel)}
`;
const cleanupHandlers = [];
modalManager.showModal(
'deleteModal',
content,
null,
() => {
cleanupHandlers.forEach(handler => {
try {
handler();
} catch (error) {
console.error('Failed to cleanup delete modal handler:', error);
}
});
cleanupHandlers.length = 0;
modalElement.innerHTML = originalMarkup;
delete modalElement.dataset.versionId;
}
);
modalElement.dataset.versionId = String(version.versionId ?? '');
const cancelButton = modalElement.querySelector('.cancel-btn');
const confirmButton = modalElement.querySelector('.delete-btn');
const closeModal = () => modalManager.closeModal('deleteModal');
if (cancelButton) {
const handleCancel = event => {
event.preventDefault();
closeModal();
};
cancelButton.addEventListener('click', handleCancel);
cleanupHandlers.push(() => {
cancelButton.removeEventListener('click', handleCancel);
});
}
if (confirmButton) {
const handleConfirm = async event => {
event.preventDefault();
await performDeleteVersion({
triggerButton,
confirmButton,
closeModal,
version,
});
};
confirmButton.addEventListener('click', handleConfirm);
cleanupHandlers.push(() => {
confirmButton.removeEventListener('click', handleConfirm);
});
}
return true;
}
async function handleDeleteVersion(button, versionId) {
if (!controller.record) {
return;
}
const version = controller.record.versions.find(item => item.versionId === versionId);
if (!version) {
console.warn('Target version missing from record for delete:', versionId);
return;
}
if (showDeleteVersionModal(version, button)) {
return;
}
const confirmText = translate(
'modals.model.versions.confirm.delete',
{},
'Delete this version from your library?'
);
if (!window.confirm(confirmText)) {
return;
}
await performDeleteVersion({
triggerButton: button,
version,
});
}
async function handleDownloadVersion(button, versionId) {
if (!controller.record) {
return;
}
const version = controller.record.versions.find(item => item.versionId === versionId);
if (!version) {
console.warn('Target version missing from record for download:', versionId);
return;
}
button.disabled = true;
try {
const success = await downloadManager.downloadVersionWithDefaults(modelType, modelId, versionId, {
versionName: version.name || `#${version.versionId}`,
});
if (success) {
await refresh();
}
} catch (error) {
console.error('Failed to start direct download for version:', error);
} finally {
if (document.body.contains(button)) {
button.disabled = false;
}
}
}
container.addEventListener('click', async event => {
const toolbarAction = event.target.closest('[data-versions-action]');
if (toolbarAction) {
const action = toolbarAction.dataset.versionsAction;
switch (action) {
case 'toggle-model-ignore':
event.preventDefault();
await handleToggleModelIgnore(toolbarAction);
break;
case 'toggle-version-display-mode':
event.preventDefault();
handleToggleVersionDisplayMode();
break;
default:
break;
}
return;
}
const actionButton = event.target.closest('[data-version-action]');
if (actionButton) {
const row = actionButton.closest('.model-version-row');
if (!row) {
return;
}
const versionId = Number(row.dataset.versionId);
const action = actionButton.dataset.versionAction;
switch (action) {
case 'download':
event.preventDefault();
await handleDownloadVersion(actionButton, versionId);
break;
case 'delete':
event.preventDefault();
await handleDeleteVersion(actionButton, versionId);
break;
case 'toggle-ignore':
event.preventDefault();
await handleToggleVersionIgnore(actionButton, versionId);
break;
default:
break;
}
return;
}
const row = event.target.closest('.model-version-row.is-clickable');
if (!row) {
return;
}
if (event.target.closest('button')) {
return;
}
if (event.target.closest('.version-actions')) {
return;
}
if (event.target.closest('a')) {
return;
}
const targetUrl = row.dataset.civitaiUrl;
if (!targetUrl) {
return;
}
event.preventDefault();
window.open(targetUrl, '_blank', 'noopener,noreferrer');
});
return {
load: options => loadVersions(options),
refresh,
};
}