diff --git a/py/routes/misc_routes.py b/py/routes/misc_routes.py index b7598d97..3a6439da 100644 --- a/py/routes/misc_routes.py +++ b/py/routes/misc_routes.py @@ -179,7 +179,7 @@ class MiscRoutes: # Define keys that should be synced from backend to frontend sync_keys = [ 'civitai_api_key', - 'default_lora_root', + 'default_lora_root', 'default_checkpoint_root', 'default_embedding_root', 'base_model_path_mappings', @@ -193,8 +193,15 @@ class MiscRoutes: 'proxy_username', 'proxy_password', 'example_images_path', - 'optimizeExampleImages', - 'autoDownloadExampleImages' + 'optimize_example_images', + 'auto_download_example_images', + 'blur_mature_content', + 'autoplay_on_hover', + 'display_density', + 'card_info_display', + 'include_trigger_words', + 'show_only_sfw', + 'compact_mode' ] # Build response with only the keys that should be synced diff --git a/py/services/settings_manager.py b/py/services/settings_manager.py index adc1fb3a..1db42eb7 100644 --- a/py/services/settings_manager.py +++ b/py/services/settings_manager.py @@ -5,10 +5,41 @@ from typing import Any, Dict logger = logging.getLogger(__name__) + +DEFAULT_SETTINGS: Dict[str, Any] = { + "civitai_api_key": "", + "language": "en", + "show_only_sfw": False, + "enable_metadata_archive_db": False, + "proxy_enabled": False, + "proxy_host": "", + "proxy_port": "", + "proxy_username": "", + "proxy_password": "", + "proxy_type": "http", + "default_lora_root": "", + "default_checkpoint_root": "", + "default_embedding_root": "", + "base_model_path_mappings": {}, + "download_path_templates": {}, + "example_images_path": "", + "optimize_example_images": True, + "auto_download_example_images": False, + "blur_mature_content": True, + "autoplay_on_hover": False, + "display_density": "default", + "card_info_display": "always", + "include_trigger_words": False, + "compact_mode": False, +} + + class SettingsManager: def __init__(self): self.settings_file = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'settings.json') self.settings = self._load_settings() + self._migrate_setting_keys() + self._ensure_default_settings() self._migrate_download_path_template() self._auto_set_default_roots() self._check_environment_variables() @@ -23,11 +54,49 @@ class SettingsManager: logger.error(f"Error loading settings: {e}") return self._get_default_settings() + def _ensure_default_settings(self) -> None: + """Ensure all default settings keys exist""" + updated = False + for key, value in self._get_default_settings().items(): + if key not in self.settings: + if isinstance(value, dict): + self.settings[key] = value.copy() + else: + self.settings[key] = value + updated = True + if updated: + self._save_settings() + + def _migrate_setting_keys(self) -> None: + """Migrate legacy camelCase setting keys to snake_case""" + key_migrations = { + 'optimizeExampleImages': 'optimize_example_images', + 'autoDownloadExampleImages': 'auto_download_example_images', + 'blurMatureContent': 'blur_mature_content', + 'autoplayOnHover': 'autoplay_on_hover', + 'displayDensity': 'display_density', + 'cardInfoDisplay': 'card_info_display', + 'includeTriggerWords': 'include_trigger_words', + 'compactMode': 'compact_mode', + } + + updated = False + for old_key, new_key in key_migrations.items(): + if old_key in self.settings: + if new_key not in self.settings: + self.settings[new_key] = self.settings[old_key] + del self.settings[old_key] + updated = True + + if updated: + logger.info("Migrated legacy setting keys to snake_case") + self._save_settings() + def _migrate_download_path_template(self): """Migrate old download_path_template to new download_path_templates""" old_template = self.settings.get('download_path_template') templates = self.settings.get('download_path_templates') - + # If old template exists and new templates don't exist, migrate if old_template is not None and not templates: logger.info("Migrating download_path_template to download_path_templates") @@ -78,18 +147,11 @@ class SettingsManager: def _get_default_settings(self) -> Dict[str, Any]: """Return default settings""" - return { - "civitai_api_key": "", - "language": "en", - "show_only_sfw": False, # Show only SFW content - "enable_metadata_archive_db": False, # Enable metadata archive database - "proxy_enabled": False, # Enable app-level proxy - "proxy_host": "", # Proxy host - "proxy_port": "", # Proxy port - "proxy_username": "", # Proxy username (optional) - "proxy_password": "", # Proxy password (optional) - "proxy_type": "http" # Proxy type: http, https, socks4, socks5 - } + defaults = DEFAULT_SETTINGS.copy() + # Ensure nested dicts are independent copies + defaults['base_model_path_mappings'] = {} + defaults['download_path_templates'] = {} + return defaults def get(self, key: str, default: Any = None) -> Any: """Get setting value""" diff --git a/static/js/api/baseModelApi.js b/static/js/api/baseModelApi.js index df80d764..2d24efe4 100644 --- a/static/js/api/baseModelApi.js +++ b/static/js/api/baseModelApi.js @@ -945,7 +945,7 @@ export class BaseModelApiClient { } // Determine optimize setting - const optimize = state.global?.settings?.optimizeExampleImages ?? true; + const optimize = state.global?.settings?.optimize_example_images ?? true; // Make the API request to start the download process const response = await fetch(DOWNLOAD_ENDPOINTS.exampleImages, { diff --git a/static/js/components/RecipeCard.js b/static/js/components/RecipeCard.js index c496a928..94db918d 100644 --- a/static/js/components/RecipeCard.js +++ b/static/js/components/RecipeCard.js @@ -46,7 +46,7 @@ class RecipeCard { // NSFW blur logic - similar to LoraCard const nsfwLevel = this.recipe.preview_nsfw_level !== undefined ? this.recipe.preview_nsfw_level : 0; - const shouldBlur = state.settings.blurMatureContent && nsfwLevel > NSFW_LEVELS.PG13; + const shouldBlur = state.settings.blur_mature_content && nsfwLevel > NSFW_LEVELS.PG13; if (shouldBlur) { card.classList.add('nsfw-content'); diff --git a/static/js/components/shared/ModelCard.js b/static/js/components/shared/ModelCard.js index ba8db595..2a3c0d2f 100644 --- a/static/js/components/shared/ModelCard.js +++ b/static/js/components/shared/ModelCard.js @@ -405,7 +405,7 @@ export function createModelCard(model, modelType) { card.dataset.nsfwLevel = nsfwLevel; // Determine if the preview should be blurred based on NSFW level and user settings - const shouldBlur = state.settings.blurMatureContent && nsfwLevel > NSFW_LEVELS.PG13; + const shouldBlur = state.settings.blur_mature_content && nsfwLevel > NSFW_LEVELS.PG13; if (shouldBlur) { card.classList.add('nsfw-content'); } @@ -433,7 +433,7 @@ export function createModelCard(model, modelType) { } // Check if autoplayOnHover is enabled for video previews - const autoplayOnHover = state.global?.settings?.autoplayOnHover || false; + const autoplayOnHover = state.global?.settings?.autoplay_on_hover || false; const isVideo = previewUrl.endsWith('.mp4'); const videoAttrs = autoplayOnHover ? 'controls muted loop' : 'controls autoplay muted loop'; diff --git a/static/js/components/shared/showcase/ShowcaseView.js b/static/js/components/shared/showcase/ShowcaseView.js index 257753fd..91a6fec0 100644 --- a/static/js/components/shared/showcase/ShowcaseView.js +++ b/static/js/components/shared/showcase/ShowcaseView.js @@ -155,7 +155,7 @@ function renderMediaItem(img, index, exampleFiles) { // Check if media should be blurred const nsfwLevel = img.nsfwLevel !== undefined ? img.nsfwLevel : 0; - const shouldBlur = state.settings.blurMatureContent && nsfwLevel > NSFW_LEVELS.PG13; + const shouldBlur = state.settings.blur_mature_content && nsfwLevel > NSFW_LEVELS.PG13; // Determine NSFW warning text based on level let nsfwText = "Mature Content"; diff --git a/static/js/core.js b/static/js/core.js index 40eb50b0..9c0bd377 100644 --- a/static/js/core.js +++ b/static/js/core.js @@ -74,7 +74,7 @@ export class AppCore { // Initialize the help manager helpManager.initialize(); - const cardInfoDisplay = state.global.settings.cardInfoDisplay || 'always'; + const cardInfoDisplay = state.global.settings.card_info_display || 'always'; document.body.classList.toggle('hover-reveal', cardInfoDisplay === 'hover'); initializeEventManagement(); diff --git a/static/js/i18n/index.js b/static/js/i18n/index.js index 4e2a8259..79b79c08 100644 --- a/static/js/i18n/index.js +++ b/static/js/i18n/index.js @@ -1,3 +1,5 @@ +import { state } from '../state/index.js'; + /** * Internationalization (i18n) system for LoRA Manager * Uses user-selected language from settings with fallback to English @@ -123,26 +125,12 @@ class I18nManager { * @returns {string} Language code */ getLanguageFromSettings() { - // Check localStorage for user-selected language - const STORAGE_PREFIX = 'lora_manager_'; - let userLanguage = null; - - try { - const settings = localStorage.getItem(STORAGE_PREFIX + 'settings'); - if (settings) { - const parsedSettings = JSON.parse(settings); - userLanguage = parsedSettings.language; - } - } catch (e) { - console.warn('Failed to parse settings from localStorage:', e); + const language = state?.global?.settings?.language; + + if (language && this.availableLocales[language]) { + return language; } - - // If user has selected a language, use it - if (userLanguage && this.availableLocales[userLanguage]) { - return userLanguage; - } - - // Fallback to English + return 'en'; } @@ -165,18 +153,10 @@ class I18nManager { this.readyPromise = this.initializeWithLocale(languageCode); await this.readyPromise; - // Save to localStorage - const STORAGE_PREFIX = 'lora_manager_'; - const currentSettings = localStorage.getItem(STORAGE_PREFIX + 'settings'); - let settings = {}; - - if (currentSettings) { - settings = JSON.parse(currentSettings); + if (state?.global?.settings) { + state.global.settings.language = languageCode; } - settings.language = languageCode; - localStorage.setItem(STORAGE_PREFIX + 'settings', JSON.stringify(settings)); - console.log(`Language changed to: ${languageCode}`); // Dispatch event to notify components of language change diff --git a/static/js/managers/ExampleImagesManager.js b/static/js/managers/ExampleImagesManager.js index 1c710040..d2a05fef 100644 --- a/static/js/managers/ExampleImagesManager.js +++ b/static/js/managers/ExampleImagesManager.js @@ -63,7 +63,7 @@ export class ExampleImagesManager { } // Setup auto download if enabled - if (state.global.settings.autoDownloadExampleImages) { + if (state.global.settings.auto_download_example_images) { this.setupAutoDownload(); } @@ -106,7 +106,7 @@ export class ExampleImagesManager { showToast('toast.exampleImages.pathUpdateFailed', { message: error.message }, 'error'); } // Setup or clear auto download based on path availability - if (state.global.settings.autoDownloadExampleImages) { + if (state.global.settings.auto_download_example_images) { if (hasPath) { this.setupAutoDownload(); } else { @@ -225,7 +225,7 @@ export class ExampleImagesManager { } try { - const optimize = state.global.settings.optimizeExampleImages; + const optimize = state.global.settings.optimize_example_images; const response = await fetch('/api/lm/download-example-images', { method: 'POST', @@ -677,7 +677,7 @@ export class ExampleImagesManager { canAutoDownload() { // Check if auto download is enabled - if (!state.global.settings.autoDownloadExampleImages) { + if (!state.global.settings.auto_download_example_images) { return false; } @@ -713,7 +713,7 @@ export class ExampleImagesManager { try { console.log('Performing auto download check...'); - const optimize = state.global.settings.optimizeExampleImages; + const optimize = state.global.settings.optimize_example_images; const response = await fetch('/api/lm/download-example-images', { method: 'POST', diff --git a/static/js/managers/OnboardingManager.js b/static/js/managers/OnboardingManager.js index 047cd000..ecfdf3e7 100644 --- a/static/js/managers/OnboardingManager.js +++ b/static/js/managers/OnboardingManager.js @@ -182,9 +182,6 @@ export class OnboardingManager { // Update state state.global.settings.language = languageCode; - // Save to localStorage - setStorageItem('settings', state.global.settings); - // Save to backend const response = await fetch('/api/lm/settings', { method: 'POST', diff --git a/static/js/managers/SettingsManager.js b/static/js/managers/SettingsManager.js index 8d4da06a..b6ffe6c2 100644 --- a/static/js/managers/SettingsManager.js +++ b/static/js/managers/SettingsManager.js @@ -1,10 +1,10 @@ import { modalManager } from './ModalManager.js'; import { showToast } from '../utils/uiHelpers.js'; -import { state } from '../state/index.js'; +import { state, createDefaultSettings } from '../state/index.js'; import { resetAndReload } from '../api/modelApiFactory.js'; -import { setStorageItem, getStorageItem } from '../utils/storageHelpers.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'; export class SettingsManager { constructor() { @@ -14,7 +14,9 @@ export class SettingsManager { // 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(); @@ -29,177 +31,91 @@ export class SettingsManager { } async initializeSettings() { - // Load frontend-only settings from localStorage - this.loadFrontendSettingsFromStorage(); - + // Reset to defaults before syncing + state.global.settings = createDefaultSettings(); + // Sync settings from backend to frontend await this.syncSettingsFromBackend(); } - loadFrontendSettingsFromStorage() { - // Get saved settings from localStorage - const savedSettings = getStorageItem('settings'); - - // Frontend-only settings that should be stored in localStorage - const frontendOnlyKeys = [ - 'blurMatureContent', - 'autoplayOnHover', - 'displayDensity', - 'cardInfoDisplay', - 'includeTriggerWords' - ]; - - // Apply saved frontend settings to state if available - if (savedSettings) { - const frontendSettings = {}; - frontendOnlyKeys.forEach(key => { - if (savedSettings[key] !== undefined) { - frontendSettings[key] = savedSettings[key]; - } - }); - state.global.settings = { ...state.global.settings, ...frontendSettings }; - } - - // Initialize default values for frontend settings if they don't exist - if (state.global.settings.blurMatureContent === undefined) { - state.global.settings.blurMatureContent = true; - } - - if (state.global.settings.show_only_sfw === undefined) { - state.global.settings.show_only_sfw = false; - } - - if (state.global.settings.autoplayOnHover === undefined) { - state.global.settings.autoplayOnHover = false; - } - - if (state.global.settings.cardInfoDisplay === undefined) { - state.global.settings.cardInfoDisplay = 'always'; - } - - if (state.global.settings.displayDensity === undefined) { - // Migrate legacy compactMode if it exists - if (state.global.settings.compactMode === true) { - state.global.settings.displayDensity = 'compact'; - } else { - state.global.settings.displayDensity = 'default'; - } - } - - if (state.global.settings.includeTriggerWords === undefined) { - state.global.settings.includeTriggerWords = false; - } - - // Save updated frontend settings to localStorage - this.saveFrontendSettingsToStorage(); - } - 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) { - // Merge backend settings with current state - state.global.settings = { ...state.global.settings, ...data.settings }; - - // Set defaults for backend settings if they're null/undefined - this.setBackendSettingDefaults(); - + 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); - // Set defaults if backend sync fails - this.setBackendSettingDefaults(); + 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); } } - setBackendSettingDefaults() { - // Set defaults for backend settings - const backendDefaults = { - civitai_api_key: '', - default_lora_root: '', - default_checkpoint_root: '', - default_embedding_root: '', - base_model_path_mappings: {}, - download_path_templates: { ...DEFAULT_PATH_TEMPLATES }, - enable_metadata_archive_db: false, - language: 'en', - show_only_sfw: false, - proxy_enabled: false, - proxy_type: 'http', - proxy_host: '', - proxy_port: '', - proxy_username: '', - proxy_password: '', - example_images_path: '', - optimizeExampleImages: true, - autoDownloadExampleImages: false - }; + mergeSettingsWithDefaults(backendSettings = {}) { + const defaults = createDefaultSettings(); + const merged = { ...defaults, ...backendSettings }; - Object.keys(backendDefaults).forEach(key => { - if (state.global.settings[key] === undefined || state.global.settings[key] === null) { - state.global.settings[key] = backendDefaults[key]; + 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; } - }); + } - // Ensure all model types have templates - Object.keys(DEFAULT_PATH_TEMPLATES).forEach(modelType => { - if (!state.global.settings.download_path_templates[modelType]) { - state.global.settings.download_path_templates[modelType] = DEFAULT_PATH_TEMPLATES[modelType]; - } - }); - } + if (!templates || typeof templates !== 'object' || Array.isArray(templates)) { + templates = {}; + } - saveFrontendSettingsToStorage() { - // Save only frontend-specific settings to localStorage - const frontendOnlyKeys = [ - 'blurMatureContent', - 'autoplayOnHover', - 'displayDensity', - 'cardInfoDisplay', - 'includeTriggerWords' - ]; + merged.download_path_templates = { ...DEFAULT_PATH_TEMPLATES, ...templates }; - const frontendSettings = {}; - frontendOnlyKeys.forEach(key => { - if (state.global.settings[key] !== undefined) { - frontendSettings[key] = state.global.settings[key]; - } - }); + Object.keys(merged).forEach(key => this.backendSettingKeys.add(key)); - setStorageItem('settings', frontendSettings); + return merged; } // Helper method to determine if a setting should be saved to backend isBackendSetting(settingKey) { - const backendKeys = [ - 'civitai_api_key', - 'default_lora_root', - 'default_checkpoint_root', - 'default_embedding_root', - 'base_model_path_mappings', - 'download_path_templates', - 'enable_metadata_archive_db', - 'language', - 'show_only_sfw', - 'proxy_enabled', - 'proxy_type', - 'proxy_host', - 'proxy_port', - 'proxy_username', - 'proxy_password', - 'example_images_path', - 'optimizeExampleImages', - 'autoDownloadExampleImages' - ]; - return backendKeys.includes(settingKey); + return this.backendSettingKeys.has(settingKey); } // Helper method to save setting based on whether it's frontend or backend @@ -207,36 +123,35 @@ export class SettingsManager { // Update state state.global.settings[settingKey] = value; - if (this.isBackendSetting(settingKey)) { - // Save to backend - try { - const payload = {}; - payload[settingKey] = value; + if (!this.isBackendSetting(settingKey)) { + return; + } - const response = await fetch('/api/lm/settings', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(payload) - }); + // Save to backend + try { + const payload = {}; + payload[settingKey] = value; - if (!response.ok) { - throw new Error('Failed to save setting to backend'); - } + const response = await fetch('/api/lm/settings', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload) + }); - // 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; + if (!response.ok) { + throw new Error('Failed to save setting to backend'); } - } else { - // Save frontend settings to localStorage - this.saveFrontendSettingsToStorage(); + + // 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; } } @@ -298,43 +213,42 @@ export class SettingsManager { // Set frontend settings from state const blurMatureContentCheckbox = document.getElementById('blurMatureContent'); if (blurMatureContentCheckbox) { - blurMatureContentCheckbox.checked = state.global.settings.blurMatureContent; + blurMatureContentCheckbox.checked = state.global.settings.blur_mature_content ?? true; } - + const showOnlySFWCheckbox = document.getElementById('showOnlySFW'); if (showOnlySFWCheckbox) { - // Sync with state (backend will set this via template) - state.global.settings.show_only_sfw = showOnlySFWCheckbox.checked; + 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.autoplayOnHover || false; + 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.displayDensity || 'default'; + 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.cardInfoDisplay || 'always'; + 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.optimizeExampleImages || false; + 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.autoDownloadExampleImages || false; + autoDownloadExampleImagesCheckbox.checked = state.global.settings.auto_download_example_images || false; } // Load download path templates @@ -343,7 +257,7 @@ export class SettingsManager { // Set include trigger words setting const includeTriggerWordsCheckbox = document.getElementById('includeTriggerWords'); if (includeTriggerWordsCheckbox) { - includeTriggerWordsCheckbox.checked = state.global.settings.includeTriggerWords || false; + includeTriggerWordsCheckbox.checked = state.global.settings.include_trigger_words || false; } // Load metadata archive settings @@ -883,38 +797,17 @@ export class SettingsManager { if (!element) return; const value = element.checked; - + try { - // Update frontend state with mapped keys - if (settingKey === 'blur_mature_content') { - await this.saveSetting('blurMatureContent', value); - } else if (settingKey === 'show_only_sfw') { - await this.saveSetting('show_only_sfw', value); - } else if (settingKey === 'autoplay_on_hover') { - await this.saveSetting('autoplayOnHover', value); - } else if (settingKey === 'optimize_example_images') { - await this.saveSetting('optimizeExampleImages', value); - } else if (settingKey === 'auto_download_example_images') { - await this.saveSetting('autoDownloadExampleImages', value); - } else if (settingKey === 'compact_mode') { - await this.saveSetting('compactMode', value); - } else if (settingKey === 'include_trigger_words') { - await this.saveSetting('includeTriggerWords', value); - } else if (settingKey === 'enable_metadata_archive_db') { - await this.saveSetting('enable_metadata_archive_db', value); - } else if (settingKey === 'proxy_enabled') { - await this.saveSetting('proxy_enabled', value); - - // Toggle visibility of proxy settings group + await this.saveSetting(settingKey, value); + + if (settingKey === 'proxy_enabled') { const proxySettingsGroup = document.getElementById('proxySettingsGroup'); if (proxySettingsGroup) { proxySettingsGroup.style.display = value ? 'block' : 'none'; } - } else { - // For any other settings that might be added in the future - await this.saveSetting(settingKey, value); } - + // Refresh metadata archive status when enable setting changes if (settingKey === 'enable_metadata_archive_db') { await this.updateMetadataArchiveStatus(); @@ -941,16 +834,11 @@ export class SettingsManager { // 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' + showToast('toast.settings.compactModeToggled', { + state: value ? 'toast.settings.compactModeEnabled' : 'toast.settings.compactModeDisabled' }, 'success'); } - // Special handling for metadata archive settings - if (settingKey === 'enable_metadata_archive_db') { - await this.updateMetadataArchiveStatus(); - } - } catch (error) { showToast('toast.settings.settingSaveFailed', { message: error.message }, 'error'); } @@ -964,23 +852,8 @@ export class SettingsManager { try { // Update frontend state with mapped keys - if (settingKey === 'default_lora_root') { - await this.saveSetting('default_lora_root', value); - } else if (settingKey === 'default_checkpoint_root') { - await this.saveSetting('default_checkpoint_root', value); - } else if (settingKey === 'default_embedding_root') { - await this.saveSetting('default_embedding_root', value); - } else if (settingKey === 'display_density') { - await this.saveSetting('displayDensity', value); - } else if (settingKey === 'card_info_display') { - await this.saveSetting('cardInfoDisplay', value); - } else if (settingKey === 'proxy_type') { - await this.saveSetting('proxy_type', value); - } else { - // For any other settings that might be added in the future - await this.saveSetting(settingKey, value); - } - + await this.saveSetting(settingKey, value); + // Apply frontend settings immediately this.applyFrontendSettings(); @@ -1296,13 +1169,13 @@ export class SettingsManager { 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(); @@ -1347,7 +1220,7 @@ export class SettingsManager { applyFrontendSettings() { // Apply autoplay setting to existing videos in card previews - const autoplayOnHover = state.global.settings.autoplayOnHover; + const autoplayOnHover = state.global.settings.autoplay_on_hover; document.querySelectorAll('.card-preview video').forEach(video => { // Remove previous event listeners by cloning and replacing the element const videoParent = video.parentElement; @@ -1377,17 +1250,17 @@ export class SettingsManager { // Apply display density class to grid const grid = document.querySelector('.card-grid'); if (grid) { - const density = state.global.settings.displayDensity || 'default'; - + 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.cardInfoDisplay || 'always'; + const cardInfoDisplay = state.global.settings.card_info_display || 'always'; document.body.classList.toggle('hover-reveal', cardInfoDisplay === 'hover'); } } diff --git a/static/js/state/index.js b/static/js/state/index.js index 9e48e86f..6f0cedd1 100644 --- a/static/js/state/index.js +++ b/static/js/state/index.js @@ -1,20 +1,43 @@ // Create the new hierarchical state structure import { getStorageItem, getMapFromStorage } from '../utils/storageHelpers.js'; import { MODEL_TYPES } from '../api/apiConfig.js'; +import { DEFAULT_PATH_TEMPLATES } from '../utils/constants.js'; -// Load only frontend settings from localStorage with defaults -// Backend settings will be loaded by SettingsManager from the backend -const savedSettings = getStorageItem('settings', { - blurMatureContent: true, +const DEFAULT_SETTINGS_BASE = Object.freeze({ + civitai_api_key: '', + language: 'en', show_only_sfw: false, - cardInfoDisplay: 'always', - autoplayOnHover: false, - displayDensity: 'default', - optimizeExampleImages: true, - autoDownloadExampleImages: true, - includeTriggerWords: false + enable_metadata_archive_db: false, + proxy_enabled: false, + proxy_type: 'http', + proxy_host: '', + proxy_port: '', + proxy_username: '', + proxy_password: '', + default_lora_root: '', + default_checkpoint_root: '', + default_embedding_root: '', + base_model_path_mappings: {}, + download_path_templates: {}, + example_images_path: '', + optimize_example_images: true, + auto_download_example_images: false, + blur_mature_content: true, + autoplay_on_hover: false, + display_density: 'default', + card_info_display: 'always', + include_trigger_words: false, + compact_mode: false, }); +export function createDefaultSettings() { + return { + ...DEFAULT_SETTINGS_BASE, + base_model_path_mappings: {}, + download_path_templates: { ...DEFAULT_PATH_TEMPLATES }, + }; +} + // Load preview versions from localStorage for each model type const loraPreviewVersions = getMapFromStorage('loras_preview_versions'); const checkpointPreviewVersions = getMapFromStorage('checkpoints_preview_versions'); @@ -23,7 +46,7 @@ const embeddingPreviewVersions = getMapFromStorage('embeddings_preview_versions' export const state = { // Global state global: { - settings: savedSettings, + settings: createDefaultSettings(), loadingManager: null, observer: null, }, diff --git a/static/js/utils/VirtualScroller.js b/static/js/utils/VirtualScroller.js index 57f46c06..e25da266 100644 --- a/static/js/utils/VirtualScroller.js +++ b/static/js/utils/VirtualScroller.js @@ -102,7 +102,7 @@ export class VirtualScroller { const availableContentWidth = containerWidth - paddingLeft - paddingRight; // Get display density setting - const displayDensity = state.global.settings?.displayDensity || 'default'; + const displayDensity = state.global.settings?.display_density || 'default'; // Set exact column counts and grid widths to match CSS container widths let maxColumns, maxGridWidth; diff --git a/static/js/utils/uiHelpers.js b/static/js/utils/uiHelpers.js index 3aed6942..3e02d086 100644 --- a/static/js/utils/uiHelpers.js +++ b/static/js/utils/uiHelpers.js @@ -339,7 +339,7 @@ export function copyLoraSyntax(card) { const baseSyntax = buildLoraSyntax(card.dataset.file_name, usageTips); // Check if trigger words should be included - const includeTriggerWords = state.global.settings.includeTriggerWords; + const includeTriggerWords = state.global.settings.include_trigger_words; if (!includeTriggerWords) { const message = translate('uiHelpers.lora.syntaxCopied', {}, 'LoRA syntax copied to clipboard');