import { modalManager } from './ModalManager.js';
import { showToast } from '../utils/uiHelpers.js';
import { state, createDefaultSettings } from '../state/index.js';
import { resetAndReload } from '../api/modelApiFactory.js';
import { DOWNLOAD_PATH_TEMPLATES, MAPPABLE_BASE_MODELS, PATH_TEMPLATE_PLACEHOLDERS, DEFAULT_PATH_TEMPLATES } from '../utils/constants.js';
import { translate } from '../utils/i18nHelpers.js';
import { i18n } from '../i18n/index.js';
import { configureModelCardVideo } from '../components/shared/ModelCard.js';
export class SettingsManager {
constructor() {
this.initialized = false;
this.isOpen = false;
this.initializationPromise = null;
this.availableLibraries = {};
this.activeLibrary = '';
// Add initialization to sync with modal state
this.currentPage = document.body.dataset.page || 'loras';
this.backendSettingKeys = new Set(Object.keys(createDefaultSettings()));
// Start initialization but don't await here to avoid blocking constructor
this.initializationPromise = this.initializeSettings();
this.initialize();
}
// Add method to wait for initialization to complete
async waitForInitialization() {
if (this.initializationPromise) {
await this.initializationPromise;
}
}
async initializeSettings() {
// Reset to defaults before syncing
state.global.settings = createDefaultSettings();
// Sync settings from backend to frontend
await this.syncSettingsFromBackend();
}
async syncSettingsFromBackend() {
try {
const response = await fetch('/api/lm/settings');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.success && data.settings) {
state.global.settings = this.mergeSettingsWithDefaults(data.settings);
console.log('Settings synced from backend');
} else {
console.error('Failed to sync settings from backend:', data.error);
state.global.settings = this.mergeSettingsWithDefaults();
}
} catch (error) {
console.error('Failed to sync settings from backend:', error);
state.global.settings = this.mergeSettingsWithDefaults();
}
await this.applyLanguageSetting();
this.applyFrontendSettings();
}
async applyLanguageSetting() {
const desiredLanguage = state?.global?.settings?.language;
if (!desiredLanguage) {
return;
}
try {
if (i18n.getCurrentLocale() !== desiredLanguage) {
await i18n.setLanguage(desiredLanguage);
}
} catch (error) {
console.warn('Failed to apply language from settings:', error);
}
}
mergeSettingsWithDefaults(backendSettings = {}) {
const defaults = createDefaultSettings();
const merged = { ...defaults, ...backendSettings };
const baseMappings = backendSettings?.base_model_path_mappings;
if (baseMappings && typeof baseMappings === 'object' && !Array.isArray(baseMappings)) {
merged.base_model_path_mappings = baseMappings;
} else {
merged.base_model_path_mappings = defaults.base_model_path_mappings;
}
let templates = backendSettings?.download_path_templates;
if (typeof templates === 'string') {
try {
const parsed = JSON.parse(templates);
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
templates = parsed;
}
} catch (parseError) {
console.warn('Failed to parse download_path_templates string from backend, using defaults');
templates = null;
}
}
if (!templates || typeof templates !== 'object' || Array.isArray(templates)) {
templates = {};
}
merged.download_path_templates = { ...DEFAULT_PATH_TEMPLATES, ...templates };
Object.keys(merged).forEach(key => this.backendSettingKeys.add(key));
return merged;
}
// Helper method to determine if a setting should be saved to backend
isBackendSetting(settingKey) {
return this.backendSettingKeys.has(settingKey);
}
// Helper method to save setting based on whether it's frontend or backend
async saveSetting(settingKey, value) {
// Update state
state.global.settings[settingKey] = value;
if (!this.isBackendSetting(settingKey)) {
return;
}
// Save to backend
try {
const payload = {};
payload[settingKey] = value;
const response = await fetch('/api/lm/settings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload)
});
if (!response.ok) {
throw new Error('Failed to save setting to backend');
}
// Parse response and check for success
const data = await response.json();
if (data.success === false) {
throw new Error(data.error || 'Failed to save setting to backend');
}
} catch (error) {
console.error(`Failed to save backend setting ${settingKey}:`, error);
throw error;
}
}
initialize() {
if (this.initialized) return;
// Add event listener to sync state when modal is closed via other means (like Escape key)
const settingsModal = document.getElementById('settingsModal');
if (settingsModal) {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
this.isOpen = settingsModal.style.display === 'block';
// When modal is opened, update checkbox state from current settings
if (this.isOpen) {
this.loadSettingsToUI();
}
}
});
});
observer.observe(settingsModal, { attributes: true });
}
// Add event listeners for all toggle-visibility buttons
document.querySelectorAll('.toggle-visibility').forEach(button => {
button.addEventListener('click', () => this.toggleInputVisibility(button));
});
const openSettingsLocationButton = document.querySelector('.settings-open-location-button');
if (openSettingsLocationButton) {
openSettingsLocationButton.addEventListener('click', () => {
const filePath = openSettingsLocationButton.dataset.settingsPath;
this.openSettingsFileLocation(filePath);
});
}
['lora', 'checkpoint', 'embedding'].forEach(modelType => {
const customInput = document.getElementById(`${modelType}CustomTemplate`);
if (customInput) {
customInput.addEventListener('input', (e) => {
const template = e.target.value;
settingsManager.validateTemplate(modelType, template);
settingsManager.updateTemplatePreview(modelType, template);
});
customInput.addEventListener('blur', (e) => {
const template = e.target.value;
if (settingsManager.validateTemplate(modelType, template)) {
settingsManager.updateTemplate(modelType, template);
}
});
customInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.target.blur();
}
});
}
});
this.initialized = true;
}
async openSettingsFileLocation(filePath) {
if (!filePath) {
showToast('settings.openSettingsFileLocation.failed', {}, 'error');
return;
}
try {
const response = await fetch('/api/lm/open-file-location', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ file_path: filePath }),
});
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`);
}
showToast('settings.openSettingsFileLocation.success', {}, 'success');
} catch (error) {
console.error('Failed to open settings file location:', error);
showToast('settings.openSettingsFileLocation.failed', {}, 'error');
}
}
async loadSettingsToUI() {
// Set frontend settings from state
const blurMatureContentCheckbox = document.getElementById('blurMatureContent');
if (blurMatureContentCheckbox) {
blurMatureContentCheckbox.checked = state.global.settings.blur_mature_content ?? true;
}
const showOnlySFWCheckbox = document.getElementById('showOnlySFW');
if (showOnlySFWCheckbox) {
showOnlySFWCheckbox.checked = state.global.settings.show_only_sfw ?? false;
}
// Set video autoplay on hover setting
const autoplayOnHoverCheckbox = document.getElementById('autoplayOnHover');
if (autoplayOnHoverCheckbox) {
autoplayOnHoverCheckbox.checked = state.global.settings.autoplay_on_hover || false;
}
// Set display density setting
const displayDensitySelect = document.getElementById('displayDensity');
if (displayDensitySelect) {
displayDensitySelect.value = state.global.settings.display_density || 'default';
}
// Set card info display setting
const cardInfoDisplaySelect = document.getElementById('cardInfoDisplay');
if (cardInfoDisplaySelect) {
cardInfoDisplaySelect.value = state.global.settings.card_info_display || 'always';
}
// Set optimize example images setting
const optimizeExampleImagesCheckbox = document.getElementById('optimizeExampleImages');
if (optimizeExampleImagesCheckbox) {
optimizeExampleImagesCheckbox.checked = state.global.settings.optimize_example_images ?? true;
}
// Set auto download example images setting
const autoDownloadExampleImagesCheckbox = document.getElementById('autoDownloadExampleImages');
if (autoDownloadExampleImagesCheckbox) {
autoDownloadExampleImagesCheckbox.checked = state.global.settings.auto_download_example_images || false;
}
// Load download path templates
this.loadDownloadPathTemplates();
// Set include trigger words setting
const includeTriggerWordsCheckbox = document.getElementById('includeTriggerWords');
if (includeTriggerWordsCheckbox) {
includeTriggerWordsCheckbox.checked = state.global.settings.include_trigger_words || false;
}
// Load metadata archive settings
await this.loadMetadataArchiveSettings();
// Load base model path mappings
this.loadBaseModelMappings();
// Load library options
await this.loadLibraries();
// Load default lora root
await this.loadLoraRoots();
// Load default checkpoint root
await this.loadCheckpointRoots();
// Load default embedding root
await this.loadEmbeddingRoots();
// Load language setting
const languageSelect = document.getElementById('languageSelect');
if (languageSelect) {
const currentLanguage = state.global.settings.language || 'en';
languageSelect.value = currentLanguage;
}
this.loadProxySettings();
}
loadProxySettings() {
// Load proxy enabled setting
const proxyEnabledCheckbox = document.getElementById('proxyEnabled');
if (proxyEnabledCheckbox) {
proxyEnabledCheckbox.checked = state.global.settings.proxy_enabled || false;
// Add event listener for toggling proxy settings group visibility
proxyEnabledCheckbox.addEventListener('change', () => {
const proxySettingsGroup = document.getElementById('proxySettingsGroup');
if (proxySettingsGroup) {
proxySettingsGroup.style.display = proxyEnabledCheckbox.checked ? 'block' : 'none';
}
});
// Set initial visibility
const proxySettingsGroup = document.getElementById('proxySettingsGroup');
if (proxySettingsGroup) {
proxySettingsGroup.style.display = proxyEnabledCheckbox.checked ? 'block' : 'none';
}
}
// Load proxy type
const proxyTypeSelect = document.getElementById('proxyType');
if (proxyTypeSelect) {
proxyTypeSelect.value = state.global.settings.proxy_type || 'http';
}
// Load proxy host
const proxyHostInput = document.getElementById('proxyHost');
if (proxyHostInput) {
proxyHostInput.value = state.global.settings.proxy_host || '';
}
// Load proxy port
const proxyPortInput = document.getElementById('proxyPort');
if (proxyPortInput) {
proxyPortInput.value = state.global.settings.proxy_port || '';
}
// Load proxy username
const proxyUsernameInput = document.getElementById('proxyUsername');
if (proxyUsernameInput) {
proxyUsernameInput.value = state.global.settings.proxy_username || '';
}
// Load proxy password
const proxyPasswordInput = document.getElementById('proxyPassword');
if (proxyPasswordInput) {
proxyPasswordInput.value = state.global.settings.proxy_password || '';
}
}
async loadLibraries() {
const librarySelect = document.getElementById('librarySelect');
if (!librarySelect) {
return;
}
const setPlaceholderOption = (textKey, fallback) => {
librarySelect.innerHTML = '';
const option = document.createElement('option');
option.value = '';
option.textContent = translate(textKey, {}, fallback);
librarySelect.appendChild(option);
};
setPlaceholderOption('settings.folderSettings.loadingLibraries', 'Loading libraries...');
librarySelect.disabled = true;
try {
const response = await fetch('/api/lm/settings/libraries');
if (!response.ok) {
throw new Error('Failed to fetch library registry');
}
const data = await response.json();
if (data.success === false) {
throw new Error(data.error || 'Failed to fetch library registry');
}
const libraries = data.libraries && typeof data.libraries === 'object'
? data.libraries
: {};
this.availableLibraries = libraries;
const entries = Object.entries(libraries);
if (entries.length === 0) {
this.activeLibrary = '';
setPlaceholderOption('settings.folderSettings.noLibraries', 'No libraries configured');
return;
}
const activeName = data.active_library && libraries[data.active_library]
? data.active_library
: entries[0][0];
this.activeLibrary = activeName;
librarySelect.innerHTML = '';
const fragment = document.createDocumentFragment();
entries
.sort((a, b) => {
const nameA = this.getLibraryDisplayName(a[0], a[1]).toLowerCase();
const nameB = this.getLibraryDisplayName(b[0], b[1]).toLowerCase();
return nameA.localeCompare(nameB);
})
.forEach(([name, info]) => {
const option = document.createElement('option');
option.value = name;
option.textContent = this.getLibraryDisplayName(name, info);
fragment.appendChild(option);
});
librarySelect.appendChild(fragment);
librarySelect.value = activeName;
librarySelect.disabled = entries.length <= 1;
} catch (error) {
console.error('Error loading libraries:', error);
setPlaceholderOption('settings.folderSettings.noLibraries', 'No libraries configured');
this.availableLibraries = {};
this.activeLibrary = '';
librarySelect.disabled = true;
showToast('toast.settings.libraryLoadFailed', { message: error.message }, 'error');
}
}
getLibraryDisplayName(libraryName, libraryData = {}) {
if (libraryData && typeof libraryData === 'object') {
const metadata = libraryData.metadata;
if (metadata && typeof metadata === 'object' && metadata.display_name) {
return metadata.display_name;
}
if (libraryData.display_name) {
return libraryData.display_name;
}
}
return libraryName;
}
async handleLibraryChange() {
const librarySelect = document.getElementById('librarySelect');
if (!librarySelect) {
return;
}
const selectedLibrary = librarySelect.value;
if (!selectedLibrary || selectedLibrary === this.activeLibrary) {
librarySelect.value = this.activeLibrary;
return;
}
librarySelect.disabled = true;
try {
state.loadingManager.showSimpleLoading('Activating library...');
await this.activateLibrary(selectedLibrary);
// Add a short delay before reloading the page
await new Promise(resolve => setTimeout(resolve, 300));
window.location.reload();
} catch (error) {
console.error('Failed to activate library:', error);
showToast('toast.settings.libraryActivateFailed', { message: error.message }, 'error');
await this.loadLibraries();
} finally {
state.loadingManager.hide();
if (!document.hidden) {
librarySelect.disabled = librarySelect.options.length <= 1;
}
}
}
async activateLibrary(libraryName) {
const response = await fetch('/api/lm/settings/libraries/activate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ library: libraryName }),
});
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`);
}
const data = await response.json();
if (data.success === false) {
throw new Error(data.error || 'Failed to activate library');
}
const activeName = data.active_library || libraryName;
this.activeLibrary = activeName;
return data;
}
async loadLoraRoots() {
try {
const defaultLoraRootSelect = document.getElementById('defaultLoraRoot');
if (!defaultLoraRootSelect) return;
// Fetch lora roots
const response = await fetch('/api/lm/loras/roots');
if (!response.ok) {
throw new Error('Failed to fetch LoRA roots');
}
const data = await response.json();
if (!data.roots || data.roots.length === 0) {
throw new Error('No LoRA roots found');
}
// Clear existing options except the first one (No Default)
const noDefaultOption = defaultLoraRootSelect.querySelector('option[value=""]');
defaultLoraRootSelect.innerHTML = '';
defaultLoraRootSelect.appendChild(noDefaultOption);
// Add options for each root
data.roots.forEach(root => {
const option = document.createElement('option');
option.value = root;
option.textContent = root;
defaultLoraRootSelect.appendChild(option);
});
// Set selected value from settings
const defaultRoot = state.global.settings.default_lora_root || '';
defaultLoraRootSelect.value = defaultRoot;
} catch (error) {
console.error('Error loading LoRA roots:', error);
showToast('toast.settings.loraRootsFailed', { message: error.message }, 'error');
}
}
async loadCheckpointRoots() {
try {
const defaultCheckpointRootSelect = document.getElementById('defaultCheckpointRoot');
if (!defaultCheckpointRootSelect) return;
// Fetch checkpoint roots
const response = await fetch('/api/lm/checkpoints/roots');
if (!response.ok) {
throw new Error('Failed to fetch checkpoint roots');
}
const data = await response.json();
if (!data.roots || data.roots.length === 0) {
throw new Error('No checkpoint roots found');
}
// Clear existing options except the first one (No Default)
const noDefaultOption = defaultCheckpointRootSelect.querySelector('option[value=""]');
defaultCheckpointRootSelect.innerHTML = '';
defaultCheckpointRootSelect.appendChild(noDefaultOption);
// Add options for each root
data.roots.forEach(root => {
const option = document.createElement('option');
option.value = root;
option.textContent = root;
defaultCheckpointRootSelect.appendChild(option);
});
// Set selected value from settings
const defaultRoot = state.global.settings.default_checkpoint_root || '';
defaultCheckpointRootSelect.value = defaultRoot;
} catch (error) {
console.error('Error loading checkpoint roots:', error);
showToast('toast.settings.checkpointRootsFailed', { message: error.message }, 'error');
}
}
async loadEmbeddingRoots() {
try {
const defaultEmbeddingRootSelect = document.getElementById('defaultEmbeddingRoot');
if (!defaultEmbeddingRootSelect) return;
// Fetch embedding roots
const response = await fetch('/api/lm/embeddings/roots');
if (!response.ok) {
throw new Error('Failed to fetch embedding roots');
}
const data = await response.json();
if (!data.roots || data.roots.length === 0) {
throw new Error('No embedding roots found');
}
// Clear existing options except the first one (No Default)
const noDefaultOption = defaultEmbeddingRootSelect.querySelector('option[value=""]');
defaultEmbeddingRootSelect.innerHTML = '';
defaultEmbeddingRootSelect.appendChild(noDefaultOption);
// Add options for each root
data.roots.forEach(root => {
const option = document.createElement('option');
option.value = root;
option.textContent = root;
defaultEmbeddingRootSelect.appendChild(option);
});
// Set selected value from settings
const defaultRoot = state.global.settings.default_embedding_root || '';
defaultEmbeddingRootSelect.value = defaultRoot;
} catch (error) {
console.error('Error loading embedding roots:', error);
showToast('toast.settings.embeddingRootsFailed', { message: error.message }, 'error');
}
}
loadBaseModelMappings() {
const mappingsContainer = document.getElementById('baseModelMappingsContainer');
if (!mappingsContainer) return;
const mappings = state.global.settings.base_model_path_mappings || {};
// Clear existing mappings
mappingsContainer.innerHTML = '';
// Add existing mappings
Object.entries(mappings).forEach(([baseModel, pathValue]) => {
this.addMappingRow(baseModel, pathValue);
});
// Add empty row for new mappings if none exist
if (Object.keys(mappings).length === 0) {
this.addMappingRow('', '');
}
}
addMappingRow(baseModel = '', pathValue = '') {
const mappingsContainer = document.getElementById('baseModelMappingsContainer');
if (!mappingsContainer) return;
const row = document.createElement('div');
row.className = 'mapping-row';
const availableModels = MAPPABLE_BASE_MODELS.filter(model => {
const existingMappings = state.global.settings.base_model_path_mappings || {};
return !existingMappings.hasOwnProperty(model) || model === baseModel;
});
row.innerHTML = `
`;
// Add event listeners
const baseModelSelect = row.querySelector('.base-model-select');
const pathValueInput = row.querySelector('.path-value-input');
const removeBtn = row.querySelector('.remove-mapping-btn');
// Save on select change immediately
baseModelSelect.addEventListener('change', () => this.updateBaseModelMappings());
// Save on input blur or Enter key
pathValueInput.addEventListener('blur', () => this.updateBaseModelMappings());
pathValueInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.target.blur();
}
});
removeBtn.addEventListener('click', () => {
row.remove();
this.updateBaseModelMappings();
});
mappingsContainer.appendChild(row);
}
updateBaseModelMappings() {
const mappingsContainer = document.getElementById('baseModelMappingsContainer');
if (!mappingsContainer) return;
const rows = mappingsContainer.querySelectorAll('.mapping-row');
const newMappings = {};
let hasValidMapping = false;
rows.forEach(row => {
const baseModelSelect = row.querySelector('.base-model-select');
const pathValueInput = row.querySelector('.path-value-input');
const baseModel = baseModelSelect.value.trim();
const pathValue = pathValueInput.value.trim();
if (baseModel && pathValue) {
newMappings[baseModel] = pathValue;
hasValidMapping = true;
}
});
// Check if mappings have actually changed
const currentMappings = state.global.settings.base_model_path_mappings || {};
const mappingsChanged = JSON.stringify(currentMappings) !== JSON.stringify(newMappings);
if (mappingsChanged) {
// Update state and save
state.global.settings.base_model_path_mappings = newMappings;
this.saveBaseModelMappings();
}
// Add empty row if no valid mappings exist
const hasEmptyRow = Array.from(rows).some(row => {
const baseModelSelect = row.querySelector('.base-model-select');
const pathValueInput = row.querySelector('.path-value-input');
return !baseModelSelect.value && !pathValueInput.value;
});
if (!hasEmptyRow) {
this.addMappingRow('', '');
}
// Update available options in all selects
this.updateAvailableBaseModels();
}
updateAvailableBaseModels() {
const mappingsContainer = document.getElementById('baseModelMappingsContainer');
if (!mappingsContainer) return;
const existingMappings = state.global.settings.base_model_path_mappings || {};
const rows = mappingsContainer.querySelectorAll('.mapping-row');
rows.forEach(row => {
const select = row.querySelector('.base-model-select');
const currentValue = select.value;
// Get available models (not already mapped, except current)
const availableModels = MAPPABLE_BASE_MODELS.filter(model =>
!existingMappings.hasOwnProperty(model) || model === currentValue
);
// Rebuild options
select.innerHTML = `` +
availableModels.map(model =>
``
).join('');
});
}
async saveBaseModelMappings() {
try {
// Save to backend using universal save method
await this.saveSetting('base_model_path_mappings', state.global.settings.base_model_path_mappings);
// Show success toast
const mappingCount = Object.keys(state.global.settings.base_model_path_mappings).length;
if (mappingCount > 0) {
showToast('toast.settings.mappingsUpdated', {
count: mappingCount,
plural: mappingCount !== 1 ? 's' : ''
}, 'success');
} else {
showToast('toast.settings.mappingsCleared', {}, 'success');
}
} catch (error) {
console.error('Error saving base model mappings:', error);
showToast('toast.settings.mappingSaveFailed', { message: error.message }, 'error');
}
}
loadDownloadPathTemplates() {
const templates = state.global.settings.download_path_templates || DEFAULT_PATH_TEMPLATES;
Object.keys(templates).forEach(modelType => {
this.loadTemplateForModelType(modelType, templates[modelType]);
});
}
loadTemplateForModelType(modelType, template) {
const presetSelect = document.getElementById(`${modelType}TemplatePreset`);
const customRow = document.getElementById(`${modelType}CustomRow`);
const customInput = document.getElementById(`${modelType}CustomTemplate`);
if (!presetSelect) return;
// Find matching preset
const matchingPreset = this.findMatchingPreset(template);
if (matchingPreset !== null) {
presetSelect.value = matchingPreset;
if (customRow) customRow.style.display = 'none';
} else {
// Custom template
presetSelect.value = 'custom';
if (customRow) customRow.style.display = 'block';
if (customInput) {
customInput.value = template;
this.validateTemplate(modelType, template);
}
}
this.updateTemplatePreview(modelType, template);
}
findMatchingPreset(template) {
const presetValues = Object.values(DOWNLOAD_PATH_TEMPLATES)
.map(t => t.value)
.filter(v => v !== 'custom');
return presetValues.includes(template) ? template : null;
}
updateTemplatePreset(modelType, value) {
const customRow = document.getElementById(`${modelType}CustomRow`);
const customInput = document.getElementById(`${modelType}CustomTemplate`);
if (value === 'custom') {
if (customRow) customRow.style.display = 'block';
if (customInput) customInput.focus();
return;
} else {
if (customRow) customRow.style.display = 'none';
}
// Update template
this.updateTemplate(modelType, value);
}
updateTemplate(modelType, template) {
// Validate template if it's custom
if (document.getElementById(`${modelType}TemplatePreset`).value === 'custom') {
if (!this.validateTemplate(modelType, template)) {
return; // Don't save invalid templates
}
}
// Update state
if (!state.global.settings.download_path_templates) {
state.global.settings.download_path_templates = { ...DEFAULT_PATH_TEMPLATES };
}
state.global.settings.download_path_templates[modelType] = template;
// Update preview
this.updateTemplatePreview(modelType, template);
// Save settings
this.saveDownloadPathTemplates();
}
validateTemplate(modelType, template) {
const validationElement = document.getElementById(`${modelType}Validation`);
if (!validationElement) return true;
// Reset validation state
validationElement.innerHTML = '';
validationElement.className = 'template-validation';
if (!template) {
validationElement.innerHTML = ` ${translate('settings.downloadPathTemplates.validation.validFlat', {}, 'Valid (flat structure)')}`;
validationElement.classList.add('valid');
return true;
}
// Check for invalid characters
const invalidChars = /[<>:"|?*]/;
if (invalidChars.test(template)) {
validationElement.innerHTML = ` ${translate('settings.downloadPathTemplates.validation.invalidChars', {}, 'Invalid characters detected')}`;
validationElement.classList.add('invalid');
return false;
}
// Check for double slashes
if (template.includes('//')) {
validationElement.innerHTML = ` ${translate('settings.downloadPathTemplates.validation.doubleSlashes', {}, 'Double slashes not allowed')}`;
validationElement.classList.add('invalid');
return false;
}
// Check if it starts or ends with slash
if (template.startsWith('/') || template.endsWith('/')) {
validationElement.innerHTML = ` ${translate('settings.downloadPathTemplates.validation.leadingTrailingSlash', {}, 'Cannot start or end with slash')}`;
validationElement.classList.add('invalid');
return false;
}
// Extract placeholders
const placeholderRegex = /\{([^}]+)\}/g;
const matches = template.match(placeholderRegex) || [];
// Check for invalid placeholders
const invalidPlaceholders = matches.filter(match =>
!PATH_TEMPLATE_PLACEHOLDERS.includes(match)
);
if (invalidPlaceholders.length > 0) {
validationElement.innerHTML = ` ${translate('settings.downloadPathTemplates.validation.invalidPlaceholder', { placeholder: invalidPlaceholders[0] }, `Invalid placeholder: ${invalidPlaceholders[0]}`)}`;
validationElement.classList.add('invalid');
return false;
}
// Template is valid
validationElement.innerHTML = ` ${translate('settings.downloadPathTemplates.validation.validTemplate', {}, 'Valid template')}`;
validationElement.classList.add('valid');
return true;
}
updateTemplatePreview(modelType, template) {
const previewElement = document.getElementById(`${modelType}Preview`);
if (!previewElement) return;
if (!template) {
previewElement.textContent = 'model-name.safetensors';
} else {
// Generate example preview
const exampleTemplate = template
.replace('{base_model}', 'Flux.1 D')
.replace('{author}', 'authorname')
.replace('{first_tag}', 'style');
previewElement.textContent = `${exampleTemplate}/model-name.safetensors`;
}
previewElement.style.display = 'block';
}
async saveDownloadPathTemplates() {
try {
// Save to backend using universal save method
await this.saveSetting('download_path_templates', state.global.settings.download_path_templates);
showToast('toast.settings.downloadTemplatesUpdated', {}, 'success');
} catch (error) {
console.error('Error saving download path templates:', error);
showToast('toast.settings.downloadTemplatesFailed', { message: error.message }, 'error');
}
}
toggleSettings() {
if (this.isOpen) {
modalManager.closeModal('settingsModal');
} else {
modalManager.showModal('settingsModal');
}
this.isOpen = !this.isOpen;
}
async saveToggleSetting(elementId, settingKey) {
const element = document.getElementById(elementId);
if (!element) return;
const value = element.checked;
try {
await this.saveSetting(settingKey, value);
if (settingKey === 'proxy_enabled') {
const proxySettingsGroup = document.getElementById('proxySettingsGroup');
if (proxySettingsGroup) {
proxySettingsGroup.style.display = value ? 'block' : 'none';
}
}
// Refresh metadata archive status when enable setting changes
if (settingKey === 'enable_metadata_archive_db') {
await this.updateMetadataArchiveStatus();
}
showToast('toast.settings.settingsUpdated', { setting: settingKey.replace(/_/g, ' ') }, 'success');
// Apply frontend settings immediately
this.applyFrontendSettings();
// Trigger auto download setup/teardown when setting changes
if (settingKey === 'auto_download_example_images' && window.exampleImagesManager) {
if (value) {
window.exampleImagesManager.setupAutoDownload();
} else {
window.exampleImagesManager.clearAutoDownload();
}
}
if (settingKey === 'show_only_sfw' || settingKey === 'blur_mature_content') {
this.reloadContent();
}
// Recalculate layout when compact mode changes
if (settingKey === 'compact_mode' && state.virtualScroller) {
state.virtualScroller.calculateLayout();
showToast('toast.settings.compactModeToggled', {
state: value ? 'toast.settings.compactModeEnabled' : 'toast.settings.compactModeDisabled'
}, 'success');
}
} catch (error) {
showToast('toast.settings.settingSaveFailed', { message: error.message }, 'error');
}
}
async saveSelectSetting(elementId, settingKey) {
const element = document.getElementById(elementId);
if (!element) return;
const value = element.value;
try {
// Update frontend state with mapped keys
await this.saveSetting(settingKey, value);
// Apply frontend settings immediately
this.applyFrontendSettings();
// Recalculate layout when display density changes
if (settingKey === 'display_density' && state.virtualScroller) {
state.virtualScroller.calculateLayout();
let densityName = "Default";
if (value === 'medium') densityName = "Medium";
if (value === 'compact') densityName = "Compact";
showToast('toast.settings.displayDensitySet', { density: densityName }, 'success');
return;
}
showToast('toast.settings.settingsUpdated', { setting: settingKey.replace(/_/g, ' ') }, 'success');
} catch (error) {
showToast('toast.settings.settingSaveFailed', { message: error.message }, 'error');
}
}
async loadMetadataArchiveSettings() {
try {
// Load current settings from state
const enableMetadataArchiveCheckbox = document.getElementById('enableMetadataArchive');
if (enableMetadataArchiveCheckbox) {
enableMetadataArchiveCheckbox.checked = state.global.settings.enable_metadata_archive_db || false;
}
// Load status
await this.updateMetadataArchiveStatus();
} catch (error) {
console.error('Error loading metadata archive settings:', error);
}
}
async updateMetadataArchiveStatus() {
try {
const response = await fetch('/api/lm/metadata-archive-status');
const data = await response.json();
const statusContainer = document.getElementById('metadataArchiveStatus');
if (statusContainer && data.success) {
const status = data;
const sizeText = status.databaseSize > 0 ? ` (${this.formatFileSize(status.databaseSize)})` : '';
statusContainer.innerHTML = `
${translate('settings.metadataArchive.status')}:
${status.isAvailable ? translate('settings.metadataArchive.statusAvailable') : translate('settings.metadataArchive.statusUnavailable')}
${sizeText}
${translate('settings.metadataArchive.enabled')}:
${status.isEnabled ? translate('common.status.enabled') : translate('common.status.disabled')}
`;
// Update button states
const downloadBtn = document.getElementById('downloadMetadataArchiveBtn');
const removeBtn = document.getElementById('removeMetadataArchiveBtn');
if (downloadBtn) {
downloadBtn.disabled = status.isAvailable;
downloadBtn.textContent = status.isAvailable ?
translate('settings.metadataArchive.downloadedButton') :
translate('settings.metadataArchive.downloadButton');
}
if (removeBtn) {
removeBtn.disabled = !status.isAvailable;
}
}
} catch (error) {
console.error('Error updating metadata archive status:', error);
}
}
formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
async downloadMetadataArchive() {
try {
const downloadBtn = document.getElementById('downloadMetadataArchiveBtn');
if (downloadBtn) {
downloadBtn.disabled = true;
downloadBtn.textContent = translate('settings.metadataArchive.downloadingButton');
}
// Show loading with enhanced progress
const progressUpdater = state.loadingManager.showEnhancedProgress(translate('settings.metadataArchive.preparing'));
// Set up WebSocket for progress updates
const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
const downloadId = `metadata_archive_${Date.now()}`;
const ws = new WebSocket(`${wsProtocol}${window.location.host}/ws/download-progress?id=${downloadId}`);
let wsConnected = false;
let actualDownloadId = downloadId; // Will be updated when WebSocket confirms the ID
// Promise to wait for WebSocket connection and ID confirmation
const wsReady = new Promise((resolve) => {
ws.onopen = () => {
wsConnected = true;
console.log('Connected to metadata archive download progress WebSocket');
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
// Handle download ID confirmation
if (data.type === 'download_id') {
actualDownloadId = data.download_id;
console.log(`Connected to metadata archive download progress with ID: ${data.download_id}`);
resolve(data.download_id);
return;
}
// Handle metadata archive download progress
if (data.type === 'metadata_archive_download') {
const message = data.message || '';
// Update progress bar based on stage
let progressPercent = 0;
if (data.stage === 'download') {
// Extract percentage from message if available
const percentMatch = data.message.match(/(\d+\.?\d*)%/);
if (percentMatch) {
progressPercent = Math.min(parseFloat(percentMatch[1]), 90); // Cap at 90% for download
} else {
progressPercent = 0; // Default download progress
}
} else if (data.stage === 'extract') {
progressPercent = 95; // Near completion for extraction
}
// Update loading manager progress
progressUpdater.updateProgress(progressPercent, '', `${message}`);
}
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
resolve(downloadId); // Fallback to original ID
};
// Timeout fallback
setTimeout(() => resolve(downloadId), 5000);
});
ws.onclose = () => {
console.log('WebSocket connection closed');
};
// Wait for WebSocket to be ready
await wsReady;
const response = await fetch(`/api/lm/download-metadata-archive?download_id=${encodeURIComponent(actualDownloadId)}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const data = await response.json();
// Close WebSocket
if (ws.readyState === WebSocket.OPEN) {
ws.close();
}
if (data.success) {
// Complete progress
await progressUpdater.complete(translate('settings.metadataArchive.downloadComplete'));
showToast('settings.metadataArchive.downloadSuccess', 'success');
// Update settings using universal save method
await this.saveSetting('enable_metadata_archive_db', true);
// Update UI
const enableCheckbox = document.getElementById('enableMetadataArchive');
if (enableCheckbox) {
enableCheckbox.checked = true;
}
await this.updateMetadataArchiveStatus();
} else {
// Hide loading on error
state.loadingManager.hide();
showToast('settings.metadataArchive.downloadError' + ': ' + data.error, 'error');
}
} catch (error) {
console.error('Error downloading metadata archive:', error);
// Hide loading on error
state.loadingManager.hide();
showToast('settings.metadataArchive.downloadError' + ': ' + error.message, 'error');
} finally {
const downloadBtn = document.getElementById('downloadMetadataArchiveBtn');
if (downloadBtn) {
downloadBtn.disabled = false;
downloadBtn.textContent = translate('settings.metadataArchive.downloadButton');
}
}
}
async removeMetadataArchive() {
if (!confirm(translate('settings.metadataArchive.removeConfirm'))) {
return;
}
try {
const removeBtn = document.getElementById('removeMetadataArchiveBtn');
if (removeBtn) {
removeBtn.disabled = true;
removeBtn.textContent = translate('settings.metadataArchive.removingButton');
}
const response = await fetch('/api/lm/remove-metadata-archive', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (data.success) {
showToast('settings.metadataArchive.removeSuccess', 'success');
// Update settings using universal save method
await this.saveSetting('enable_metadata_archive_db', false);
// Update UI
const enableCheckbox = document.getElementById('enableMetadataArchive');
if (enableCheckbox) {
enableCheckbox.checked = false;
}
await this.updateMetadataArchiveStatus();
} else {
showToast('settings.metadataArchive.removeError' + ': ' + data.error, 'error');
}
} catch (error) {
console.error('Error removing metadata archive:', error);
showToast('settings.metadataArchive.removeError' + ': ' + error.message, 'error');
} finally {
const removeBtn = document.getElementById('removeMetadataArchiveBtn');
if (removeBtn) {
removeBtn.disabled = false;
removeBtn.textContent = translate('settings.metadataArchive.removeButton');
}
}
}
async saveInputSetting(elementId, settingKey) {
const element = document.getElementById(elementId);
if (!element) return;
const value = element.value.trim(); // Trim whitespace
try {
// Check if value has changed from existing value
const currentValue = state.global.settings[settingKey] || '';
if (value === currentValue) {
return; // No change, exit early
}
// For username and password, handle empty values specially
if ((settingKey === 'proxy_username' || settingKey === 'proxy_password') && value === '') {
// Remove from state instead of setting to empty string
delete state.global.settings[settingKey];
// Send delete flag to backend
const payload = {};
payload[settingKey] = '__DELETE__';
const response = await fetch('/api/lm/settings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload)
});
if (!response.ok) {
throw new Error('Failed to delete setting');
}
} else {
// Use the universal save method
await this.saveSetting(settingKey, value);
}
showToast('toast.settings.settingsUpdated', { setting: settingKey.replace(/_/g, ' ') }, 'success');
} catch (error) {
showToast('toast.settings.settingSaveFailed', { message: error.message }, 'error');
}
}
async saveLanguageSetting() {
const element = document.getElementById('languageSelect');
if (!element) return;
const selectedLanguage = element.value;
try {
// Use the universal save method for language (frontend-only setting)
await this.saveSetting('language', selectedLanguage);
// Reload the page to apply the new language
window.location.reload();
} catch (error) {
showToast('toast.settings.languageChangeFailed', { message: error.message }, 'error');
}
}
toggleInputVisibility(button) {
const input = button.parentElement.querySelector('input');
const icon = button.querySelector('i');
if (input.type === 'password') {
input.type = 'text';
icon.className = 'fas fa-eye-slash';
} else {
input.type = 'password';
icon.className = 'fas fa-eye';
}
}
confirmClearCache() {
// Show confirmation modal
modalManager.showModal('clearCacheModal');
}
async reloadContent() {
if (this.currentPage === 'loras') {
// Reload the loras without updating folders
await resetAndReload(false);
} else if (this.currentPage === 'recipes') {
// Reload the recipes without updating folders
await window.recipeManager.loadRecipes();
} else if (this.currentPage === 'checkpoints') {
// Reload the checkpoints without updating folders
await resetAndReload(false);
} else if (this.currentPage === 'embeddings') {
// Reload the embeddings without updating folders
await resetAndReload(false);
}
}
applyFrontendSettings() {
// Apply autoplay setting to existing videos in card previews
const autoplayOnHover = state.global.settings.autoplay_on_hover;
document.querySelectorAll('.card-preview video').forEach(video => {
configureModelCardVideo(video, autoplayOnHover);
});
// Apply display density class to grid
const grid = document.querySelector('.card-grid');
if (grid) {
const density = state.global.settings.display_density || 'default';
// Remove all density classes first
grid.classList.remove('default-density', 'medium-density', 'compact-density');
// Add the appropriate density class
grid.classList.add(`${density}-density`);
}
// Apply card info display setting
const cardInfoDisplay = state.global.settings.card_info_display || 'always';
document.body.classList.toggle('hover-reveal', cardInfoDisplay === 'hover');
}
}
// Create singleton instance
export const settingsManager = new SettingsManager();