diff --git a/locales/en.json b/locales/en.json index b55ff9bb..9b1d95ae 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1465,7 +1465,7 @@ "resumeModelUpdates": "Resume updates for this model", "ignoreModelUpdates": "Ignore updates for this model", "viewLocalVersions": "View all local versions", - "viewLocalTooltip": "Coming soon" + "viewLocalTooltip": "Show all local versions of this model on the main page" }, "filters": { "label": "Base filter", diff --git a/py/routes/handlers/model_handlers.py b/py/routes/handlers/model_handlers.py index 916864b7..8e21415d 100644 --- a/py/routes/handlers/model_handlers.py +++ b/py/routes/handlers/model_handlers.py @@ -373,6 +373,14 @@ class ModelListingHandler: request.query.get("group_by_model", "false").lower() == "true" ) + # View-local-versions filter: show all local versions of a specific model + civitai_model_id = request.query.get("civitai_model_id") + if civitai_model_id is not None: + try: + civitai_model_id = int(civitai_model_id) + except (TypeError, ValueError): + civitai_model_id = None + return { "page": page, "page_size": page_size, @@ -397,6 +405,7 @@ class ModelListingHandler: "name_pattern_exclude": name_pattern_exclude, "name_pattern_use_regex": name_pattern_use_regex, "group_by_model": group_by_model, + "civitai_model_id": civitai_model_id, **self._parse_specific_params(request), } diff --git a/py/services/base_model_service.py b/py/services/base_model_service.py index 13f3bfbe..e7ba379b 100644 --- a/py/services/base_model_service.py +++ b/py/services/base_model_service.py @@ -104,9 +104,17 @@ class BaseModelService(ABC): fetch_duration = time.perf_counter() - t0 initial_count = len(sorted_data) + # Optionally filter by civitai model ID (shows all local versions of a specific model) + civitai_model_id = kwargs.get("civitai_model_id") + if civitai_model_id is not None: + sorted_data = [ + item for item in sorted_data + if self._extract_model_id(item) == civitai_model_id + ] + # Optionally group by civitai modelId, showing only the latest version per model dedup_lost = 0 - if kwargs.get("group_by_model"): + if kwargs.get("group_by_model") and civitai_model_id is None: dedup_map = {} # modelId -> (item, version_id) standalone = [] for item in sorted_data: diff --git a/static/js/api/baseModelApi.js b/static/js/api/baseModelApi.js index d32bf601..68761bc7 100644 --- a/static/js/api/baseModelApi.js +++ b/static/js/api/baseModelApi.js @@ -1271,8 +1271,9 @@ export class BaseModelApiClient { params.append('recursive', pageState.searchOptions.recursive ? 'true' : 'false'); - // Pass group-by-model mode to backend - if (state.global.settings.group_by_model) { + // Pass group-by-model mode to backend (skip when showing all versions of a specific model) + const vlmModelId = getSessionItem('vlm_model_id'); + if (state.global.settings.group_by_model && !vlmModelId) { params.append('group_by_model', 'true'); } @@ -1357,6 +1358,17 @@ export class BaseModelApiClient { } _addModelSpecificParams(params, pageState) { + // Check for View Local Versions filter (takes priority over recipe filters) + const vlmModelId = getSessionItem('vlm_model_id'); + if (vlmModelId) { + params.append('civitai_model_id', vlmModelId); + const vlmBaseModel = getSessionItem('vlm_base_model'); + if (vlmBaseModel) { + params.append('base_model', vlmBaseModel); + } + return; + } + if (this.modelType === 'loras') { const filterLoraHash = getSessionItem('recipe_to_lora_filterLoraHash'); const filterLoraHashes = getSessionItem('recipe_to_lora_filterLoraHashes'); diff --git a/static/js/api/loraApi.js b/static/js/api/loraApi.js index d93cb1a8..9a10518d 100644 --- a/static/js/api/loraApi.js +++ b/static/js/api/loraApi.js @@ -9,6 +9,13 @@ export class LoraApiClient extends BaseModelApiClient { * Add LoRA-specific parameters to query */ _addModelSpecificParams(params, pageState) { + // Let parent handle View Local Versions filter first + super._addModelSpecificParams(params, pageState); + // If VLM filter was applied, skip recipe-specific filters + if (params.has('civitai_model_id')) { + return; + } + const filterLoraHash = getSessionItem('recipe_to_lora_filterLoraHash'); const filterLoraHashes = getSessionItem('recipe_to_lora_filterLoraHashes'); diff --git a/static/js/components/controls/CheckpointsControls.js b/static/js/components/controls/CheckpointsControls.js index 079037ff..5bdb62e6 100644 --- a/static/js/components/controls/CheckpointsControls.js +++ b/static/js/components/controls/CheckpointsControls.js @@ -95,6 +95,16 @@ export class CheckpointsControls extends PageControls { * Clear checkpoint custom filter and reload */ async clearCustomFilter() { + // Check for View Local Versions filter first + const vlmModelId = getSessionItem('vlm_model_id'); + if (vlmModelId) { + removeSessionItem('vlm_model_id'); + removeSessionItem('vlm_model_name'); + removeSessionItem('vlm_base_model'); + window.location.reload(); + return; + } + removeSessionItem('recipe_to_checkpoint_filterHash'); removeSessionItem('recipe_to_checkpoint_filterHashes'); removeSessionItem('filterCheckpointRecipeName'); diff --git a/static/js/components/controls/LorasControls.js b/static/js/components/controls/LorasControls.js index 725a960a..8ddc166b 100644 --- a/static/js/components/controls/LorasControls.js +++ b/static/js/components/controls/LorasControls.js @@ -112,6 +112,16 @@ export class LorasControls extends PageControls { * Clear the custom filter and reload the page */ async clearCustomFilter() { + // Check for View Local Versions filter first (handles VLM and reloads) + const vlmModelId = getSessionItem('vlm_model_id'); + if (vlmModelId) { + removeSessionItem('vlm_model_id'); + removeSessionItem('vlm_model_name'); + removeSessionItem('vlm_base_model'); + window.location.reload(); + return; + } + console.log("Clearing custom filter..."); // Remove filter parameters from session storage removeSessionItem('recipe_to_lora_filterLoraHash'); diff --git a/static/js/components/controls/PageControls.js b/static/js/components/controls/PageControls.js index 801a2bd0..c8431916 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 } from '../../utils/storageHelpers.js'; +import { getStorageItem, setStorageItem, 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'; @@ -129,6 +129,9 @@ export class PageControls { clearFilterBtn.addEventListener('click', () => this.clearCustomFilter()); } + // Check for View Local Versions filter + this.checkVlmFilter(); + // Page-specific event listeners this.initPageSpecificListeners(); } @@ -459,15 +462,57 @@ export class PageControls { this.api.toggleBulkMode(); } + /** + * Clear custom filter + */ + /** + * Check for View Local Versions filter in sessionStorage + */ + checkVlmFilter() { + const vlmModelId = getSessionItem('vlm_model_id'); + const vlmModelName = getSessionItem('vlm_model_name'); + const vlmBaseModel = getSessionItem('vlm_base_model'); + + if (vlmModelId && vlmModelName) { + const indicator = document.getElementById('customFilterIndicator'); + const filterText = indicator?.querySelector('.customFilterText'); + + if (indicator && filterText) { + indicator.classList.remove('hidden'); + + const prefix = vlmBaseModel + ? 'Showing same-base versions from' + : 'Showing all versions from'; + const displayText = `${prefix}: ${vlmModelName}`; + + filterText.textContent = this._truncateText(displayText, 40); + filterText.setAttribute('title', displayText); + } + } + } + /** * Clear custom filter */ async clearCustomFilter() { + // Check for View Local Versions filter first + const vlmModelId = getSessionItem('vlm_model_id'); + if (vlmModelId) { + removeSessionItem('vlm_model_id'); + removeSessionItem('vlm_model_name'); + removeSessionItem('vlm_base_model'); + + // Full page reload to restore initial state (mirrors the "set" action) + window.location.reload(); + return; + } + + // Otherwise delegate to subclass for recipe filters if (!this.api) { console.error('API methods not registered'); return; } - + try { await this.api.clearCustomFilter(); } catch (error) { @@ -475,6 +520,14 @@ export class PageControls { showToast('toast.controls.clearFilterFailed', { message: error.message }, 'error'); } } + + /** + * Truncate text with ellipsis + */ + _truncateText(text, maxLength) { + if (!text) return ''; + return text.length > maxLength ? `${text.substring(0, maxLength - 3)}...` : text; + } /** * Initialize the favorites filter button state diff --git a/static/js/components/shared/ModelModal.js b/static/js/components/shared/ModelModal.js index 04493932..f530a09a 100644 --- a/static/js/components/shared/ModelModal.js +++ b/static/js/components/shared/ModelModal.js @@ -752,6 +752,7 @@ export async function showModelModal(model, modelType) { modelId: civitaiModelId, currentVersionId: civitaiVersionId, currentBaseModel: modelWithFullData.base_model, + modelName: model.model_name, onUpdateStatusChange: handleUpdateStatusChange, }); setupEditableFields(modelWithFullData.file_path, modelType); diff --git a/static/js/components/shared/ModelVersionsTab.js b/static/js/components/shared/ModelVersionsTab.js index c00a2e98..6c8b1881 100644 --- a/static/js/components/shared/ModelVersionsTab.js +++ b/static/js/components/shared/ModelVersionsTab.js @@ -6,6 +6,7 @@ import { translate } from '../../utils/i18nHelpers.js'; import { state } from '../../state/index.js'; import { buildCivitaiModelUrl } from '../../utils/civitaiUtils.js'; import { formatFileSize } from './utils.js'; +import { setSessionItem, removeSessionItem } from '../../utils/storageHelpers.js'; const VIDEO_EXTENSIONS = ['.mp4', '.webm', '.mov', '.mkv']; const PREVIEW_PLACEHOLDER_URL = '/loras_static/images/no-preview.png'; @@ -744,7 +745,7 @@ function renderToolbar(record, toolbarState = {}) { - @@ -792,6 +793,7 @@ export function initVersionsTab({ modelId, currentVersionId, currentBaseModel, + modelName, onUpdateStatusChange, }) { const pane = document.querySelector(`#${modalId} #versions-tab`); @@ -1019,6 +1021,31 @@ export function initVersionsTab({ render(controller.record); } + function handleViewLocalVersions() { + if (!controller.record || !modelId) { + return; + } + // Determine base model filter based on current display mode + const baseModelInfo = getCurrentVersionBaseModel(controller.record, normalizedCurrentVersionId); + const isFilteringActive = + displayMode === DISPLAY_FILTER_MODES.SAME_BASE && + Boolean(baseModelInfo.normalized); + + // Write filter params to sessionStorage + setSessionItem('vlm_model_id', String(modelId)); + setSessionItem('vlm_model_name', modelName || String(modelId)); + if (isFilteringActive) { + // Use raw (non-normalized) base model for exact backend matching + setSessionItem('vlm_base_model', baseModelInfo.raw); + } else { + removeSessionItem('vlm_base_model'); + } + + // Close the modal and reload the page to show filtered cards + modalManager.closeModal(modalId); + window.location.reload(); + } + async function handleToggleVersionIgnore(button, versionId) { if (!controller.record) { return; @@ -1348,6 +1375,10 @@ export function initVersionsTab({ event.preventDefault(); handleToggleVersionDisplayMode(); break; + case 'view-local': + event.preventDefault(); + handleViewLocalVersions(); + break; default: break; }