mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-21 03:01:27 -03:00
feat(frontend): enable downloads on Other page and default_other_roots settings UI
This commit is contained in:
@@ -146,6 +146,7 @@ export const MODEL_SPECIFIC_ENDPOINTS = {
|
||||
},
|
||||
[MODEL_TYPES.OTHER]: {
|
||||
metadata: `/api/lm/${MODEL_TYPES.OTHER}/metadata`,
|
||||
roots_by_subtype: `/api/lm/${MODEL_TYPES.OTHER}/roots_by_subtype`,
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -4,4 +4,42 @@ import { BaseModelApiClient } from './baseModelApi.js';
|
||||
* Other-models-specific API client (VAE, upscalers, text encoders, etc.)
|
||||
*/
|
||||
export class OtherApiClient extends BaseModelApiClient {
|
||||
/**
|
||||
* Get other-model roots, optionally narrowed to one sub_type
|
||||
* (vae/upscaler/text_encoder/clip_vision/controlnet).
|
||||
*
|
||||
* Without a sub_type this falls back to the merged roots list
|
||||
* (GET /api/lm/other/roots); with one it reads the grouped
|
||||
* roots_by_subtype map and extracts the matching list.
|
||||
*/
|
||||
async fetchModelRoots(subType = null) {
|
||||
if (!subType) {
|
||||
return super.fetchModelRoots();
|
||||
}
|
||||
|
||||
const data = await this.fetchRootsBySubType();
|
||||
const groupedRoots = data.roots_by_subtype || {};
|
||||
return {
|
||||
success: data.success !== false,
|
||||
roots: groupedRoots[subType] || [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get other-model roots grouped by sub_type.
|
||||
* GET /api/lm/other/roots_by_subtype
|
||||
* -> { success, roots_by_subtype: {sub_type: [...]} }
|
||||
*/
|
||||
async fetchRootsBySubType() {
|
||||
try {
|
||||
const response = await fetch(this.apiConfig.endpoints.specific.roots_by_subtype);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch other-model roots by sub_type');
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error fetching other-model roots by sub_type:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { PageControls } from './PageControls.js';
|
||||
import { getModelApiClient, resetAndReload } from '../../api/modelApiFactory.js';
|
||||
import { showToast } from '../../utils/uiHelpers.js';
|
||||
import { downloadManager } from '../../managers/DownloadManager.js';
|
||||
|
||||
/**
|
||||
* OtherControls class - Extends PageControls for the Other Models page
|
||||
@@ -39,6 +40,11 @@ export class OtherControls extends PageControls {
|
||||
return await getModelApiClient().fetchCivitaiMetadata();
|
||||
},
|
||||
|
||||
// Add show download modal functionality
|
||||
showDownloadModal: () => {
|
||||
downloadManager.showDownloadModal();
|
||||
},
|
||||
|
||||
toggleBulkMode: () => {
|
||||
if (window.bulkManager) {
|
||||
window.bulkManager.toggleBulkMode();
|
||||
|
||||
@@ -8,6 +8,7 @@ import { isModelWeightFile } from '../utils/modelFileTypes.js';
|
||||
import { getStorageItem, setStorageItem } from '../utils/storageHelpers.js';
|
||||
import { FolderTreeManager } from '../components/FolderTreeManager.js';
|
||||
import { translate } from '../utils/i18nHelpers.js';
|
||||
import { MODEL_SUBTYPE_DISPLAY_NAMES } from '../utils/constants.js';
|
||||
import { buildCivitaiUrl, extractCivitaiModelUrlParts, normalizeCivitaiPageHost } from '../utils/civitaiUtils.js';
|
||||
import { formatFileSize } from '../utils/formatters.js';
|
||||
import { showDownloadBatchSummary } from '../components/DownloadBatchSummaryModal.js';
|
||||
@@ -956,11 +957,17 @@ export class DownloadManager {
|
||||
|
||||
try {
|
||||
this._isDiffusionModel = await this._resolveIsDiffusionModel();
|
||||
this._otherSubType = await this._resolveOtherSubType();
|
||||
|
||||
let rootsData;
|
||||
if (this._isDiffusionModel && this.apiClient.modelType === 'checkpoints') {
|
||||
rootsData = await this.apiClient.fetchModelRoots('diffusion_model');
|
||||
} else if (this.apiClient.modelType === 'other' && this._otherSubType) {
|
||||
rootsData = await this.apiClient.fetchModelRoots(this._otherSubType);
|
||||
} else {
|
||||
// An undecidable other sub_type (null) intentionally lands
|
||||
// here: fetchModelRoots() lists all other roots so the user
|
||||
// can pick manually.
|
||||
rootsData = await this.apiClient.fetchModelRoots();
|
||||
}
|
||||
const modelRoot = document.getElementById('modelRoot');
|
||||
@@ -968,19 +975,29 @@ export class DownloadManager {
|
||||
`<option value="${root}">${root}</option>`
|
||||
).join('');
|
||||
|
||||
const singularType = this._isDiffusionModel
|
||||
? 'unet'
|
||||
: this.apiClient.modelType.replace(/s$/, '');
|
||||
const defaultRootKey = `default_${singularType}_root`;
|
||||
const defaultRoot = state.global.settings[defaultRootKey];
|
||||
console.log(`Default root for ${singularType}:`, defaultRoot);
|
||||
let defaultRoot;
|
||||
let subtypeDisplay;
|
||||
if (this.apiClient.modelType === 'other') {
|
||||
const otherDefaultRoots = state.global.settings.default_other_roots || {};
|
||||
defaultRoot = this._otherSubType ? (otherDefaultRoots[this._otherSubType] || '') : '';
|
||||
subtypeDisplay = this._otherSubType
|
||||
? (MODEL_SUBTYPE_DISPLAY_NAMES[this._otherSubType] || this._otherSubType)
|
||||
: this.apiClient.apiConfig.config.displayName;
|
||||
} else {
|
||||
const singularType = this._isDiffusionModel
|
||||
? 'unet'
|
||||
: this.apiClient.modelType.replace(/s$/, '');
|
||||
const defaultRootKey = `default_${singularType}_root`;
|
||||
defaultRoot = state.global.settings[defaultRootKey];
|
||||
subtypeDisplay = this._isDiffusionModel ? 'Diffusion Model' : this.apiClient.apiConfig.config.displayName;
|
||||
}
|
||||
console.log('Default root:', defaultRoot);
|
||||
console.log('Available roots:', rootsData.roots);
|
||||
if (defaultRoot && rootsData.roots.includes(defaultRoot)) {
|
||||
console.log(`Setting default root: ${defaultRoot}`);
|
||||
modelRoot.value = defaultRoot;
|
||||
}
|
||||
|
||||
const subtypeDisplay = this._isDiffusionModel ? 'Diffusion Model' : this.apiClient.apiConfig.config.displayName;
|
||||
document.getElementById('modelRootLabel').textContent =
|
||||
translate('modals.download.selectTypeRoot', { type: subtypeDisplay });
|
||||
|
||||
@@ -1065,6 +1082,50 @@ export class DownloadManager {
|
||||
return localFileTypeCheck;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve which other-page sub_type (vae/upscaler/text_encoder/
|
||||
* clip_vision/controlnet) this download routes to. The backend owns the
|
||||
* routing rule (explicit file pick first, model.type next, file.type
|
||||
* fallback), so the location step sends both the picked file's type
|
||||
* (selected_file_type) and the version's full file-type list and lets
|
||||
* the backend apply its priority chain. Returns null when the sub_type
|
||||
* cannot be decided; the location step then lists all other roots for
|
||||
* manual selection instead of guessing a folder.
|
||||
*/
|
||||
async _resolveOtherSubType() {
|
||||
// Only other-page downloads route by sub_type; without version
|
||||
// metadata (e.g. Hugging Face downloads) there is nothing to route on.
|
||||
if (this.apiClient.modelType !== 'other'
|
||||
|| (!this.selectedFile && !this.currentVersion)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const fileTypes = (this.currentVersion?.files || []).map(f => f.type);
|
||||
const response = await fetch(DOWNLOAD_ENDPOINTS.routing, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model_type: 'other',
|
||||
base_model: this.currentVersion?.baseModel || '',
|
||||
file_types: fileTypes,
|
||||
...(this.selectedFile
|
||||
? { selected_file_type: this.selectedFile.type }
|
||||
: {}),
|
||||
}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`routing endpoint returned ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
return data.sub_type || null;
|
||||
} catch (error) {
|
||||
console.warn('[download] other routing endpoint unavailable, '
|
||||
+ 'falling back to manual root selection:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
loadDefaultPathSetting() {
|
||||
const modelType = this.apiClient.modelType;
|
||||
const storageKey = `use_default_path_${modelType}`;
|
||||
|
||||
@@ -1153,6 +1153,9 @@ export class SettingsManager {
|
||||
// Load default unet root
|
||||
await this.loadUnetRoots();
|
||||
|
||||
// Load default other-model roots (per sub_type)
|
||||
await this.loadOtherRoots();
|
||||
|
||||
// Load extra folder paths
|
||||
this.loadExtraFolderPaths();
|
||||
|
||||
@@ -1658,6 +1661,51 @@ export class SettingsManager {
|
||||
}
|
||||
}
|
||||
|
||||
async loadOtherRoots() {
|
||||
const selects = document.querySelectorAll('select[data-other-root-subtype]');
|
||||
if (!selects.length) return;
|
||||
|
||||
try {
|
||||
// Fetch other-model roots grouped by sub_type
|
||||
const response = await fetch('/api/lm/other/roots_by_subtype');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch other model roots');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const groupedRoots = data.roots_by_subtype || {};
|
||||
const defaultRoots = state.global.settings.default_other_roots || {};
|
||||
|
||||
selects.forEach((select) => {
|
||||
const subType = select.dataset.otherRootSubtype;
|
||||
const roots = groupedRoots[subType] || [];
|
||||
if (!roots.length) {
|
||||
this.showNoRootsPlaceholder(select);
|
||||
return;
|
||||
}
|
||||
|
||||
select.innerHTML = '';
|
||||
select.disabled = false;
|
||||
|
||||
// Add options for each root
|
||||
roots.forEach(root => {
|
||||
const option = document.createElement('option');
|
||||
option.value = root;
|
||||
option.textContent = root;
|
||||
select.appendChild(option);
|
||||
});
|
||||
|
||||
const defaultRoot = defaultRoots[subType] || '';
|
||||
select.value = roots.includes(defaultRoot) ? defaultRoot : roots[0];
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading other model roots:', error);
|
||||
selects.forEach((select) => this.showNoRootsPlaceholder(select));
|
||||
showToast('toast.settings.otherRootsFailed', { message: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async loadEmbeddingRoots() {
|
||||
const defaultEmbeddingRootSelect = document.getElementById('defaultEmbeddingRoot');
|
||||
if (!defaultEmbeddingRootSelect) return;
|
||||
@@ -2339,6 +2387,27 @@ export class SettingsManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save one sub_type entry of the default_other_roots dict setting
|
||||
* (read-modify-write: the backend stores the whole mapping).
|
||||
*/
|
||||
async saveOtherRootSetting(subType, value) {
|
||||
try {
|
||||
const defaultRoots = { ...(state.global.settings.default_other_roots || {}) };
|
||||
if (value) {
|
||||
defaultRoots[subType] = value;
|
||||
} else {
|
||||
delete defaultRoots[subType];
|
||||
}
|
||||
|
||||
await this.saveSetting('default_other_roots', defaultRoots);
|
||||
|
||||
showToast('toast.settings.settingsUpdated', { setting: `default ${subType} root` }, 'success');
|
||||
} catch (error) {
|
||||
showToast('toast.settings.settingSaveFailed', { message: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the recipes page layout (grid | masonry) and rebuild the scroller.
|
||||
* Shared entry point for the settings modal segmented control and the
|
||||
|
||||
@@ -24,6 +24,7 @@ const DEFAULT_SETTINGS_BASE = Object.freeze({
|
||||
default_lora_root: '',
|
||||
default_checkpoint_root: '',
|
||||
default_embedding_root: '',
|
||||
default_other_roots: {},
|
||||
recipes_path: '',
|
||||
base_model_path_mappings: {},
|
||||
download_path_templates: {},
|
||||
@@ -72,6 +73,7 @@ export function createDefaultSettings() {
|
||||
base_model_path_mappings: {},
|
||||
download_path_templates: { ...DEFAULT_PATH_TEMPLATES },
|
||||
priority_tags: { ...DEFAULT_PRIORITY_TAG_CONFIG },
|
||||
default_other_roots: {},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user