From 9366d3d2d0c4f543fe66c56097e9f9fbdfa3630e Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Sun, 14 Sep 2025 22:57:17 +0800 Subject: [PATCH] feat: add API endpoint for fetching application settings and update frontend settings management --- py/routes/misc_routes.py | 42 +++ py/services/settings_manager.py | 1 - settings.json.example | 1 - static/js/core.js | 5 + static/js/managers/SettingsManager.js | 498 ++++++++++++-------------- static/js/state/index.js | 10 +- 6 files changed, 291 insertions(+), 266 deletions(-) diff --git a/py/routes/misc_routes.py b/py/routes/misc_routes.py index 013923b7..494eca9b 100644 --- a/py/routes/misc_routes.py +++ b/py/routes/misc_routes.py @@ -88,6 +88,7 @@ class MiscRoutes: @staticmethod def setup_routes(app): """Register miscellaneous routes""" + app.router.add_get('/api/settings', MiscRoutes.get_settings) app.router.add_post('/api/settings', MiscRoutes.update_settings) app.router.add_get('/api/health-check', lambda request: web.json_response({'status': 'ok'})) @@ -119,6 +120,47 @@ class MiscRoutes: app.router.add_post('/api/remove-metadata-archive', MiscRoutes.remove_metadata_archive) app.router.add_get('/api/metadata-archive-status', MiscRoutes.get_metadata_archive_status) + @staticmethod + async def get_settings(request): + """Get application settings that should be synced to frontend""" + try: + # Define keys that should be synced from backend to frontend + sync_keys = [ + 'civitai_api_key', + 'default_lora_root', + 'default_checkpoint_root', + 'default_embedding_root', + 'base_model_path_mappings', + 'download_path_templates', + 'enable_metadata_archive_db', + 'language', + 'proxy_enabled', + 'proxy_type', + 'proxy_host', + 'proxy_port', + 'proxy_username', + 'proxy_password' + ] + + # Build response with only the keys that should be synced + response_data = {} + for key in sync_keys: + value = settings.get(key) + if value is not None: + response_data[key] = value + + return web.json_response({ + 'success': True, + 'settings': response_data + }) + + except Exception as e: + logger.error(f"Error getting settings: {e}", exc_info=True) + return web.json_response({ + 'success': False, + 'error': str(e) + }, status=500) + @staticmethod async def update_settings(request): """Update application settings""" diff --git a/py/services/settings_manager.py b/py/services/settings_manager.py index 5a95e58e..ec71104e 100644 --- a/py/services/settings_manager.py +++ b/py/services/settings_manager.py @@ -80,7 +80,6 @@ class SettingsManager: """Return default settings""" return { "civitai_api_key": "", - "show_only_sfw": False, "language": "en", "enable_metadata_archive_db": False, # Enable metadata archive database "proxy_enabled": False, # Enable app-level proxy diff --git a/settings.json.example b/settings.json.example index 0765577b..673aa76d 100644 --- a/settings.json.example +++ b/settings.json.example @@ -1,6 +1,5 @@ { "civitai_api_key": "your_civitai_api_key_here", - "show_only_sfw": false, "folder_paths": { "loras": [ "C:/path/to/your/loras_folder", diff --git a/static/js/core.js b/static/js/core.js index d7daa0c5..779f3094 100644 --- a/static/js/core.js +++ b/static/js/core.js @@ -38,6 +38,11 @@ export class AppCore { console.log(`AppCore: Language set: ${i18n.getCurrentLocale()}`); + // Initialize settings manager and wait for it to sync from backend + console.log('AppCore: Initializing settings...'); + await settingsManager.waitForInitialization(); + console.log('AppCore: Settings initialized'); + // Initialize managers state.loadingManager = new LoadingManager(); modalManager.initialize(); diff --git a/static/js/managers/SettingsManager.js b/static/js/managers/SettingsManager.js index 96e3064d..0767baab 100644 --- a/static/js/managers/SettingsManager.js +++ b/static/js/managers/SettingsManager.js @@ -10,122 +10,194 @@ export class SettingsManager { constructor() { this.initialized = false; this.isOpen = false; + this.initializationPromise = null; // Add initialization to sync with modal state this.currentPage = document.body.dataset.page || 'loras'; - // Ensure settings are loaded from localStorage - this.loadSettingsFromStorage(); - - // Sync settings to backend if needed - this.syncSettingsToBackendIfNeeded(); + // Start initialization but don't await here to avoid blocking constructor + this.initializationPromise = this.initializeSettings(); this.initialize(); } - loadSettingsFromStorage() { + // Add method to wait for initialization to complete + async waitForInitialization() { + if (this.initializationPromise) { + await this.initializationPromise; + } + } + + async initializeSettings() { + // Load frontend-only settings from localStorage + this.loadFrontendSettingsFromStorage(); + + // Sync settings from backend to frontend + await this.syncSettingsFromBackend(); + } + + loadFrontendSettingsFromStorage() { // Get saved settings from localStorage const savedSettings = getStorageItem('settings'); - // Migrate legacy default_loras_root to default_lora_root if present - if (savedSettings && savedSettings.default_loras_root && !savedSettings.default_lora_root) { - savedSettings.default_lora_root = savedSettings.default_loras_root; - delete savedSettings.default_loras_root; - setStorageItem('settings', savedSettings); - } + // Frontend-only settings that should be stored in localStorage + const frontendOnlyKeys = [ + 'blurMatureContent', + 'show_only_sfw', + 'autoplayOnHover', + 'displayDensity', + 'cardInfoDisplay', + 'optimizeExampleImages', + 'autoDownloadExampleImages', + 'includeTriggerWords' + ]; - // Apply saved settings to state if available + // Apply saved frontend settings to state if available if (savedSettings) { - state.global.settings = { ...state.global.settings, ...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 new settings if they don't exist - if (state.global.settings.compactMode === undefined) { - state.global.settings.compactMode = false; + // 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; } - // Set default for optimizeExampleImages if undefined if (state.global.settings.optimizeExampleImages === undefined) { state.global.settings.optimizeExampleImages = true; } - // Set default for autoDownloadExampleImages if undefined if (state.global.settings.autoDownloadExampleImages === undefined) { state.global.settings.autoDownloadExampleImages = true; } - // Set default for cardInfoDisplay if undefined if (state.global.settings.cardInfoDisplay === undefined) { state.global.settings.cardInfoDisplay = 'always'; } - // Set default for defaultCheckpointRoot if undefined - if (state.global.settings.default_checkpoint_root === undefined) { - state.global.settings.default_checkpoint_root = ''; - } - - // Convert old boolean compactMode to new displayDensity string - if (typeof state.global.settings.displayDensity === 'undefined') { + 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'; } - // We can delete the old setting, but keeping it for backwards compatibility } - // Migrate legacy download_path_template to new structure - if (state.global.settings.download_path_template && !state.global.settings.download_path_templates) { - const legacyTemplate = state.global.settings.download_path_template; - state.global.settings.download_path_templates = { - lora: legacyTemplate, - checkpoint: legacyTemplate, - embedding: legacyTemplate - }; - delete state.global.settings.download_path_template; - setStorageItem('settings', state.global.settings); - } - - // Set default for download path templates if undefined - if (state.global.settings.download_path_templates === undefined) { - state.global.settings.download_path_templates = { ...DEFAULT_PATH_TEMPLATES }; - } - - // Ensure all model types have templates - Object.keys(DEFAULT_PATH_TEMPLATES).forEach(modelType => { - if (typeof state.global.settings.download_path_templates[modelType] === 'undefined') { - state.global.settings.download_path_templates[modelType] = DEFAULT_PATH_TEMPLATES[modelType]; - } - }); - - // Set default for base model path mappings if undefined - if (state.global.settings.base_model_path_mappings === undefined) { - state.global.settings.base_model_path_mappings = {}; - } - - // Set default for defaultEmbeddingRoot if undefined - if (state.global.settings.default_embedding_root === undefined) { - state.global.settings.default_embedding_root = ''; - } - - // Set default for includeTriggerWords if undefined if (state.global.settings.includeTriggerWords === undefined) { state.global.settings.includeTriggerWords = false; } + + // Save updated frontend settings to localStorage + this.saveFrontendSettingsToStorage(); } - async syncSettingsToBackendIfNeeded() { - // Get local settings from storage - const localSettings = getStorageItem('settings') || {}; + async syncSettingsFromBackend() { + try { + const response = await fetch('/api/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(); + + console.log('Settings synced from backend'); + } else { + console.error('Failed to sync settings from backend:', data.error); + } + } catch (error) { + console.error('Failed to sync settings from backend:', error); + // Set defaults if backend sync fails + this.setBackendSettingDefaults(); + } + } - // Fields that need to be synced to backend - const fieldsToSync = [ + 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', + proxy_enabled: false, + proxy_type: 'http', + proxy_host: '', + proxy_port: '', + proxy_username: '', + proxy_password: '' + }; + + Object.keys(backendDefaults).forEach(key => { + if (state.global.settings[key] === undefined || state.global.settings[key] === null) { + state.global.settings[key] = backendDefaults[key]; + } + }); + + // 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]; + } + }); + } + + saveFrontendSettingsToStorage() { + // Save only frontend-specific settings to localStorage + const frontendOnlyKeys = [ + 'blurMatureContent', + 'show_only_sfw', + 'autoplayOnHover', + 'displayDensity', + 'cardInfoDisplay', + 'optimizeExampleImages', + 'autoDownloadExampleImages', + 'includeTriggerWords' + ]; + + const frontendSettings = {}; + frontendOnlyKeys.forEach(key => { + if (state.global.settings[key] !== undefined) { + frontendSettings[key] = state.global.settings[key]; + } + }); + + setStorageItem('settings', frontendSettings); + } + + // 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_checkpoint_root', 'default_embedding_root', 'base_model_path_mappings', 'download_path_templates', + 'enable_metadata_archive_db', + 'language', 'proxy_enabled', 'proxy_type', 'proxy_host', @@ -133,30 +205,38 @@ export class SettingsManager { 'proxy_username', 'proxy_password' ]; + return backendKeys.includes(settingKey); + } - // Build payload for syncing - const payload = {}; + // Helper method to save setting based on whether it's frontend or backend + async saveSetting(settingKey, value) { + // Update state + state.global.settings[settingKey] = value; - fieldsToSync.forEach(key => { - if (localSettings[key] !== undefined) { - payload[key] = localSettings[key]; - } - }); - - // Only send request if there is something to sync - if (Object.keys(payload).length > 0) { + if (this.isBackendSetting(settingKey)) { + // Save to backend try { - await fetch('/api/settings', { + const payload = {}; + payload[settingKey] = value; + + const response = await fetch('/api/settings', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { + 'Content-Type': 'application/json', + }, body: JSON.stringify(payload) }); - // Log success to console - console.log('Settings synced to backend'); - } catch (e) { - // Log error to console - console.error('Failed to sync settings to backend:', e); + + if (!response.ok) { + throw new Error('Failed to save setting to backend'); + } + } catch (error) { + console.error(`Failed to save backend setting ${settingKey}:`, error); + throw error; } + } else { + // Save frontend settings to localStorage + this.saveFrontendSettingsToStorage(); } } @@ -603,23 +683,8 @@ export class SettingsManager { async saveBaseModelMappings() { try { - // Save to localStorage - setStorageItem('settings', state.global.settings); - - // Save to backend - const response = await fetch('/api/settings', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - base_model_path_mappings: state.global.settings.base_model_path_mappings - }) - }); - - if (!response.ok) { - throw new Error('Failed to save base model mappings'); - } + // 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; @@ -793,23 +858,8 @@ export class SettingsManager { async saveDownloadPathTemplates() { try { - // Save to localStorage - setStorageItem('settings', state.global.settings); - - // Save to backend - const response = await fetch('/api/settings', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - download_path_templates: state.global.settings.download_path_templates - }) - }); - - if (!response.ok) { - throw new Error('Failed to save download path templates'); - } + // Save to backend using universal save method + await this.saveSetting('download_path_templates', state.global.settings.download_path_templates); showToast('toast.settings.downloadTemplatesUpdated', {}, 'success'); @@ -834,61 +884,40 @@ export class SettingsManager { const value = element.checked; - // Update frontend state - if (settingKey === 'blur_mature_content') { - state.global.settings.blurMatureContent = value; - } else if (settingKey === 'show_only_sfw') { - state.global.settings.show_only_sfw = value; - } else if (settingKey === 'autoplay_on_hover') { - state.global.settings.autoplayOnHover = value; - } else if (settingKey === 'optimize_example_images') { - state.global.settings.optimizeExampleImages = value; - } else if (settingKey === 'auto_download_example_images') { - state.global.settings.autoDownloadExampleImages = value; - } else if (settingKey === 'compact_mode') { - state.global.settings.compactMode = value; - } else if (settingKey === 'include_trigger_words') { - state.global.settings.includeTriggerWords = value; - } else if (settingKey === 'enable_metadata_archive_db') { - state.global.settings.enable_metadata_archive_db = value; - } else if (settingKey === 'proxy_enabled') { - state.global.settings.proxy_enabled = value; - - // Toggle visibility of proxy settings group - 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 - state.global.settings[settingKey] = value; - } - - // Save to localStorage - setStorageItem('settings', state.global.settings); - try { - // For backend settings, make API call - if (['show_only_sfw', 'enable_metadata_archive_db', 'proxy_enabled'].includes(settingKey)) { - const payload = {}; - payload[settingKey] = value; + // 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); - const response = await fetch('/api/settings', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(payload) - }); - - if (!response.ok) { - throw new Error('Failed to save setting'); - } - - // Refresh metadata archive status when enable setting changes - if (settingKey === 'enable_metadata_archive_db') { - await this.updateMetadataArchiveStatus(); + // Toggle visibility of proxy settings group + 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(); } showToast('toast.settings.settingsUpdated', { setting: settingKey.replace(/_/g, ' ') }, 'success'); @@ -933,55 +962,31 @@ export class SettingsManager { const value = element.value; - // Update frontend state - if (settingKey === 'default_lora_root') { - state.global.settings.default_lora_root = value; - } else if (settingKey === 'default_checkpoint_root') { - state.global.settings.default_checkpoint_root = value; - } else if (settingKey === 'default_embedding_root') { - state.global.settings.default_embedding_root = value; - } else if (settingKey === 'display_density') { - state.global.settings.displayDensity = value; - - // Also update compactMode for backwards compatibility - state.global.settings.compactMode = (value !== 'default'); - } else if (settingKey === 'card_info_display') { - state.global.settings.cardInfoDisplay = value; - } else if (settingKey === 'proxy_type') { - state.global.settings.proxy_type = value; - } else { - // For any other settings that might be added in the future - state.global.settings[settingKey] = value; - } - - // Save to localStorage - setStorageItem('settings', state.global.settings); - try { - // For backend settings, make API call - if (settingKey === 'default_lora_root' || settingKey === 'default_checkpoint_root' || settingKey === 'default_embedding_root' || settingKey === 'download_path_templates' || settingKey.startsWith('proxy_')) { - const payload = {}; - if (settingKey === 'download_path_templates') { - payload[settingKey] = state.global.settings.download_path_templates; - } else { - payload[settingKey] = value; - } + // 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); - const response = await fetch('/api/settings', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(payload) - }); - - if (!response.ok) { - throw new Error('Failed to save setting'); - } - - showToast('toast.settings.settingsUpdated', { setting: settingKey.replace(/_/g, ' ') }, 'success'); + // Also update compactMode for backwards compatibility + state.global.settings.compactMode = (value !== 'default'); + this.saveFrontendSettingsToStorage(); + } 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); } + showToast('toast.settings.settingsUpdated', { setting: settingKey.replace(/_/g, ' ') }, 'success'); + // Apply frontend settings immediately this.applyFrontendSettings(); @@ -1167,9 +1172,8 @@ export class SettingsManager { showToast('settings.metadataArchive.downloadSuccess', 'success'); - // Update settings in state - state.global.settings.enable_metadata_archive_db = true; - setStorageItem('settings', state.global.settings); + // Update settings using universal save method + await this.saveSetting('enable_metadata_archive_db', true); // Update UI const enableCheckbox = document.getElementById('enableMetadataArchive'); @@ -1223,9 +1227,8 @@ export class SettingsManager { if (data.success) { showToast('settings.metadataArchive.removeSuccess', 'success'); - // Update settings in state - state.global.settings.enable_metadata_archive_db = false; - setStorageItem('settings', state.global.settings); + // Update settings using universal save method + await this.saveSetting('enable_metadata_archive_db', false); // Update UI const enableCheckbox = document.getElementById('enableMetadataArchive'); @@ -1255,7 +1258,6 @@ export class SettingsManager { const value = element.value.trim(); // Trim whitespace - // For API key or other inputs that need to be saved on backend try { // Check if value has changed from existing value const currentValue = state.global.settings[settingKey] || ''; @@ -1263,27 +1265,14 @@ export class SettingsManager { return; // No change, exit early } - // For username and password, remove the setting if value is empty + // 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]; - } else { - // Update state with value (including empty strings for non-optional fields) - state.global.settings[settingKey] = value; - } - - setStorageItem('settings', state.global.settings); - - // For backend settings, make API call - if (settingKey === 'civitai_api_key' || settingKey.startsWith('proxy_')) { - const payload = {}; - // For username and password, send delete flag if empty to remove from backend - if ((settingKey === 'proxy_username' || settingKey === 'proxy_password') && value === '') { - payload[settingKey] = '__DELETE__'; - } else { - payload[settingKey] = value; - } + // Send delete flag to backend + const payload = {}; + payload[settingKey] = '__DELETE__'; const response = await fetch('/api/settings', { method: 'POST', @@ -1294,8 +1283,11 @@ export class SettingsManager { }); if (!response.ok) { - throw new Error('Failed to save setting'); + 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'); @@ -1312,26 +1304,8 @@ export class SettingsManager { const selectedLanguage = element.value; try { - // Update local state - state.global.settings.language = selectedLanguage; - - // Save to localStorage - setStorageItem('settings', state.global.settings); - - // Save to backend - const response = await fetch('/api/settings', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - language: selectedLanguage - }) - }); - - if (!response.ok) { - throw new Error('Failed to save language setting to backend'); - } + // 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(); diff --git a/static/js/state/index.js b/static/js/state/index.js index 71f6b859..9e48e86f 100644 --- a/static/js/state/index.js +++ b/static/js/state/index.js @@ -2,11 +2,17 @@ import { getStorageItem, getMapFromStorage } from '../utils/storageHelpers.js'; import { MODEL_TYPES } from '../api/apiConfig.js'; -// Load settings from localStorage or use defaults +// Load only frontend settings from localStorage with defaults +// Backend settings will be loaded by SettingsManager from the backend const savedSettings = getStorageItem('settings', { blurMatureContent: true, show_only_sfw: false, - cardInfoDisplay: 'always' + cardInfoDisplay: 'always', + autoplayOnHover: false, + displayDensity: 'default', + optimizeExampleImages: true, + autoDownloadExampleImages: true, + includeTriggerWords: false }); // Load preview versions from localStorage for each model type