feat(frontend): add Other Models page with subtype filter and badges

This commit is contained in:
Will Miao
2026-09-12 11:25:51 +08:00
parent 27da7b3ca3
commit fa7ce725c1
45 changed files with 988 additions and 30 deletions
@@ -139,8 +139,8 @@ export class BulkContextMenu extends BaseContextMenu {
const downloadExampleImagesSubmenu = this.menu.querySelector('[data-has-submenu="download-example-images"]');
if (downloadExampleImagesSubmenu) {
// Show on model pages (loras, checkpoints, embeddings), hide on recipes
downloadExampleImagesSubmenu.style.display = ['loras', 'checkpoints', 'embeddings'].includes(currentModelType) ? 'flex' : 'none';
// Show on model pages (loras, checkpoints, embeddings, other), hide on recipes
downloadExampleImagesSubmenu.style.display = ['loras', 'checkpoints', 'embeddings', 'other'].includes(currentModelType) ? 'flex' : 'none';
}
const skipMetadataRefreshItem = this.menu.querySelector('[data-action="skip-metadata-refresh"]');
@@ -112,7 +112,8 @@ export const ModelContextMenuMixin = {
const prefixMap = {
lora: 'loras',
checkpoint: 'checkpoints',
embedding: 'embeddings'
embedding: 'embeddings',
other: 'other'
};
return prefixMap[this.modelType] || 'loras';
},
@@ -0,0 +1,72 @@
import { BaseContextMenu } from './BaseContextMenu.js';
import { ModelContextMenuMixin } from './ModelContextMenuMixin.js';
import { getModelApiClient, resetAndReload } from '../../api/modelApiFactory.js';
import { moveManager } from '../../managers/MoveManager.js';
import { showDeleteModal, showExcludeModal } from '../../utils/modalUtils.js';
export class OtherContextMenu extends BaseContextMenu {
constructor() {
super('otherContextMenu', '.model-card');
this.nsfwSelector = document.getElementById('nsfwLevelSelector');
this.modelType = 'other';
this.resetAndReload = resetAndReload;
this.initNSFWSelector();
}
// Implementation needed by the mixin
async saveModelMetadata(filePath, data) {
return getModelApiClient().saveModelMetadata(filePath, data);
}
showMenu(x, y, card) {
super.showMenu(x, y, card);
this.updateExcludeMenuItem();
}
handleMenuAction(action) {
// First try to handle with common actions
if (ModelContextMenuMixin.handleCommonMenuActions.call(this, action)) {
return;
}
const apiClient = getModelApiClient();
// Otherwise handle other-models-specific actions
switch(action) {
case 'details':
// Show model details
this.currentCard.click();
break;
case 'replace-preview':
// Add new action for replacing preview images
apiClient.replaceModelPreview(this.currentCard.dataset.filepath);
break;
case 'delete':
showDeleteModal(this.currentCard.dataset.filepath);
break;
case 'copyname':
// Copy model name
if (this.currentCard.querySelector('.fa-copy')) {
this.currentCard.querySelector('.fa-copy').click();
}
break;
case 'refresh-metadata':
// Refresh metadata from CivitAI
apiClient.refreshSingleModelMetadata(this.currentCard.dataset.filepath);
break;
case 'move':
moveManager.showMoveModal(this.currentCard.dataset.filepath);
break;
case 'exclude':
showExcludeModal(this.currentCard.dataset.filepath);
break;
case 'restore':
this.restoreExcludedModel(this.currentCard.dataset.filepath);
break;
}
}
}
// Mix in shared methods
Object.assign(OtherContextMenu.prototype, ModelContextMenuMixin);
@@ -2,6 +2,7 @@ export { LoraContextMenu } from './LoraContextMenu.js';
export { RecipeContextMenu } from './RecipeContextMenu.js';
export { CheckpointContextMenu } from './CheckpointContextMenu.js';
export { EmbeddingContextMenu } from './EmbeddingContextMenu.js';
export { OtherContextMenu } from './OtherContextMenu.js';
export { GlobalContextMenu } from './GlobalContextMenu.js';
export { ModelContextMenuMixin } from './ModelContextMenuMixin.js';
@@ -9,6 +10,7 @@ import { LoraContextMenu } from './LoraContextMenu.js';
import { RecipeContextMenu } from './RecipeContextMenu.js';
import { CheckpointContextMenu } from './CheckpointContextMenu.js';
import { EmbeddingContextMenu } from './EmbeddingContextMenu.js';
import { OtherContextMenu } from './OtherContextMenu.js';
import { GlobalContextMenu } from './GlobalContextMenu.js';
// Factory method to create page-specific context menu instances
@@ -22,6 +24,8 @@ export function createPageContextMenu(pageType) {
return new CheckpointContextMenu();
case 'embeddings':
return new EmbeddingContextMenu();
case 'other':
return new OtherContextMenu();
default:
return null;
}
+1
View File
@@ -32,6 +32,7 @@ export class HeaderManager {
if (path.includes('/loras/recipes')) return 'recipes';
if (path.includes('/checkpoints')) return 'checkpoints';
if (path.includes('/embeddings')) return 'embeddings';
if (path.includes('/other')) return 'other';
if (path.includes('/statistics')) return 'statistics';
if (path.includes('/loras')) return 'loras';
return 'unknown';
+2 -1
View File
@@ -1126,6 +1126,7 @@ export class SidebarManager {
recipes: 'Recipes',
checkpoints: 'Checkpoints',
embeddings: 'Embeddings',
other: 'Other Models',
};
return names[this.pageType] || this.pageType;
}
@@ -1804,7 +1805,7 @@ export class SidebarManager {
_migrateOldSettings() {
if (getStorageItem('_sidebar_migration_done')) return;
const PAGES = ['loras', 'recipes', 'checkpoints', 'embeddings'];
const PAGES = ['loras', 'recipes', 'checkpoints', 'embeddings', 'other'];
// 1. Migrate global hide setting to per-page
if (state?.global?.settings?.show_folder_sidebar === false) {
@@ -0,0 +1,60 @@
// OtherControls.js - Specific implementation for the Other Models page
import { PageControls } from './PageControls.js';
import { getModelApiClient, resetAndReload } from '../../api/modelApiFactory.js';
import { showToast } from '../../utils/uiHelpers.js';
/**
* OtherControls class - Extends PageControls for the Other Models page
* (VAE, upscalers, text encoders, CLIP vision, ControlNet, ...)
*/
export class OtherControls extends PageControls {
constructor() {
// Initialize with 'other' page type
super('other');
// Register API methods specific to the Other Models page
this.registerOtherAPI();
}
/**
* Register Other-models-specific API methods
*/
registerOtherAPI() {
const otherAPI = {
// Core API functions
loadMoreModels: async (resetPage = false, updateFolders = false) => {
return await getModelApiClient().loadMoreWithVirtualScroll(resetPage, updateFolders);
},
resetAndReload: async (updateFolders = false) => {
return await resetAndReload(updateFolders);
},
refreshModels: async (fullRebuild = false) => {
return await getModelApiClient().refreshModels(fullRebuild);
},
// Add fetch from Civitai functionality for other models
fetchFromCivitai: async () => {
return await getModelApiClient().fetchCivitaiMetadata();
},
toggleBulkMode: () => {
if (window.bulkManager) {
window.bulkManager.toggleBulkMode();
} else {
console.error('Bulk manager not available');
}
},
// No clearCustomFilter implementation is needed for other models
// as custom filters are currently only used for LoRAs
clearCustomFilter: async () => {
showToast('toast.filters.noCustomFilterToClear', {}, 'info');
}
};
// Register the API
this.registerAPI(otherAPI);
}
}
+6 -3
View File
@@ -3,13 +3,14 @@ import { PageControls } from './PageControls.js';
import { LorasControls } from './LorasControls.js';
import { CheckpointsControls } from './CheckpointsControls.js';
import { EmbeddingsControls } from './EmbeddingsControls.js';
import { OtherControls } from './OtherControls.js';
// Export the classes
export { PageControls, LorasControls, CheckpointsControls, EmbeddingsControls };
export { PageControls, LorasControls, CheckpointsControls, EmbeddingsControls, OtherControls };
/**
* Factory function to create the appropriate controls based on page type
* @param {string} pageType - The type of page ('loras', 'checkpoints', or 'embeddings')
* @param {string} pageType - The type of page ('loras', 'checkpoints', 'embeddings', or 'other')
* @returns {PageControls} - The appropriate controls instance
*/
export function createPageControls(pageType) {
@@ -19,8 +20,10 @@ export function createPageControls(pageType) {
return new CheckpointsControls();
} else if (pageType === 'embeddings') {
return new EmbeddingsControls();
} else if (pageType === 'other') {
return new OtherControls();
} else {
console.error(`Unknown page type: ${pageType}`);
return null;
}
}
}
+3
View File
@@ -58,6 +58,8 @@ class InitializationManager {
this.pageType = 'recipes';
} else if (path.includes('/checkpoints')) {
this.pageType = 'checkpoints';
} else if (path.includes('/other')) {
this.pageType = 'other';
} else if (path.includes('/loras')) {
this.pageType = 'loras';
} else if (path.includes('/embeddings')) {
@@ -221,6 +223,7 @@ class InitializationManager {
'lora': 'loras',
'checkpoint': 'checkpoints',
'embedding': 'embeddings',
'other': 'other',
'recipe': 'recipes'
};
+5
View File
@@ -250,6 +250,11 @@ function handleCopyAction(card, modelType) {
const embeddingCode = folder ? `embedding:${folder}/${name}` : `embedding:${name}`;
const message = translate('modelCard.actions.embeddingNameCopied', {}, 'Embedding syntax copied');
copyToClipboard(embeddingCode, message);
} else {
// Other model types (VAE, upscalers, ...) - copy the file name
const fileName = card.dataset.file_name;
const message = translate('modelCard.actions.modelNameCopied', {}, 'Model name copied');
copyToClipboard(fileName, message);
}
}