mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-20 18:51:26 -03:00
feat(frontend): opt-in Other Models toggles, hidden nav and announcement
- The Other nav entry is hidden while the feature is off (nav-item--hidden, toggled client-side after enabling) and now uses the fa-shapes icon. - Shared utils/otherModels.js helpers (enable through the settings API, open the settings Library section) are reused by the disabled page, the announcement banner and the download modal. - BannerService registers a one-time dismissible "other-models-announcement" banner while the feature is off; SettingsManager drops the banner and updates the nav when the master switch flips. - A disabled download routing answer now surfaces a showActionToast with an "Enable Other Models" action. - Settings UI: master toggle + five sub_type checkboxes whose default-root selects are disabled when unchecked; i18n keys added to en.json and synced (other locales keep TODO placeholders).
This commit is contained in:
@@ -6,9 +6,11 @@ import {
|
||||
import { translate } from '../utils/i18nHelpers.js';
|
||||
import { state } from '../state/index.js';
|
||||
import { getModelApiClient } from '../api/modelApiFactory.js';
|
||||
import { enableOtherModels, openOtherModelsSettings } from '../utils/otherModels.js';
|
||||
|
||||
const COMMUNITY_SUPPORT_BANNER_ID = 'community-support';
|
||||
const CACHE_HEALTH_BANNER_ID = 'cache-health-warning';
|
||||
const OTHER_MODELS_BANNER_ID = 'other-models-announcement';
|
||||
const COMMUNITY_SUPPORT_BANNER_DELAY_MS = 5 * 24 * 60 * 60 * 1000; // 5 days
|
||||
const COMMUNITY_SUPPORT_FIRST_SEEN_AT_KEY = 'community_support_banner_first_seen_at';
|
||||
const COMMUNITY_SUPPORT_VERSION_KEY = 'community_support_banner_state_version';
|
||||
@@ -80,6 +82,7 @@ class BannerService {
|
||||
});
|
||||
|
||||
this.prepareCommunitySupportBanner();
|
||||
this.prepareOtherModelsBanner();
|
||||
|
||||
await this.showActiveBanners();
|
||||
this.initialized = true;
|
||||
@@ -541,6 +544,92 @@ class BannerService {
|
||||
this.updateContainerVisibility();
|
||||
}
|
||||
|
||||
/**
|
||||
* Announce the opt-in Other Models management to users who have not turned
|
||||
* it on yet. Dismissal is persisted through the shared dismissed_banners
|
||||
* setting, so users who are not interested are not nagged again.
|
||||
*/
|
||||
prepareOtherModelsBanner() {
|
||||
if (state.global.settings.enable_other_models) {
|
||||
return;
|
||||
}
|
||||
if (this.isBannerDismissed(OTHER_MODELS_BANNER_ID)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.registerBanner(OTHER_MODELS_BANNER_ID, {
|
||||
id: OTHER_MODELS_BANNER_ID,
|
||||
title: translate(
|
||||
'banners.otherModels.title',
|
||||
{},
|
||||
'Other Models Management is available'
|
||||
),
|
||||
content: translate(
|
||||
'banners.otherModels.content',
|
||||
{},
|
||||
'Scan and manage VAE, upscaler, text encoder and CLIP vision files — and download them from CivitAI — from one dedicated page.'
|
||||
),
|
||||
actions: [
|
||||
{
|
||||
text: translate(
|
||||
'banners.otherModels.enable',
|
||||
{},
|
||||
'Enable Other Models'
|
||||
),
|
||||
icon: 'fas fa-shapes',
|
||||
type: 'primary',
|
||||
action: 'enable-other-models'
|
||||
},
|
||||
{
|
||||
text: translate(
|
||||
'banners.otherModels.openSettings',
|
||||
{},
|
||||
'Open Settings'
|
||||
),
|
||||
icon: 'fas fa-cog',
|
||||
type: 'secondary',
|
||||
action: 'open-other-models-settings'
|
||||
}
|
||||
],
|
||||
dismissible: true,
|
||||
priority: 0,
|
||||
onRegister: (bannerElement) => {
|
||||
const enableButton = bannerElement.querySelector(
|
||||
'.banner-action[data-action="enable-other-models"]'
|
||||
);
|
||||
if (enableButton) {
|
||||
enableButton.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
enableOtherModels().catch((error) => {
|
||||
console.error('Failed to enable Other Models:', error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const settingsButton = bannerElement.querySelector(
|
||||
'.banner-action[data-action="open-other-models-settings"]'
|
||||
);
|
||||
if (settingsButton) {
|
||||
settingsButton.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
openOtherModelsSettings();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.updateContainerVisibility();
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop the Other Models announcement once the feature is enabled.
|
||||
* Dismissal is deliberately NOT persisted, so the announcement can come
|
||||
* back if the user switches the feature off again.
|
||||
*/
|
||||
removeOtherModelsAnnouncement() {
|
||||
this.removeBannerElement(OTHER_MODELS_BANNER_ID);
|
||||
}
|
||||
|
||||
initializeCommunitySupportState() {
|
||||
const storedVersion = getStorageItem(COMMUNITY_SUPPORT_VERSION_KEY, null);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { modalManager } from './ModalManager.js';
|
||||
import { showToast, setupAutoNewlineOnPaste } from '../utils/uiHelpers.js';
|
||||
import { showToast, showActionToast, setupAutoNewlineOnPaste } from '../utils/uiHelpers.js';
|
||||
import { state } from '../state/index.js';
|
||||
import { LoadingManager } from './LoadingManager.js';
|
||||
import { getModelApiClient, resetAndReload } from '../api/modelApiFactory.js';
|
||||
@@ -12,6 +12,7 @@ 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';
|
||||
import { openOtherModelsSettings } from '../utils/otherModels.js';
|
||||
|
||||
export class DownloadManager {
|
||||
constructor() {
|
||||
@@ -1118,6 +1119,16 @@ export class DownloadManager {
|
||||
throw new Error(`routing endpoint returned ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
if (data.disabled) {
|
||||
// The matching sub_type (or the whole Other Models feature) is
|
||||
// switched off: auto-routing is refused, so offer the settings
|
||||
// shortcut while the user's intent is clear.
|
||||
showActionToast('other.disabled.downloadBlocked', {}, 'warning', {
|
||||
actionText: translate('other.disabled.enableAction', {}, 'Enable Other Models'),
|
||||
onAction: () => openOtherModelsSettings(),
|
||||
});
|
||||
return null;
|
||||
}
|
||||
return data.sub_type || null;
|
||||
} catch (error) {
|
||||
console.warn('[download] other routing endpoint unavailable, '
|
||||
|
||||
@@ -1155,6 +1155,7 @@ export class SettingsManager {
|
||||
|
||||
// Load default other-model roots (per sub_type)
|
||||
await this.loadOtherRoots();
|
||||
this.updateOtherModelsControls();
|
||||
|
||||
// Load extra folder paths
|
||||
this.loadExtraFolderPaths();
|
||||
@@ -2304,6 +2305,16 @@ export class SettingsManager {
|
||||
await this.updateBackupStatus();
|
||||
}
|
||||
|
||||
if (settingKey === 'enable_other_models') {
|
||||
// Roots only exist while the feature is on, so re-fetch them
|
||||
// after the backend rebuilt the other-model root set.
|
||||
this.updateOtherModelsControls();
|
||||
await this.loadOtherRoots();
|
||||
this.updateOtherModelsControls();
|
||||
this.updateOtherModelsNavVisibility(value);
|
||||
this.removeOtherModelsAnnouncement(value);
|
||||
}
|
||||
|
||||
showToast('toast.settings.settingsUpdated', { setting: settingKey.replace(/_/g, ' ') }, 'success');
|
||||
|
||||
// Apply frontend settings immediately
|
||||
@@ -2408,6 +2419,82 @@ export class SettingsManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reflect the opt-in Other Models state in the settings UI: the master
|
||||
* toggle gates every sub_type checkbox, and a switched-off sub_type has
|
||||
* its default-root select disabled. Never force-enables a select (the
|
||||
* no-roots placeholder owns that state).
|
||||
*/
|
||||
updateOtherModelsControls() {
|
||||
const enableOtherModels = !!state.global.settings.enable_other_models;
|
||||
const enabledSubTypes = new Set(
|
||||
state.global.settings.enabled_other_sub_types
|
||||
|| ['vae', 'upscaler', 'text_encoder', 'clip_vision']
|
||||
);
|
||||
|
||||
document.querySelectorAll('[data-other-subtype-toggle]').forEach((input) => {
|
||||
input.checked = enabledSubTypes.has(input.value);
|
||||
input.disabled = !enableOtherModels;
|
||||
});
|
||||
|
||||
const container = document.getElementById('otherSubTypeToggles');
|
||||
if (container) {
|
||||
container.classList.toggle('is-disabled', !enableOtherModels);
|
||||
}
|
||||
|
||||
document.querySelectorAll('select[data-other-root-subtype]').forEach((select) => {
|
||||
const subType = select.dataset.otherRootSubtype;
|
||||
if (!enableOtherModels || !enabledSubTypes.has(subType)) {
|
||||
select.disabled = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist the whole enabled_other_sub_types list (the backend stores an
|
||||
* allow-list) and refresh the per-sub_type default-root selects.
|
||||
*/
|
||||
async saveEnabledOtherSubTypes() {
|
||||
const values = Array.from(
|
||||
document.querySelectorAll('[data-other-subtype-toggle]')
|
||||
)
|
||||
.filter((input) => input.checked)
|
||||
.map((input) => input.value);
|
||||
|
||||
try {
|
||||
await this.saveSetting('enabled_other_sub_types', values);
|
||||
this.updateOtherModelsControls();
|
||||
await this.loadOtherRoots();
|
||||
this.updateOtherModelsControls();
|
||||
|
||||
showToast('toast.settings.settingsUpdated', { setting: 'other model types' }, 'success');
|
||||
} catch (error) {
|
||||
showToast('toast.settings.settingSaveFailed', { message: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show or hide the Other Models nav entry. The nav is server-rendered, so
|
||||
* toggling the class here keeps it in sync when the switch is flipped from
|
||||
* the settings modal (no reload needed).
|
||||
*/
|
||||
updateOtherModelsNavVisibility(enabled) {
|
||||
const navItem = document.getElementById('otherNavItem');
|
||||
if (navItem) {
|
||||
navItem.classList.toggle('nav-item--hidden', !enabled);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop the Other Models announcement banner once the feature is on.
|
||||
*/
|
||||
removeOtherModelsAnnouncement(enabled) {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
bannerService.removeOtherModelsAnnouncement();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the recipes page layout (grid | masonry) and rebuild the scroller.
|
||||
* Shared entry point for the settings modal segmented control and the
|
||||
|
||||
Reference in New Issue
Block a user