feat(versions): add View all local versions button to model versions tab

Clicking the button closes the modal, writes filter params to sessionStorage,
and reloads the page to show all local versions of the model as individual
cards (bypassing group-by-model dedup). The filter respects the update flag
strategy and the versions-filter-toggle state (same-base vs all versions).

Supporting changes:
- sessionStorage keys vlm_model_id / vlm_model_name / vlm_base_model
- BaseModelApiClient._addModelSpecificParams adds civitai_model_id param
- LoraApiClient calls super._addModelSpecificParams for VLM detection
- LorasControls / CheckpointsControls clearCustomFilter checks VLM first
- PageControls.checkVlmFilter shows customFilterIndicator with label
- Backend parses civitai_model_id, filters before group_by_model dedup
This commit is contained in:
Will Miao
2026-06-21 11:13:53 +08:00
parent 559ca946dc
commit fc29cde82a
10 changed files with 148 additions and 7 deletions

View File

@@ -1465,7 +1465,7 @@
"resumeModelUpdates": "Resume updates for this model", "resumeModelUpdates": "Resume updates for this model",
"ignoreModelUpdates": "Ignore updates for this model", "ignoreModelUpdates": "Ignore updates for this model",
"viewLocalVersions": "View all local versions", "viewLocalVersions": "View all local versions",
"viewLocalTooltip": "Coming soon" "viewLocalTooltip": "Show all local versions of this model on the main page"
}, },
"filters": { "filters": {
"label": "Base filter", "label": "Base filter",

View File

@@ -373,6 +373,14 @@ class ModelListingHandler:
request.query.get("group_by_model", "false").lower() == "true" 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 { return {
"page": page, "page": page,
"page_size": page_size, "page_size": page_size,
@@ -397,6 +405,7 @@ class ModelListingHandler:
"name_pattern_exclude": name_pattern_exclude, "name_pattern_exclude": name_pattern_exclude,
"name_pattern_use_regex": name_pattern_use_regex, "name_pattern_use_regex": name_pattern_use_regex,
"group_by_model": group_by_model, "group_by_model": group_by_model,
"civitai_model_id": civitai_model_id,
**self._parse_specific_params(request), **self._parse_specific_params(request),
} }

View File

@@ -104,9 +104,17 @@ class BaseModelService(ABC):
fetch_duration = time.perf_counter() - t0 fetch_duration = time.perf_counter() - t0
initial_count = len(sorted_data) 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 # Optionally group by civitai modelId, showing only the latest version per model
dedup_lost = 0 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) dedup_map = {} # modelId -> (item, version_id)
standalone = [] standalone = []
for item in sorted_data: for item in sorted_data:

View File

@@ -1271,8 +1271,9 @@ export class BaseModelApiClient {
params.append('recursive', pageState.searchOptions.recursive ? 'true' : 'false'); params.append('recursive', pageState.searchOptions.recursive ? 'true' : 'false');
// Pass group-by-model mode to backend // Pass group-by-model mode to backend (skip when showing all versions of a specific model)
if (state.global.settings.group_by_model) { const vlmModelId = getSessionItem('vlm_model_id');
if (state.global.settings.group_by_model && !vlmModelId) {
params.append('group_by_model', 'true'); params.append('group_by_model', 'true');
} }
@@ -1357,6 +1358,17 @@ export class BaseModelApiClient {
} }
_addModelSpecificParams(params, pageState) { _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') { if (this.modelType === 'loras') {
const filterLoraHash = getSessionItem('recipe_to_lora_filterLoraHash'); const filterLoraHash = getSessionItem('recipe_to_lora_filterLoraHash');
const filterLoraHashes = getSessionItem('recipe_to_lora_filterLoraHashes'); const filterLoraHashes = getSessionItem('recipe_to_lora_filterLoraHashes');

View File

@@ -9,6 +9,13 @@ export class LoraApiClient extends BaseModelApiClient {
* Add LoRA-specific parameters to query * Add LoRA-specific parameters to query
*/ */
_addModelSpecificParams(params, pageState) { _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 filterLoraHash = getSessionItem('recipe_to_lora_filterLoraHash');
const filterLoraHashes = getSessionItem('recipe_to_lora_filterLoraHashes'); const filterLoraHashes = getSessionItem('recipe_to_lora_filterLoraHashes');

View File

@@ -95,6 +95,16 @@ export class CheckpointsControls extends PageControls {
* Clear checkpoint custom filter and reload * Clear checkpoint custom filter and reload
*/ */
async clearCustomFilter() { 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_filterHash');
removeSessionItem('recipe_to_checkpoint_filterHashes'); removeSessionItem('recipe_to_checkpoint_filterHashes');
removeSessionItem('filterCheckpointRecipeName'); removeSessionItem('filterCheckpointRecipeName');

View File

@@ -112,6 +112,16 @@ export class LorasControls extends PageControls {
* Clear the custom filter and reload the page * Clear the custom filter and reload the page
*/ */
async clearCustomFilter() { 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..."); console.log("Clearing custom filter...");
// Remove filter parameters from session storage // Remove filter parameters from session storage
removeSessionItem('recipe_to_lora_filterLoraHash'); removeSessionItem('recipe_to_lora_filterLoraHash');

View File

@@ -1,6 +1,6 @@
// PageControls.js - Manages controls for both LoRAs and Checkpoints pages // PageControls.js - Manages controls for both LoRAs and Checkpoints pages
import { state, getCurrentPageState, setCurrentPageType } from '../../state/index.js'; 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 { showToast, openCivitaiByMetadata } from '../../utils/uiHelpers.js';
import { performModelUpdateCheck } from '../../utils/updateCheckHelpers.js'; import { performModelUpdateCheck } from '../../utils/updateCheckHelpers.js';
import { sidebarManager } from '../SidebarManager.js'; import { sidebarManager } from '../SidebarManager.js';
@@ -129,6 +129,9 @@ export class PageControls {
clearFilterBtn.addEventListener('click', () => this.clearCustomFilter()); clearFilterBtn.addEventListener('click', () => this.clearCustomFilter());
} }
// Check for View Local Versions filter
this.checkVlmFilter();
// Page-specific event listeners // Page-specific event listeners
this.initPageSpecificListeners(); this.initPageSpecificListeners();
} }
@@ -459,15 +462,57 @@ export class PageControls {
this.api.toggleBulkMode(); 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 * Clear custom filter
*/ */
async clearCustomFilter() { 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) { if (!this.api) {
console.error('API methods not registered'); console.error('API methods not registered');
return; return;
} }
try { try {
await this.api.clearCustomFilter(); await this.api.clearCustomFilter();
} catch (error) { } catch (error) {
@@ -475,6 +520,14 @@ export class PageControls {
showToast('toast.controls.clearFilterFailed', { message: error.message }, 'error'); 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 * Initialize the favorites filter button state

View File

@@ -752,6 +752,7 @@ export async function showModelModal(model, modelType) {
modelId: civitaiModelId, modelId: civitaiModelId,
currentVersionId: civitaiVersionId, currentVersionId: civitaiVersionId,
currentBaseModel: modelWithFullData.base_model, currentBaseModel: modelWithFullData.base_model,
modelName: model.model_name,
onUpdateStatusChange: handleUpdateStatusChange, onUpdateStatusChange: handleUpdateStatusChange,
}); });
setupEditableFields(modelWithFullData.file_path, modelType); setupEditableFields(modelWithFullData.file_path, modelType);

View File

@@ -6,6 +6,7 @@ import { translate } from '../../utils/i18nHelpers.js';
import { state } from '../../state/index.js'; import { state } from '../../state/index.js';
import { buildCivitaiModelUrl } from '../../utils/civitaiUtils.js'; import { buildCivitaiModelUrl } from '../../utils/civitaiUtils.js';
import { formatFileSize } from './utils.js'; import { formatFileSize } from './utils.js';
import { setSessionItem, removeSessionItem } from '../../utils/storageHelpers.js';
const VIDEO_EXTENSIONS = ['.mp4', '.webm', '.mov', '.mkv']; const VIDEO_EXTENSIONS = ['.mp4', '.webm', '.mov', '.mkv'];
const PREVIEW_PLACEHOLDER_URL = '/loras_static/images/no-preview.png'; const PREVIEW_PLACEHOLDER_URL = '/loras_static/images/no-preview.png';
@@ -744,7 +745,7 @@ function renderToolbar(record, toolbarState = {}) {
<button class="versions-toolbar-btn versions-toolbar-btn-primary" data-versions-action="toggle-model-ignore"> <button class="versions-toolbar-btn versions-toolbar-btn-primary" data-versions-action="toggle-model-ignore">
${escapeHtml(ignoreText)} ${escapeHtml(ignoreText)}
</button> </button>
<button class="versions-toolbar-btn versions-toolbar-btn-secondary" data-versions-action="view-local" title="${escapeHtml(translate('modals.model.versions.actions.viewLocalTooltip', {}, 'Coming soon'))}" disabled> <button class="versions-toolbar-btn versions-toolbar-btn-secondary" data-versions-action="view-local" title="${escapeHtml(translate('modals.model.versions.actions.viewLocalTooltip', {}, 'Show all local versions of this model on the main page'))}">
${escapeHtml(viewLocalText)} ${escapeHtml(viewLocalText)}
</button> </button>
</div> </div>
@@ -792,6 +793,7 @@ export function initVersionsTab({
modelId, modelId,
currentVersionId, currentVersionId,
currentBaseModel, currentBaseModel,
modelName,
onUpdateStatusChange, onUpdateStatusChange,
}) { }) {
const pane = document.querySelector(`#${modalId} #versions-tab`); const pane = document.querySelector(`#${modalId} #versions-tab`);
@@ -1019,6 +1021,31 @@ export function initVersionsTab({
render(controller.record); 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) { async function handleToggleVersionIgnore(button, versionId) {
if (!controller.record) { if (!controller.record) {
return; return;
@@ -1348,6 +1375,10 @@ export function initVersionsTab({
event.preventDefault(); event.preventDefault();
handleToggleVersionDisplayMode(); handleToggleVersionDisplayMode();
break; break;
case 'view-local':
event.preventDefault();
handleViewLocalVersions();
break;
default: default:
break; break;
} }