diff --git a/locales/de.json b/locales/de.json index 860602f4..d2ffa18f 100644 --- a/locales/de.json +++ b/locales/de.json @@ -145,6 +145,10 @@ }, "usage": { "timesUsed": "Verwendungsanzahl" + }, + "footer": { + "versionCount": "{count} Versionen", + "viewAllVersions": "Alle lokalen Versionen anzeigen" } }, "globalContextMenu": { @@ -675,7 +679,10 @@ "sizeAsc": "Kleinste", "usage": "Anzahl Nutzung", "usageDesc": "Meiste", - "usageAsc": "Wenigste" + "usageAsc": "Wenigste", + "versionsCount": "Lokale Versionen", + "versionsCountDesc": "Meiste Versionen zuerst", + "versionsCountAsc": "Wenigste Versionen zuerst" }, "refresh": { "title": "Modelliste aktualisieren", diff --git a/locales/en.json b/locales/en.json index 91d1f28f..36b5aacd 100644 --- a/locales/en.json +++ b/locales/en.json @@ -145,6 +145,10 @@ }, "usage": { "timesUsed": "Times used" + }, + "footer": { + "versionCount": "{count} versions", + "viewAllVersions": "View all local versions" } }, "globalContextMenu": { @@ -675,7 +679,10 @@ "sizeAsc": "Smallest", "usage": "Use Count", "usageDesc": "Most", - "usageAsc": "Least" + "usageAsc": "Least", + "versionsCount": "Local Versions", + "versionsCountDesc": "Most versions first", + "versionsCountAsc": "Fewest versions first" }, "refresh": { "title": "Refresh model list", diff --git a/locales/es.json b/locales/es.json index 8ea639a7..7a6f9289 100644 --- a/locales/es.json +++ b/locales/es.json @@ -145,6 +145,10 @@ }, "usage": { "timesUsed": "Veces usado" + }, + "footer": { + "versionCount": "{count} versiones", + "viewAllVersions": "Ver todas las versiones locales" } }, "globalContextMenu": { @@ -675,7 +679,10 @@ "sizeAsc": "Menor", "usage": "Número de usos", "usageDesc": "Más", - "usageAsc": "Menos" + "usageAsc": "Menos", + "versionsCount": "Versiones locales", + "versionsCountDesc": "Más versiones primero", + "versionsCountAsc": "Menos versiones primero" }, "refresh": { "title": "Actualizar lista de modelos", diff --git a/locales/fr.json b/locales/fr.json index a53052cd..8226802c 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -145,6 +145,10 @@ }, "usage": { "timesUsed": "Nombre d'utilisations" + }, + "footer": { + "versionCount": "{count} versions", + "viewAllVersions": "Voir toutes les versions locales" } }, "globalContextMenu": { @@ -675,7 +679,10 @@ "sizeAsc": "Plus petit", "usage": "Nombre d'utilisations", "usageDesc": "Plus", - "usageAsc": "Moins" + "usageAsc": "Moins", + "versionsCount": "Versions locales", + "versionsCountDesc": "Plus de versions d'abord", + "versionsCountAsc": "Moins de versions d'abord" }, "refresh": { "title": "Actualiser la liste des modèles", diff --git a/locales/he.json b/locales/he.json index 701a6f28..cf4a4a86 100644 --- a/locales/he.json +++ b/locales/he.json @@ -145,6 +145,10 @@ }, "usage": { "timesUsed": "מספר שימושים" + }, + "footer": { + "versionCount": "{count} גרסאות", + "viewAllVersions": "הצג את כל הגרסאות המקומיות" } }, "globalContextMenu": { @@ -675,7 +679,10 @@ "sizeAsc": "הקטן ביותר", "usage": "מספר שימושים", "usageDesc": "הכי הרבה", - "usageAsc": "הכי פחות" + "usageAsc": "הכי פחות", + "versionsCount": "גרסאות מקומיות", + "versionsCountDesc": "הכי הרבה גרסאות ראשונות", + "versionsCountAsc": "הכי מעט גרסאות ראשונות" }, "refresh": { "title": "רענן רשימת מודלים", diff --git a/locales/ja.json b/locales/ja.json index 7f7f6996..24f8c689 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -145,6 +145,10 @@ }, "usage": { "timesUsed": "使用回数" + }, + "footer": { + "versionCount": "{count} バージョン", + "viewAllVersions": "ローカルの全バージョンを表示" } }, "globalContextMenu": { @@ -675,7 +679,10 @@ "sizeAsc": "小さい順", "usage": "使用回数", "usageDesc": "多い", - "usageAsc": "少ない" + "usageAsc": "少ない", + "versionsCount": "ローカルバージョン数", + "versionsCountDesc": "バージョン数の多い順", + "versionsCountAsc": "バージョン数の少ない順" }, "refresh": { "title": "モデルリストを更新", diff --git a/locales/ko.json b/locales/ko.json index 9701ed04..12eaa3c2 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -145,6 +145,10 @@ }, "usage": { "timesUsed": "사용 횟수" + }, + "footer": { + "versionCount": "{count}개 버전", + "viewAllVersions": "모든 로컬 버전 보기" } }, "globalContextMenu": { @@ -675,7 +679,10 @@ "sizeAsc": "작은 순서", "usage": "사용 횟수", "usageDesc": "많은 순", - "usageAsc": "적은 순" + "usageAsc": "적은 순", + "versionsCount": "로컬 버전 수", + "versionsCountDesc": "버전 수 많은 순", + "versionsCountAsc": "버전 수 적은 순" }, "refresh": { "title": "모델 목록 새로고침", diff --git a/locales/ru.json b/locales/ru.json index 64907d45..15078501 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -145,6 +145,10 @@ }, "usage": { "timesUsed": "Количество использований" + }, + "footer": { + "versionCount": "{count} версий", + "viewAllVersions": "Показать все локальные версии" } }, "globalContextMenu": { @@ -675,7 +679,10 @@ "sizeAsc": "Наименьшим", "usage": "Число использований", "usageDesc": "Больше", - "usageAsc": "Меньше" + "usageAsc": "Меньше", + "versionsCount": "Локальные версии", + "versionsCountDesc": "Сначала больше версий", + "versionsCountAsc": "Сначала меньше версий" }, "refresh": { "title": "Обновить список моделей", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 66156f98..e7212976 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -145,6 +145,10 @@ }, "usage": { "timesUsed": "使用次数" + }, + "footer": { + "versionCount": "{count} 个版本", + "viewAllVersions": "查看所有本地版本" } }, "globalContextMenu": { @@ -675,7 +679,10 @@ "sizeAsc": "最小", "usage": "使用次数", "usageDesc": "最多", - "usageAsc": "最少" + "usageAsc": "最少", + "versionsCount": "本地版本数", + "versionsCountDesc": "版本数从多到少", + "versionsCountAsc": "版本数从少到多" }, "refresh": { "title": "刷新模型列表", diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 5ff2028d..5760d932 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -145,6 +145,10 @@ }, "usage": { "timesUsed": "使用次數" + }, + "footer": { + "versionCount": "{count} 個版本", + "viewAllVersions": "檢視所有本地版本" } }, "globalContextMenu": { @@ -675,7 +679,10 @@ "sizeAsc": "最小", "usage": "使用次數", "usageDesc": "最多", - "usageAsc": "最少" + "usageAsc": "最少", + "versionsCount": "本地版本數", + "versionsCountDesc": "版本數從多到少", + "versionsCountAsc": "版本數從少到多" }, "refresh": { "title": "重新整理模型列表", diff --git a/py/services/base_model_service.py b/py/services/base_model_service.py index e7ba379b..016a9d76 100644 --- a/py/services/base_model_service.py +++ b/py/services/base_model_service.py @@ -115,19 +115,50 @@ class BaseModelService(ABC): # Optionally group by civitai modelId, showing only the latest version per model dedup_lost = 0 if kwargs.get("group_by_model") and civitai_model_id is None: - dedup_map = {} # modelId -> (item, version_id) + # Determine whether to further sub-group by base model + # When update_flag_strategy is "same_base", versions with different + # base models are effectively different groups — the dedup key + # needs to include base_model so the version count and VLM flow + # stay consistent (card shows correct count for its base model). + ufs = self.settings.get("update_flag_strategy", "same_base") + group_by_base = ufs == "same_base" + + dedup_map = {} # (modelId [,base_model]) -> (item, version_id) + version_counter = {} # same-key -> count standalone = [] for item in sorted_data: mid = self._extract_model_id(item) if mid is None: standalone.append(item) continue + key = (mid, item.get("base_model") or "") if group_by_base else mid + # Count all versions per key + version_counter[key] = version_counter.get(key, 0) + 1 vid = self._extract_version_id(item) or 0 - if mid not in dedup_map or vid > dedup_map[mid][1]: - dedup_map[mid] = (item, vid) + if key not in dedup_map or vid > dedup_map[key][1]: + dedup_map[key] = (item, vid) + # Attach version_count to each surviving grouped item (shallow copy + # to avoid mutating cached dicts — the cache is shared across requests) + for key, (item, vid) in dedup_map.items(): + item = dict(item) + item["version_count"] = version_counter[key] + dedup_map[key] = (item, vid) dedup_lost = len(sorted_data) - (len(dedup_map) + len(standalone)) sorted_data = [entry[0] for entry in dedup_map.values()] + standalone + # Re-sort by version_count after dedup (only makes sense in group_by_model mode) + is_group_by_active = kwargs.get("group_by_model") and civitai_model_id is None + if sort_params.key == "versions_count" and is_group_by_active: + reverse = sort_params.order == "desc" + sorted_data.sort( + key=lambda x: ( + x.get("version_count", 0), + (x.get("model_name") or x.get("file_name") or "").lower(), + x.get("file_path", "").lower(), + ), + reverse=reverse, + ) + t1 = time.perf_counter() if hash_filters: filtered_data = await self._apply_hash_filters(sorted_data, hash_filters) diff --git a/py/services/checkpoint_service.py b/py/services/checkpoint_service.py index 02befbda..82cf8aaa 100644 --- a/py/services/checkpoint_service.py +++ b/py/services/checkpoint_service.py @@ -48,6 +48,7 @@ class CheckpointService(BaseModelService): "skip_metadata_refresh": bool(checkpoint_data.get("skip_metadata_refresh", False)), "civitai": self.filter_civitai_data(checkpoint_data.get("civitai", {}), minimal=True), "auto_tags": checkpoint_data.get("auto_tags") or extract_auto_tags(checkpoint_data), + "version_count": checkpoint_data.get("version_count"), } def find_duplicate_hashes(self) -> Dict: diff --git a/py/services/embedding_service.py b/py/services/embedding_service.py index ea187f85..779cbee3 100644 --- a/py/services/embedding_service.py +++ b/py/services/embedding_service.py @@ -48,6 +48,7 @@ class EmbeddingService(BaseModelService): "skip_metadata_refresh": bool(embedding_data.get("skip_metadata_refresh", False)), "civitai": self.filter_civitai_data(embedding_data.get("civitai", {}), minimal=True), "auto_tags": embedding_data.get("auto_tags") or extract_auto_tags(embedding_data), + "version_count": embedding_data.get("version_count"), } def find_duplicate_hashes(self) -> Dict: diff --git a/py/services/lora_service.py b/py/services/lora_service.py index 6819e4fd..75c21bf7 100644 --- a/py/services/lora_service.py +++ b/py/services/lora_service.py @@ -59,6 +59,7 @@ class LoraService(BaseModelService): lora_data.get("civitai", {}), minimal=True ), "auto_tags": lora_data.get("auto_tags") or extract_auto_tags(lora_data), + "version_count": lora_data.get("version_count"), } async def _apply_specific_filters(self, data: List[Dict], **kwargs) -> List[Dict]: diff --git a/py/services/model_cache.py b/py/services/model_cache.py index 9b396634..ba515115 100644 --- a/py/services/model_cache.py +++ b/py/services/model_cache.py @@ -18,6 +18,8 @@ SUPPORTED_SORT_MODES = [ ('size', 'desc'), ('usage', 'asc'), ('usage', 'desc'), + ('versions_count', 'asc'), + ('versions_count', 'desc'), ] # Is this in use? @@ -263,6 +265,17 @@ class ModelCache: ), reverse=reverse ) + elif sort_key == 'versions_count': + # Pre-dedup sort: fall back to name sort. + # Actual re-sort by version_count happens in get_paginated_data after dedup. + result = natsorted( + data, + key=lambda x: ( + self._get_display_name(x).lower(), + x.get('file_path', '').lower() + ), + reverse=reverse + ) else: # Fallback: no sort result = list(data) diff --git a/static/css/components/card.css b/static/css/components/card.css index 71bf2bb5..f3c83105 100644 --- a/static/css/components/card.css +++ b/static/css/components/card.css @@ -509,6 +509,50 @@ background: rgba(0,0,0,0.18); /* Optional: subtle background for contrast */ } +/* Clickable version count link (shown in group-by-model mode) */ +.version-count-link { + display: inline-block; + color: var(--color-accent); + text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5); + font-size: 0.85em; + line-height: 1.4; + margin-top: 2px; + border: 1px solid var(--color-accent-border); + border-radius: var(--border-radius-xs); + padding: 1px 6px; + background: var(--color-accent-subtle); + cursor: pointer; + transition: background 0.15s ease, border-color 0.15s ease; +} +.version-count-link:hover { + background: var(--color-accent-border); + border-color: var(--color-accent-transparent); +} + +/* Medium density adjustments for version count link */ +.medium-density .version-count-link { + font-size: 0.8em; +} + +.medium-density .badge-version-unit .version-count-link { + max-width: 90px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Compact density adjustments for version count link */ +.compact-density .version-count-link { + font-size: 0.75em; +} + +.compact-density .badge-version-unit .version-count-link { + max-width: 70px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + /* Version row — flex container for badges + version names */ .version-row { display: flex; diff --git a/static/css/style.css b/static/css/style.css index 9d88d6ff..dfc00782 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -59,3 +59,8 @@ .initialization-notice .loading-spinner { margin-bottom: var(--space-2); } + +/* Hide versions_count sort option when group-by-model is off */ +body:not(.group-by-model) .sort-option-versions-count { + display: none; +} diff --git a/static/js/components/ContextMenu/GlobalContextMenu.js b/static/js/components/ContextMenu/GlobalContextMenu.js index c081a926..be834c7b 100644 --- a/static/js/components/ContextMenu/GlobalContextMenu.js +++ b/static/js/components/ContextMenu/GlobalContextMenu.js @@ -108,6 +108,11 @@ export class GlobalContextMenu extends BaseContextMenu { const newValue = !state.global.settings.group_by_model; state.global.settings.group_by_model = newValue; + // Save/restore sort preference when toggling group_by_model + if (window.pageControls?.onGroupByModelToggled) { + window.pageControls.onGroupByModelToggled(newValue); + } + sm.saveSetting('group_by_model', newValue).catch((error) => { console.error('Failed to save group_by_model setting:', error); // Revert state on failure diff --git a/static/js/components/controls/CheckpointsControls.js b/static/js/components/controls/CheckpointsControls.js index 57c72897..0d61644d 100644 --- a/static/js/components/controls/CheckpointsControls.js +++ b/static/js/components/controls/CheckpointsControls.js @@ -102,7 +102,12 @@ export class CheckpointsControls extends PageControls { removeSessionItem('vlm_model_name'); removeSessionItem('vlm_base_model'); removeSessionItem('vlm_page_type'); - window.location.reload(); + // Hide the indicator + const indicator = document.getElementById('customFilterIndicator'); + if (indicator) { + indicator.classList.add('hidden'); + } + await resetAndReload(); return; } diff --git a/static/js/components/controls/LorasControls.js b/static/js/components/controls/LorasControls.js index b9e42473..d9733bcb 100644 --- a/static/js/components/controls/LorasControls.js +++ b/static/js/components/controls/LorasControls.js @@ -119,7 +119,11 @@ export class LorasControls extends PageControls { removeSessionItem('vlm_model_name'); removeSessionItem('vlm_base_model'); removeSessionItem('vlm_page_type'); - window.location.reload(); + const indicator = document.getElementById('customFilterIndicator'); + if (indicator) { + indicator.classList.add('hidden'); + } + await resetAndReload(); return; } diff --git a/static/js/components/controls/PageControls.js b/static/js/components/controls/PageControls.js index 3c6ce31c..d00441b0 100644 --- a/static/js/components/controls/PageControls.js +++ b/static/js/components/controls/PageControls.js @@ -1,6 +1,6 @@ // PageControls.js - Manages controls for both LoRAs and Checkpoints pages import { state, getCurrentPageState, setCurrentPageType } from '../../state/index.js'; -import { getStorageItem, setStorageItem, getSessionItem, setSessionItem, removeSessionItem } from '../../utils/storageHelpers.js'; +import { getStorageItem, setStorageItem, removeStorageItem, getSessionItem, setSessionItem, removeSessionItem } from '../../utils/storageHelpers.js'; import { showToast, openCivitaiByMetadata } from '../../utils/uiHelpers.js'; import { performModelUpdateCheck } from '../../utils/updateCheckHelpers.js'; import { sidebarManager } from '../SidebarManager.js'; @@ -465,6 +465,68 @@ export class PageControls { /** * Clear custom filter */ + /** + * Trigger View Local Versions without page reload + * Sets sessionStorage and reloads data via the API. + */ + triggerVlmView(modelId, modelName, baseModel, pageType) { + const targetPageType = pageType || this.pageType; + setSessionItem('vlm_model_id', String(modelId)); + setSessionItem('vlm_model_name', modelName || String(modelId)); + setSessionItem('vlm_page_type', targetPageType); + if (baseModel) { + setSessionItem('vlm_base_model', baseModel); + } else { + removeSessionItem('vlm_base_model'); + } + // Reload data via API (no page reload) + this.resetAndReload(true).then(() => { + // Show the VLM indicator after data loads + this.checkVlmFilter(); + }); + } + + /** + * Called when group_by_model is toggled. + * Saves current sort when entering grouped mode, restores normal sort + * when leaving — prevents "Most versions first" persisting after exit. + */ + onGroupByModelToggled(isEnabled) { + const normalKey = `${this.pageType}_sort_normal`; + const groupedKey = `${this.pageType}_sort_grouped`; + + if (isEnabled) { + // Entering group mode: save current sort for later restoration + setStorageItem(normalKey, this.pageState.sortBy); + // Restore previously saved grouped sort, if any + const savedGroupedSort = getStorageItem(groupedKey); + if (savedGroupedSort) { + this.pageState.sortBy = savedGroupedSort; + this.saveSortPreference(savedGroupedSort); + const sortSelect = document.getElementById('sortSelect'); + if (sortSelect) { + sortSelect.value = savedGroupedSort; + } + } + } else { + // Leaving group mode: save current grouped sort aside, restore normal + const currentSort = this.pageState.sortBy; + if (currentSort && currentSort.startsWith('versions_count')) { + setStorageItem(groupedKey, currentSort); + } + const savedNormalSort = getStorageItem(normalKey); + if (savedNormalSort) { + removeStorageItem(normalKey); + this.pageState.sortBy = savedNormalSort; + this.saveSortPreference(savedNormalSort); + const sortSelect = document.getElementById('sortSelect'); + if (sortSelect) { + sortSelect.value = savedNormalSort; + } + } + } + } + /** * Check for View Local Versions filter in sessionStorage (page-type-scoped) */ @@ -515,8 +577,14 @@ export class PageControls { removeSessionItem('vlm_base_model'); removeSessionItem('vlm_page_type'); - // Full page reload to restore initial state (mirrors the "set" action) - window.location.reload(); + // Hide the indicator + const indicator = document.getElementById('customFilterIndicator'); + if (indicator) { + indicator.classList.add('hidden'); + } + + // Reload data via API (no page reload) + await this.resetAndReload(true); return; } diff --git a/static/js/components/shared/ModelCard.js b/static/js/components/shared/ModelCard.js index c8b1b9ee..9233400f 100644 --- a/static/js/components/shared/ModelCard.js +++ b/static/js/components/shared/ModelCard.js @@ -100,6 +100,12 @@ function handleModelCardEvent_internal(event, modelType) { return true; // Stop propagation } + if (event.target.closest('.version-count-link')) { + event.stopPropagation(); + handleViewLocalVersionsFromCard(card, modelType); + return true; + } + // If no specific element was clicked, handle the card click (show modal or toggle selection) handleCardClick(card, modelType); return false; // Continue with other handlers (e.g., bulk selection) @@ -265,6 +271,22 @@ async function handleExampleImagesAccess(card, modelType) { } } +function handleViewLocalVersionsFromCard(card, modelType) { + const modelId = card.dataset.modelId; + const modelName = card.dataset.name; + if (!modelId) return; + // Respect update_flag_strategy: only filter by base model when the strategy says so + const strategy = state.global?.settings?.update_flag_strategy; + const shouldFilterByBase = strategy === 'same_base'; + const baseModel = shouldFilterByBase && card.dataset.base_model !== 'Unknown' + ? card.dataset.base_model + : undefined; + // Use the no-reload VLM flow via PageControls + if (window.pageControls && typeof window.pageControls.triggerVlmView === 'function') { + window.pageControls.triggerVlmView(modelId, modelName, baseModel, modelType); + } +} + function handleCardClick(card, modelType) { const pageState = getCurrentPageState(); @@ -448,6 +470,10 @@ export function createModelCard(model, modelType) { const hasUpdateAvailable = Boolean(model.update_available); card.dataset.update_available = hasUpdateAvailable ? 'true' : 'false'; card.dataset.skip_metadata_refresh = model.skip_metadata_refresh ? 'true' : 'false'; + // Store version_count for group-by-model display + if (model.version_count !== undefined) { + card.dataset.version_count = model.version_count; + } // To only show usage_count when sorting by usage. const pageState = getCurrentPageState(); @@ -659,16 +685,28 @@ export function createModelCard(model, modelType) { const autoTags = model.auto_tags || []; const hlTags = autoTags.filter(t => t === 'HIGH' || t === 'LOW'); const hasVersionName = model.civitai?.name; - if (!hlTags.length && !hasVersionName) return ''; + // When group_by_model is active and model has multiple versions, + // show clickable version count instead of version name (and hide badges) + const isGroupByModel = state.global.settings.group_by_model; + const versionCount = model.version_count; + const showVersionCount = isGroupByModel && versionCount > 1; + if (!hlTags.length && !hasVersionName && !showVersionCount) return ''; const density = state.global.settings.display_density || 'default'; const shortLabels = density === 'medium' || density === 'compact'; - const badges = hlTags.map(t => { + // Don't show HIGH/LOW badges when showing version count (confusing in grouped mode) + const badges = !showVersionCount ? hlTags.map(t => { const cls = t === 'HIGH' ? 'hl-badge hl-badge--high' : 'hl-badge hl-badge--low'; const label = shortLabels ? (t === 'HIGH' ? 'H' : 'L') : t; const titleAttr = shortLabels ? ` title="${t}"` : ''; return `${label}`; - }).join(''); - const versionHtml = hasVersionName ? `${model.civitai.name}` : ''; + }).join('') : ''; + let versionHtml = ''; + if (showVersionCount) { + const countLabel = translate('modelCard.footer.versionCount', { count: versionCount }, `${versionCount} versions`); + versionHtml = `${countLabel}`; + } else if (hasVersionName) { + versionHtml = `${model.civitai.name}`; + } return `${badges}${versionHtml}`; })()} ${hasUsageCount ? `${model.usage_count}×` : ''} diff --git a/static/js/components/shared/ModelVersionsTab.js b/static/js/components/shared/ModelVersionsTab.js index aebdb3cc..002045fb 100644 --- a/static/js/components/shared/ModelVersionsTab.js +++ b/static/js/components/shared/ModelVersionsTab.js @@ -1042,9 +1042,16 @@ export function initVersionsTab({ removeSessionItem('vlm_base_model'); } - // Close the modal and reload the page to show filtered cards + // Close the modal and navigate via no-reload VLM flow modalManager.closeModal(modalId); - window.location.reload(); + if (window.pageControls && typeof window.pageControls.triggerVlmView === 'function') { + window.pageControls.triggerVlmView( + modelId, + modelName || String(modelId), + isFilteringActive ? baseModelInfo.raw : undefined, + modelType + ); + } } async function handleToggleVersionIgnore(button, versionId) { diff --git a/static/js/managers/SettingsManager.js b/static/js/managers/SettingsManager.js index e7be6e23..6152080f 100644 --- a/static/js/managers/SettingsManager.js +++ b/static/js/managers/SettingsManager.js @@ -2018,6 +2018,10 @@ export class SettingsManager { } if (settingKey === 'show_only_sfw' || settingKey === 'blur_mature_content' || settingKey === 'group_by_model') { + // Save/restore sort preference when toggling group_by_model + if (settingKey === 'group_by_model' && window.pageControls?.onGroupByModelToggled) { + window.pageControls.onGroupByModelToggled(value); + } this.reloadContent(); } diff --git a/templates/components/controls.html b/templates/components/controls.html index fb22f016..1c122b96 100644 --- a/templates/components/controls.html +++ b/templates/components/controls.html @@ -37,6 +37,12 @@ {% endif %} + {% if page_id != 'recipes' %} + + + + + {% endif %} {% if page_id == 'recipes' %} diff --git a/tests/services/test_base_model_service.py b/tests/services/test_base_model_service.py index 58b970d3..27fc83ff 100644 --- a/tests/services/test_base_model_service.py +++ b/tests/services/test_base_model_service.py @@ -790,8 +790,14 @@ async def test_get_paginated_data_group_by_model_dedup(): for item in response["items"]: if item.get("civitai", {}).get("modelId") == 1: assert item["civitai"]["id"] == 200 + # version_count should reflect total versions for this model + assert item.get("version_count") == 2, f"Expected version_count=2, got {item.get('version_count')}" elif item.get("civitai", {}).get("modelId") == 2: assert item["civitai"]["id"] == 99 + assert item.get("version_count") == 2, f"Expected version_count=2, got {item.get('version_count')}" + else: + # Standalone item should NOT have version_count + assert "version_count" not in item, f"Standalone should not have version_count" # With group_by_model=False (default) — all 5 items pass through response_all = await service.get_paginated_data(