feat: add open settings location endpoint

- Add `open_settings_location` method to `FileSystemHandler` to open OS file explorer at settings file location
- Register new POST route `/api/lm/settings/open-location` for settings file access
- Inject `SettingsManager` dependency into `FileSystemHandler` constructor
- Add cross-platform support for Windows, macOS, and Linux file explorers
- Include error handling for missing settings files and system exceptions
This commit is contained in:
Will Miao
2025-12-31 16:09:23 +08:00
parent 8120716cd8
commit 102defe29c
6 changed files with 140 additions and 129 deletions

View File

@@ -853,6 +853,9 @@ class MetadataArchiveHandler:
class FileSystemHandler: class FileSystemHandler:
def __init__(self, settings_service=None) -> None:
self._settings = settings_service or get_settings_manager()
async def open_file_location(self, request: web.Request) -> web.Response: async def open_file_location(self, request: web.Request) -> web.Response:
try: try:
data = await request.json() data = await request.json()
@@ -877,6 +880,30 @@ class FileSystemHandler:
logger.error("Failed to open file location: %s", exc, exc_info=True) logger.error("Failed to open file location: %s", exc, exc_info=True)
return web.json_response({"success": False, "error": str(exc)}, status=500) return web.json_response({"success": False, "error": str(exc)}, status=500)
async def open_settings_location(self, request: web.Request) -> web.Response:
try:
settings_file = getattr(self._settings, "settings_file", None)
if not settings_file:
return web.json_response({"success": False, "error": "Settings file not found"}, status=404)
settings_file = os.path.abspath(settings_file)
if not os.path.isfile(settings_file):
return web.json_response({"success": False, "error": "Settings file does not exist"}, status=404)
if os.name == "nt":
subprocess.Popen(["explorer", "/select,", settings_file])
elif os.name == "posix":
if sys.platform == "darwin":
subprocess.Popen(["open", "-R", settings_file])
else:
folder = os.path.dirname(settings_file)
subprocess.Popen(["xdg-open", folder])
return web.json_response({"success": True, "message": f"Opened settings folder: {settings_file}"})
except Exception as exc: # pragma: no cover - defensive logging
logger.error("Failed to open settings location: %s", exc, exc_info=True)
return web.json_response({"success": False, "error": str(exc)}, status=500)
class NodeRegistryHandler: class NodeRegistryHandler:
def __init__( def __init__(
@@ -1103,6 +1130,7 @@ class MiscHandlerSet:
"get_metadata_archive_status": self.metadata_archive.get_metadata_archive_status, "get_metadata_archive_status": self.metadata_archive.get_metadata_archive_status,
"get_model_versions_status": self.model_library.get_model_versions_status, "get_model_versions_status": self.model_library.get_model_versions_status,
"open_file_location": self.filesystem.open_file_location, "open_file_location": self.filesystem.open_file_location,
"open_settings_location": self.filesystem.open_settings_location,
} }

View File

@@ -41,6 +41,7 @@ MISC_ROUTE_DEFINITIONS: tuple[RouteDefinition, ...] = (
RouteDefinition("POST", "/api/lm/remove-metadata-archive", "remove_metadata_archive"), RouteDefinition("POST", "/api/lm/remove-metadata-archive", "remove_metadata_archive"),
RouteDefinition("GET", "/api/lm/metadata-archive-status", "get_metadata_archive_status"), RouteDefinition("GET", "/api/lm/metadata-archive-status", "get_metadata_archive_status"),
RouteDefinition("GET", "/api/lm/model-versions-status", "get_model_versions_status"), RouteDefinition("GET", "/api/lm/model-versions-status", "get_model_versions_status"),
RouteDefinition("POST", "/api/lm/settings/open-location", "open_settings_location"),
) )

View File

@@ -107,7 +107,7 @@ class MiscRoutes:
settings_service=self._settings, settings_service=self._settings,
metadata_provider_updater=self._metadata_provider_updater, metadata_provider_updater=self._metadata_provider_updater,
) )
filesystem = FileSystemHandler() filesystem = FileSystemHandler(settings_service=self._settings)
node_registry_handler = NodeRegistryHandler( node_registry_handler = NodeRegistryHandler(
node_registry=self._node_registry, node_registry=self._node_registry,
prompt_server=self._prompt_server, prompt_server=self._prompt_server,

View File

@@ -883,6 +883,7 @@ class SettingsManager:
if os.path.abspath(previous_path) != os.path.abspath(target_path): if os.path.abspath(previous_path) != os.path.abspath(target_path):
self._copy_model_cache_directory(previous_dir, target_dir) self._copy_model_cache_directory(previous_dir, target_dir)
logger.info("Switching settings file to: %s", target_path)
self._pending_portable_switch = {"other_path": other_path} self._pending_portable_switch = {"other_path": other_path}
self.settings_file = target_path self.settings_file = target_path
@@ -929,7 +930,12 @@ class SettingsManager:
and os.path.abspath(source_cache_dir) != os.path.abspath(target_cache_dir) and os.path.abspath(source_cache_dir) != os.path.abspath(target_cache_dir)
): ):
try: try:
shutil.copytree(source_cache_dir, target_cache_dir, dirs_exist_ok=True) shutil.copytree(
source_cache_dir,
target_cache_dir,
dirs_exist_ok=True,
ignore=shutil.ignore_patterns("*.sqlite-shm", "*.sqlite-wal"),
)
except Exception as exc: except Exception as exc:
logger.warning( logger.warning(
"Failed to copy model_cache directory from %s to %s: %s", "Failed to copy model_cache directory from %s to %s: %s",

View File

@@ -17,9 +17,8 @@ export class SettingsManager {
this.initializationPromise = null; this.initializationPromise = null;
this.availableLibraries = {}; this.availableLibraries = {};
this.activeLibrary = ''; this.activeLibrary = '';
this.settingsFilePath = null;
this.registeredStartupBannerIds = new Set(); this.registeredStartupBannerIds = new Set();
// Add initialization to sync with modal state // Add initialization to sync with modal state
this.currentPage = document.body.dataset.page || 'loras'; this.currentPage = document.body.dataset.page || 'loras';
@@ -56,7 +55,6 @@ export class SettingsManager {
const data = await response.json(); const data = await response.json();
if (data.success && data.settings) { if (data.success && data.settings) {
state.global.settings = this.mergeSettingsWithDefaults(data.settings); state.global.settings = this.mergeSettingsWithDefaults(data.settings);
this.settingsFilePath = data.settings.settings_file || this.settingsFilePath;
this.registerStartupMessages(data.messages); this.registerStartupMessages(data.messages);
console.log('Settings synced from backend'); console.log('Settings synced from backend');
} else { } else {
@@ -177,10 +175,6 @@ export class SettingsManager {
return; return;
} }
if (!this.settingsFilePath && typeof message.settings_file === 'string') {
this.settingsFilePath = message.settings_file;
}
const bannerId = `startup-${message.code || index}`; const bannerId = `startup-${message.code || index}`;
if (this.registeredStartupBannerIds.has(bannerId)) { if (this.registeredStartupBannerIds.has(bannerId)) {
return; return;
@@ -289,7 +283,7 @@ export class SettingsManager {
initialize() { initialize() {
if (this.initialized) return; if (this.initialized) return;
// Add event listener to sync state when modal is closed via other means (like Escape key) // Add event listener to sync state when modal is closed via other means (like Escape key)
const settingsModal = document.getElementById('settingsModal'); const settingsModal = document.getElementById('settingsModal');
if (settingsModal) { if (settingsModal) {
@@ -297,7 +291,7 @@ export class SettingsManager {
mutations.forEach((mutation) => { mutations.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.attributeName === 'style') { if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
this.isOpen = settingsModal.style.display === 'block'; this.isOpen = settingsModal.style.display === 'block';
// When modal is opened, update checkbox state from current settings // When modal is opened, update checkbox state from current settings
if (this.isOpen) { if (this.isOpen) {
this.loadSettingsToUI(); this.loadSettingsToUI();
@@ -305,10 +299,10 @@ export class SettingsManager {
} }
}); });
}); });
observer.observe(settingsModal, { attributes: true }); observer.observe(settingsModal, { attributes: true });
} }
// Add event listeners for all toggle-visibility buttons // Add event listeners for all toggle-visibility buttons
document.querySelectorAll('.toggle-visibility').forEach(button => { document.querySelectorAll('.toggle-visibility').forEach(button => {
button.addEventListener('click', () => this.toggleInputVisibility(button)); button.addEventListener('click', () => this.toggleInputVisibility(button));
@@ -316,12 +310,8 @@ export class SettingsManager {
const openSettingsLocationButton = document.querySelector('.settings-open-location-trigger'); const openSettingsLocationButton = document.querySelector('.settings-open-location-trigger');
if (openSettingsLocationButton) { if (openSettingsLocationButton) {
if (openSettingsLocationButton.dataset.settingsPath) {
this.settingsFilePath = openSettingsLocationButton.dataset.settingsPath;
}
openSettingsLocationButton.addEventListener('click', () => { openSettingsLocationButton.addEventListener('click', () => {
const filePath = openSettingsLocationButton.dataset.settingsPath; this.openSettingsFileLocation();
this.openSettingsFileLocation(filePath);
}); });
} }
@@ -364,29 +354,16 @@ export class SettingsManager {
this.initialized = true; this.initialized = true;
} }
async openSettingsFileLocation(filePath) { async openSettingsFileLocation() {
const targetPath = filePath || this.settingsFilePath || document.querySelector('.settings-open-location-trigger')?.dataset.settingsPath;
if (!targetPath) {
showToast('settings.openSettingsFileLocation.failed', {}, 'error');
return;
}
try { try {
const response = await fetch('/api/lm/open-file-location', { const response = await fetch('/api/lm/settings/open-location', {
method: 'POST', method: 'POST'
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ file_path: targetPath }),
}); });
if (!response.ok) { if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`); throw new Error(`Request failed with status ${response.status}`);
} }
this.settingsFilePath = targetPath;
showToast('settings.openSettingsFileLocation.success', {}, 'success'); showToast('settings.openSettingsFileLocation.success', {}, 'success');
} catch (error) { } catch (error) {
console.error('Failed to open settings file location:', error); console.error('Failed to open settings file location:', error);
@@ -497,7 +474,7 @@ export class SettingsManager {
// Load default lora root // Load default lora root
await this.loadLoraRoots(); await this.loadLoraRoots();
// Load default checkpoint root // Load default checkpoint root
await this.loadCheckpointRoots(); await this.loadCheckpointRoots();
@@ -658,7 +635,7 @@ export class SettingsManager {
const proxyEnabledCheckbox = document.getElementById('proxyEnabled'); const proxyEnabledCheckbox = document.getElementById('proxyEnabled');
if (proxyEnabledCheckbox) { if (proxyEnabledCheckbox) {
proxyEnabledCheckbox.checked = state.global.settings.proxy_enabled || false; proxyEnabledCheckbox.checked = state.global.settings.proxy_enabled || false;
// Add event listener for toggling proxy settings group visibility // Add event listener for toggling proxy settings group visibility
proxyEnabledCheckbox.addEventListener('change', () => { proxyEnabledCheckbox.addEventListener('change', () => {
const proxySettingsGroup = document.getElementById('proxySettingsGroup'); const proxySettingsGroup = document.getElementById('proxySettingsGroup');
@@ -666,7 +643,7 @@ export class SettingsManager {
proxySettingsGroup.style.display = proxyEnabledCheckbox.checked ? 'block' : 'none'; proxySettingsGroup.style.display = proxyEnabledCheckbox.checked ? 'block' : 'none';
} }
}); });
// Set initial visibility // Set initial visibility
const proxySettingsGroup = document.getElementById('proxySettingsGroup'); const proxySettingsGroup = document.getElementById('proxySettingsGroup');
if (proxySettingsGroup) { if (proxySettingsGroup) {
@@ -854,23 +831,23 @@ export class SettingsManager {
try { try {
const defaultLoraRootSelect = document.getElementById('defaultLoraRoot'); const defaultLoraRootSelect = document.getElementById('defaultLoraRoot');
if (!defaultLoraRootSelect) return; if (!defaultLoraRootSelect) return;
// Fetch lora roots // Fetch lora roots
const response = await fetch('/api/lm/loras/roots'); const response = await fetch('/api/lm/loras/roots');
if (!response.ok) { if (!response.ok) {
throw new Error('Failed to fetch LoRA roots'); throw new Error('Failed to fetch LoRA roots');
} }
const data = await response.json(); const data = await response.json();
if (!data.roots || data.roots.length === 0) { if (!data.roots || data.roots.length === 0) {
throw new Error('No LoRA roots found'); throw new Error('No LoRA roots found');
} }
// Clear existing options except the first one (No Default) // Clear existing options except the first one (No Default)
const noDefaultOption = defaultLoraRootSelect.querySelector('option[value=""]'); const noDefaultOption = defaultLoraRootSelect.querySelector('option[value=""]');
defaultLoraRootSelect.innerHTML = ''; defaultLoraRootSelect.innerHTML = '';
defaultLoraRootSelect.appendChild(noDefaultOption); defaultLoraRootSelect.appendChild(noDefaultOption);
// Add options for each root // Add options for each root
data.roots.forEach(root => { data.roots.forEach(root => {
const option = document.createElement('option'); const option = document.createElement('option');
@@ -878,11 +855,11 @@ export class SettingsManager {
option.textContent = root; option.textContent = root;
defaultLoraRootSelect.appendChild(option); defaultLoraRootSelect.appendChild(option);
}); });
// Set selected value from settings // Set selected value from settings
const defaultRoot = state.global.settings.default_lora_root || ''; const defaultRoot = state.global.settings.default_lora_root || '';
defaultLoraRootSelect.value = defaultRoot; defaultLoraRootSelect.value = defaultRoot;
} catch (error) { } catch (error) {
console.error('Error loading LoRA roots:', error); console.error('Error loading LoRA roots:', error);
showToast('toast.settings.loraRootsFailed', { message: error.message }, 'error'); showToast('toast.settings.loraRootsFailed', { message: error.message }, 'error');
@@ -893,23 +870,23 @@ export class SettingsManager {
try { try {
const defaultCheckpointRootSelect = document.getElementById('defaultCheckpointRoot'); const defaultCheckpointRootSelect = document.getElementById('defaultCheckpointRoot');
if (!defaultCheckpointRootSelect) return; if (!defaultCheckpointRootSelect) return;
// Fetch checkpoint roots // Fetch checkpoint roots
const response = await fetch('/api/lm/checkpoints/roots'); const response = await fetch('/api/lm/checkpoints/roots');
if (!response.ok) { if (!response.ok) {
throw new Error('Failed to fetch checkpoint roots'); throw new Error('Failed to fetch checkpoint roots');
} }
const data = await response.json(); const data = await response.json();
if (!data.roots || data.roots.length === 0) { if (!data.roots || data.roots.length === 0) {
throw new Error('No checkpoint roots found'); throw new Error('No checkpoint roots found');
} }
// Clear existing options except the first one (No Default) // Clear existing options except the first one (No Default)
const noDefaultOption = defaultCheckpointRootSelect.querySelector('option[value=""]'); const noDefaultOption = defaultCheckpointRootSelect.querySelector('option[value=""]');
defaultCheckpointRootSelect.innerHTML = ''; defaultCheckpointRootSelect.innerHTML = '';
defaultCheckpointRootSelect.appendChild(noDefaultOption); defaultCheckpointRootSelect.appendChild(noDefaultOption);
// Add options for each root // Add options for each root
data.roots.forEach(root => { data.roots.forEach(root => {
const option = document.createElement('option'); const option = document.createElement('option');
@@ -917,11 +894,11 @@ export class SettingsManager {
option.textContent = root; option.textContent = root;
defaultCheckpointRootSelect.appendChild(option); defaultCheckpointRootSelect.appendChild(option);
}); });
// Set selected value from settings // Set selected value from settings
const defaultRoot = state.global.settings.default_checkpoint_root || ''; const defaultRoot = state.global.settings.default_checkpoint_root || '';
defaultCheckpointRootSelect.value = defaultRoot; defaultCheckpointRootSelect.value = defaultRoot;
} catch (error) { } catch (error) {
console.error('Error loading checkpoint roots:', error); console.error('Error loading checkpoint roots:', error);
showToast('toast.settings.checkpointRootsFailed', { message: error.message }, 'error'); showToast('toast.settings.checkpointRootsFailed', { message: error.message }, 'error');
@@ -972,7 +949,7 @@ export class SettingsManager {
if (!mappingsContainer) return; if (!mappingsContainer) return;
const mappings = state.global.settings.base_model_path_mappings || {}; const mappings = state.global.settings.base_model_path_mappings || {};
// Clear existing mappings // Clear existing mappings
mappingsContainer.innerHTML = ''; mappingsContainer.innerHTML = '';
@@ -993,7 +970,7 @@ export class SettingsManager {
const row = document.createElement('div'); const row = document.createElement('div');
row.className = 'mapping-row'; row.className = 'mapping-row';
const availableModels = MAPPABLE_BASE_MODELS.filter(model => { const availableModels = MAPPABLE_BASE_MODELS.filter(model => {
const existingMappings = state.global.settings.base_model_path_mappings || {}; const existingMappings = state.global.settings.base_model_path_mappings || {};
return !existingMappings.hasOwnProperty(model) || model === baseModel; return !existingMappings.hasOwnProperty(model) || model === baseModel;
@@ -1003,9 +980,9 @@ export class SettingsManager {
<div class="mapping-controls"> <div class="mapping-controls">
<select class="base-model-select"> <select class="base-model-select">
<option value="">${translate('settings.downloadPathTemplates.selectBaseModel', {}, 'Select Base Model')}</option> <option value="">${translate('settings.downloadPathTemplates.selectBaseModel', {}, 'Select Base Model')}</option>
${availableModels.map(model => ${availableModels.map(model =>
`<option value="${model}" ${model === baseModel ? 'selected' : ''}>${model}</option>` `<option value="${model}" ${model === baseModel ? 'selected' : ''}>${model}</option>`
).join('')} ).join('')}
</select> </select>
<input type="text" class="path-value-input" placeholder="${translate('settings.downloadPathTemplates.customPathPlaceholder', {}, 'Custom path (e.g., flux)')}" value="${pathValue}"> <input type="text" class="path-value-input" placeholder="${translate('settings.downloadPathTemplates.customPathPlaceholder', {}, 'Custom path (e.g., flux)')}" value="${pathValue}">
<button type="button" class="remove-mapping-btn" title="${translate('settings.downloadPathTemplates.removeMapping', {}, 'Remove mapping')}"> <button type="button" class="remove-mapping-btn" title="${translate('settings.downloadPathTemplates.removeMapping', {}, 'Remove mapping')}">
@@ -1021,7 +998,7 @@ export class SettingsManager {
// Save on select change immediately // Save on select change immediately
baseModelSelect.addEventListener('change', () => this.updateBaseModelMappings()); baseModelSelect.addEventListener('change', () => this.updateBaseModelMappings());
// Save on input blur or Enter key // Save on input blur or Enter key
pathValueInput.addEventListener('blur', () => this.updateBaseModelMappings()); pathValueInput.addEventListener('blur', () => this.updateBaseModelMappings());
pathValueInput.addEventListener('keydown', (e) => { pathValueInput.addEventListener('keydown', (e) => {
@@ -1029,7 +1006,7 @@ export class SettingsManager {
e.target.blur(); e.target.blur();
} }
}); });
removeBtn.addEventListener('click', () => { removeBtn.addEventListener('click', () => {
row.remove(); row.remove();
this.updateBaseModelMappings(); this.updateBaseModelMappings();
@@ -1049,10 +1026,10 @@ export class SettingsManager {
rows.forEach(row => { rows.forEach(row => {
const baseModelSelect = row.querySelector('.base-model-select'); const baseModelSelect = row.querySelector('.base-model-select');
const pathValueInput = row.querySelector('.path-value-input'); const pathValueInput = row.querySelector('.path-value-input');
const baseModel = baseModelSelect.value.trim(); const baseModel = baseModelSelect.value.trim();
const pathValue = pathValueInput.value.trim(); const pathValue = pathValueInput.value.trim();
if (baseModel && pathValue) { if (baseModel && pathValue) {
newMappings[baseModel] = pathValue; newMappings[baseModel] = pathValue;
hasValidMapping = true; hasValidMapping = true;
@@ -1094,15 +1071,15 @@ export class SettingsManager {
rows.forEach(row => { rows.forEach(row => {
const select = row.querySelector('.base-model-select'); const select = row.querySelector('.base-model-select');
const currentValue = select.value; const currentValue = select.value;
// Get available models (not already mapped, except current) // Get available models (not already mapped, except current)
const availableModels = MAPPABLE_BASE_MODELS.filter(model => const availableModels = MAPPABLE_BASE_MODELS.filter(model =>
!existingMappings.hasOwnProperty(model) || model === currentValue !existingMappings.hasOwnProperty(model) || model === currentValue
); );
// Rebuild options // Rebuild options
select.innerHTML = `<option value="">${translate('settings.downloadPathTemplates.selectBaseModel', {}, 'Select Base Model')}</option>` + select.innerHTML = `<option value="">${translate('settings.downloadPathTemplates.selectBaseModel', {}, 'Select Base Model')}</option>` +
availableModels.map(model => availableModels.map(model =>
`<option value="${model}" ${model === currentValue ? 'selected' : ''}>${model}</option>` `<option value="${model}" ${model === currentValue ? 'selected' : ''}>${model}</option>`
).join(''); ).join('');
}); });
@@ -1116,7 +1093,7 @@ export class SettingsManager {
// Show success toast // Show success toast
const mappingCount = Object.keys(state.global.settings.base_model_path_mappings).length; const mappingCount = Object.keys(state.global.settings.base_model_path_mappings).length;
if (mappingCount > 0) { if (mappingCount > 0) {
showToast('toast.settings.mappingsUpdated', { showToast('toast.settings.mappingsUpdated', {
count: mappingCount, count: mappingCount,
plural: mappingCount !== 1 ? 's' : '' plural: mappingCount !== 1 ? 's' : ''
}, 'success'); }, 'success');
@@ -1132,7 +1109,7 @@ export class SettingsManager {
loadDownloadPathTemplates() { loadDownloadPathTemplates() {
const templates = state.global.settings.download_path_templates || DEFAULT_PATH_TEMPLATES; const templates = state.global.settings.download_path_templates || DEFAULT_PATH_TEMPLATES;
Object.keys(templates).forEach(modelType => { Object.keys(templates).forEach(modelType => {
this.loadTemplateForModelType(modelType, templates[modelType]); this.loadTemplateForModelType(modelType, templates[modelType]);
}); });
@@ -1142,12 +1119,12 @@ export class SettingsManager {
const presetSelect = document.getElementById(`${modelType}TemplatePreset`); const presetSelect = document.getElementById(`${modelType}TemplatePreset`);
const customRow = document.getElementById(`${modelType}CustomRow`); const customRow = document.getElementById(`${modelType}CustomRow`);
const customInput = document.getElementById(`${modelType}CustomTemplate`); const customInput = document.getElementById(`${modelType}CustomTemplate`);
if (!presetSelect) return; if (!presetSelect) return;
// Find matching preset // Find matching preset
const matchingPreset = this.findMatchingPreset(template); const matchingPreset = this.findMatchingPreset(template);
if (matchingPreset !== null) { if (matchingPreset !== null) {
presetSelect.value = matchingPreset; presetSelect.value = matchingPreset;
if (customRow) customRow.style.display = 'none'; if (customRow) customRow.style.display = 'none';
@@ -1160,7 +1137,7 @@ export class SettingsManager {
this.validateTemplate(modelType, template); this.validateTemplate(modelType, template);
} }
} }
this.updateTemplatePreview(modelType, template); this.updateTemplatePreview(modelType, template);
} }
@@ -1168,14 +1145,14 @@ export class SettingsManager {
const presetValues = Object.values(DOWNLOAD_PATH_TEMPLATES) const presetValues = Object.values(DOWNLOAD_PATH_TEMPLATES)
.map(t => t.value) .map(t => t.value)
.filter(v => v !== 'custom'); .filter(v => v !== 'custom');
return presetValues.includes(template) ? template : null; return presetValues.includes(template) ? template : null;
} }
updateTemplatePreset(modelType, value) { updateTemplatePreset(modelType, value) {
const customRow = document.getElementById(`${modelType}CustomRow`); const customRow = document.getElementById(`${modelType}CustomRow`);
const customInput = document.getElementById(`${modelType}CustomTemplate`); const customInput = document.getElementById(`${modelType}CustomTemplate`);
if (value === 'custom') { if (value === 'custom') {
if (customRow) customRow.style.display = 'block'; if (customRow) customRow.style.display = 'block';
if (customInput) customInput.focus(); if (customInput) customInput.focus();
@@ -1183,7 +1160,7 @@ export class SettingsManager {
} else { } else {
if (customRow) customRow.style.display = 'none'; if (customRow) customRow.style.display = 'none';
} }
// Update template // Update template
this.updateTemplate(modelType, value); this.updateTemplate(modelType, value);
} }
@@ -1195,16 +1172,16 @@ export class SettingsManager {
return; // Don't save invalid templates return; // Don't save invalid templates
} }
} }
// Update state // Update state
if (!state.global.settings.download_path_templates) { if (!state.global.settings.download_path_templates) {
state.global.settings.download_path_templates = { ...DEFAULT_PATH_TEMPLATES }; state.global.settings.download_path_templates = { ...DEFAULT_PATH_TEMPLATES };
} }
state.global.settings.download_path_templates[modelType] = template; state.global.settings.download_path_templates[modelType] = template;
// Update preview // Update preview
this.updateTemplatePreview(modelType, template); this.updateTemplatePreview(modelType, template);
// Save settings // Save settings
this.saveDownloadPathTemplates(); this.saveDownloadPathTemplates();
} }
@@ -1212,17 +1189,17 @@ export class SettingsManager {
validateTemplate(modelType, template) { validateTemplate(modelType, template) {
const validationElement = document.getElementById(`${modelType}Validation`); const validationElement = document.getElementById(`${modelType}Validation`);
if (!validationElement) return true; if (!validationElement) return true;
// Reset validation state // Reset validation state
validationElement.innerHTML = ''; validationElement.innerHTML = '';
validationElement.className = 'template-validation'; validationElement.className = 'template-validation';
if (!template) { if (!template) {
validationElement.innerHTML = `<i class="fas fa-check"></i> ${translate('settings.downloadPathTemplates.validation.validFlat', {}, 'Valid (flat structure)')}`; validationElement.innerHTML = `<i class="fas fa-check"></i> ${translate('settings.downloadPathTemplates.validation.validFlat', {}, 'Valid (flat structure)')}`;
validationElement.classList.add('valid'); validationElement.classList.add('valid');
return true; return true;
} }
// Check for invalid characters // Check for invalid characters
const invalidChars = /[<>:"|?*]/; const invalidChars = /[<>:"|?*]/;
if (invalidChars.test(template)) { if (invalidChars.test(template)) {
@@ -1230,36 +1207,36 @@ export class SettingsManager {
validationElement.classList.add('invalid'); validationElement.classList.add('invalid');
return false; return false;
} }
// Check for double slashes // Check for double slashes
if (template.includes('//')) { if (template.includes('//')) {
validationElement.innerHTML = `<i class="fas fa-times"></i> ${translate('settings.downloadPathTemplates.validation.doubleSlashes', {}, 'Double slashes not allowed')}`; validationElement.innerHTML = `<i class="fas fa-times"></i> ${translate('settings.downloadPathTemplates.validation.doubleSlashes', {}, 'Double slashes not allowed')}`;
validationElement.classList.add('invalid'); validationElement.classList.add('invalid');
return false; return false;
} }
// Check if it starts or ends with slash // Check if it starts or ends with slash
if (template.startsWith('/') || template.endsWith('/')) { if (template.startsWith('/') || template.endsWith('/')) {
validationElement.innerHTML = `<i class="fas fa-times"></i> ${translate('settings.downloadPathTemplates.validation.leadingTrailingSlash', {}, 'Cannot start or end with slash')}`; validationElement.innerHTML = `<i class="fas fa-times"></i> ${translate('settings.downloadPathTemplates.validation.leadingTrailingSlash', {}, 'Cannot start or end with slash')}`;
validationElement.classList.add('invalid'); validationElement.classList.add('invalid');
return false; return false;
} }
// Extract placeholders // Extract placeholders
const placeholderRegex = /\{([^}]+)\}/g; const placeholderRegex = /\{([^}]+)\}/g;
const matches = template.match(placeholderRegex) || []; const matches = template.match(placeholderRegex) || [];
// Check for invalid placeholders // Check for invalid placeholders
const invalidPlaceholders = matches.filter(match => const invalidPlaceholders = matches.filter(match =>
!PATH_TEMPLATE_PLACEHOLDERS.includes(match) !PATH_TEMPLATE_PLACEHOLDERS.includes(match)
); );
if (invalidPlaceholders.length > 0) { if (invalidPlaceholders.length > 0) {
validationElement.innerHTML = `<i class="fas fa-times"></i> ${translate('settings.downloadPathTemplates.validation.invalidPlaceholder', { placeholder: invalidPlaceholders[0] }, `Invalid placeholder: ${invalidPlaceholders[0]}`)}`; validationElement.innerHTML = `<i class="fas fa-times"></i> ${translate('settings.downloadPathTemplates.validation.invalidPlaceholder', { placeholder: invalidPlaceholders[0] }, `Invalid placeholder: ${invalidPlaceholders[0]}`)}`;
validationElement.classList.add('invalid'); validationElement.classList.add('invalid');
return false; return false;
} }
// Template is valid // Template is valid
validationElement.innerHTML = `<i class="fas fa-check"></i> ${translate('settings.downloadPathTemplates.validation.validTemplate', {}, 'Valid template')}`; validationElement.innerHTML = `<i class="fas fa-check"></i> ${translate('settings.downloadPathTemplates.validation.validTemplate', {}, 'Valid template')}`;
validationElement.classList.add('valid'); validationElement.classList.add('valid');
@@ -1269,7 +1246,7 @@ export class SettingsManager {
updateTemplatePreview(modelType, template) { updateTemplatePreview(modelType, template) {
const previewElement = document.getElementById(`${modelType}Preview`); const previewElement = document.getElementById(`${modelType}Preview`);
if (!previewElement) return; if (!previewElement) return;
if (!template) { if (!template) {
previewElement.textContent = 'model-name.safetensors'; previewElement.textContent = 'model-name.safetensors';
} else { } else {
@@ -1308,7 +1285,7 @@ export class SettingsManager {
async saveToggleSetting(elementId, settingKey) { async saveToggleSetting(elementId, settingKey) {
const element = document.getElementById(elementId); const element = document.getElementById(elementId);
if (!element) return; if (!element) return;
const value = element.checked; const value = element.checked;
try { try {
@@ -1325,12 +1302,12 @@ export class SettingsManager {
if (settingKey === 'enable_metadata_archive_db') { if (settingKey === 'enable_metadata_archive_db') {
await this.updateMetadataArchiveStatus(); await this.updateMetadataArchiveStatus();
} }
showToast('toast.settings.settingsUpdated', { setting: settingKey.replace(/_/g, ' ') }, 'success'); showToast('toast.settings.settingsUpdated', { setting: settingKey.replace(/_/g, ' ') }, 'success');
// Apply frontend settings immediately // Apply frontend settings immediately
this.applyFrontendSettings(); this.applyFrontendSettings();
// Trigger auto download setup/teardown when setting changes // Trigger auto download setup/teardown when setting changes
if (settingKey === 'auto_download_example_images' && window.exampleImagesManager) { if (settingKey === 'auto_download_example_images' && window.exampleImagesManager) {
if (value) { if (value) {
@@ -1339,11 +1316,11 @@ export class SettingsManager {
window.exampleImagesManager.clearAutoDownload(); window.exampleImagesManager.clearAutoDownload();
} }
} }
if (settingKey === 'show_only_sfw' || settingKey === 'blur_mature_content') { if (settingKey === 'show_only_sfw' || settingKey === 'blur_mature_content') {
this.reloadContent(); this.reloadContent();
} }
// Recalculate layout when compact mode changes // Recalculate layout when compact mode changes
if (settingKey === 'compact_mode' && state.virtualScroller) { if (settingKey === 'compact_mode' && state.virtualScroller) {
state.virtualScroller.calculateLayout(); state.virtualScroller.calculateLayout();
@@ -1356,32 +1333,32 @@ export class SettingsManager {
showToast('toast.settings.settingSaveFailed', { message: error.message }, 'error'); showToast('toast.settings.settingSaveFailed', { message: error.message }, 'error');
} }
} }
async saveSelectSetting(elementId, settingKey) { async saveSelectSetting(elementId, settingKey) {
const element = document.getElementById(elementId); const element = document.getElementById(elementId);
if (!element) return; if (!element) return;
const value = element.value; const value = element.value;
try { try {
// Update frontend state with mapped keys // Update frontend state with mapped keys
await this.saveSetting(settingKey, value); await this.saveSetting(settingKey, value);
// Apply frontend settings immediately // Apply frontend settings immediately
this.applyFrontendSettings(); this.applyFrontendSettings();
// Recalculate layout when display density changes // Recalculate layout when display density changes
if (settingKey === 'display_density' && state.virtualScroller) { if (settingKey === 'display_density' && state.virtualScroller) {
state.virtualScroller.calculateLayout(); state.virtualScroller.calculateLayout();
let densityName = "Default"; let densityName = "Default";
if (value === 'medium') densityName = "Medium"; if (value === 'medium') densityName = "Medium";
if (value === 'compact') densityName = "Compact"; if (value === 'compact') densityName = "Compact";
showToast('toast.settings.displayDensitySet', { density: densityName }, 'success'); showToast('toast.settings.displayDensitySet', { density: densityName }, 'success');
return; return;
} }
showToast('toast.settings.settingsUpdated', { setting: settingKey.replace(/_/g, ' ') }, 'success'); showToast('toast.settings.settingsUpdated', { setting: settingKey.replace(/_/g, ' ') }, 'success');
if (settingKey === 'model_name_display' || settingKey === 'model_card_footer_action' || settingKey === 'update_flag_strategy') { if (settingKey === 'model_name_display' || settingKey === 'model_card_footer_action' || settingKey === 'update_flag_strategy') {
@@ -1416,7 +1393,7 @@ export class SettingsManager {
if (statusContainer && data.success) { if (statusContainer && data.success) {
const status = data; const status = data;
const sizeText = status.databaseSize > 0 ? ` (${this.formatFileSize(status.databaseSize)})` : ''; const sizeText = status.databaseSize > 0 ? ` (${this.formatFileSize(status.databaseSize)})` : '';
statusContainer.innerHTML = ` statusContainer.innerHTML = `
<div class="archive-status-item"> <div class="archive-status-item">
<span class="archive-status-label">${translate('settings.metadataArchive.status')}:</span> <span class="archive-status-label">${translate('settings.metadataArchive.status')}:</span>
@@ -1436,14 +1413,14 @@ export class SettingsManager {
// Update button states // Update button states
const downloadBtn = document.getElementById('downloadMetadataArchiveBtn'); const downloadBtn = document.getElementById('downloadMetadataArchiveBtn');
const removeBtn = document.getElementById('removeMetadataArchiveBtn'); const removeBtn = document.getElementById('removeMetadataArchiveBtn');
if (downloadBtn) { if (downloadBtn) {
downloadBtn.disabled = status.isAvailable; downloadBtn.disabled = status.isAvailable;
downloadBtn.textContent = status.isAvailable ? downloadBtn.textContent = status.isAvailable ?
translate('settings.metadataArchive.downloadedButton') : translate('settings.metadataArchive.downloadedButton') :
translate('settings.metadataArchive.downloadButton'); translate('settings.metadataArchive.downloadButton');
} }
if (removeBtn) { if (removeBtn) {
removeBtn.disabled = !status.isAvailable; removeBtn.disabled = !status.isAvailable;
} }
@@ -1464,12 +1441,12 @@ export class SettingsManager {
async downloadMetadataArchive() { async downloadMetadataArchive() {
try { try {
const downloadBtn = document.getElementById('downloadMetadataArchiveBtn'); const downloadBtn = document.getElementById('downloadMetadataArchiveBtn');
if (downloadBtn) { if (downloadBtn) {
downloadBtn.disabled = true; downloadBtn.disabled = true;
downloadBtn.textContent = translate('settings.metadataArchive.downloadingButton'); downloadBtn.textContent = translate('settings.metadataArchive.downloadingButton');
} }
// Show loading with enhanced progress // Show loading with enhanced progress
const progressUpdater = state.loadingManager.showEnhancedProgress(translate('settings.metadataArchive.preparing')); const progressUpdater = state.loadingManager.showEnhancedProgress(translate('settings.metadataArchive.preparing'));
@@ -1477,20 +1454,20 @@ export class SettingsManager {
const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://'; const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
const downloadId = `metadata_archive_${Date.now()}`; const downloadId = `metadata_archive_${Date.now()}`;
const ws = new WebSocket(`${wsProtocol}${window.location.host}/ws/download-progress?id=${downloadId}`); const ws = new WebSocket(`${wsProtocol}${window.location.host}/ws/download-progress?id=${downloadId}`);
let wsConnected = false; let wsConnected = false;
let actualDownloadId = downloadId; // Will be updated when WebSocket confirms the ID let actualDownloadId = downloadId; // Will be updated when WebSocket confirms the ID
// Promise to wait for WebSocket connection and ID confirmation // Promise to wait for WebSocket connection and ID confirmation
const wsReady = new Promise((resolve) => { const wsReady = new Promise((resolve) => {
ws.onopen = () => { ws.onopen = () => {
wsConnected = true; wsConnected = true;
console.log('Connected to metadata archive download progress WebSocket'); console.log('Connected to metadata archive download progress WebSocket');
}; };
ws.onmessage = (event) => { ws.onmessage = (event) => {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
// Handle download ID confirmation // Handle download ID confirmation
if (data.type === 'download_id') { if (data.type === 'download_id') {
actualDownloadId = data.download_id; actualDownloadId = data.download_id;
@@ -1498,11 +1475,11 @@ export class SettingsManager {
resolve(data.download_id); resolve(data.download_id);
return; return;
} }
// Handle metadata archive download progress // Handle metadata archive download progress
if (data.type === 'metadata_archive_download') { if (data.type === 'metadata_archive_download') {
const message = data.message || ''; const message = data.message || '';
// Update progress bar based on stage // Update progress bar based on stage
let progressPercent = 0; let progressPercent = 0;
if (data.stage === 'download') { if (data.stage === 'download') {
@@ -1516,21 +1493,21 @@ export class SettingsManager {
} else if (data.stage === 'extract') { } else if (data.stage === 'extract') {
progressPercent = 95; // Near completion for extraction progressPercent = 95; // Near completion for extraction
} }
// Update loading manager progress // Update loading manager progress
progressUpdater.updateProgress(progressPercent, '', `${message}`); progressUpdater.updateProgress(progressPercent, '', `${message}`);
} }
}; };
ws.onerror = (error) => { ws.onerror = (error) => {
console.error('WebSocket error:', error); console.error('WebSocket error:', error);
resolve(downloadId); // Fallback to original ID resolve(downloadId); // Fallback to original ID
}; };
// Timeout fallback // Timeout fallback
setTimeout(() => resolve(downloadId), 5000); setTimeout(() => resolve(downloadId), 5000);
}); });
ws.onclose = () => { ws.onclose = () => {
console.log('WebSocket connection closed'); console.log('WebSocket connection closed');
}; };
@@ -1555,18 +1532,18 @@ export class SettingsManager {
if (data.success) { if (data.success) {
// Complete progress // Complete progress
await progressUpdater.complete(translate('settings.metadataArchive.downloadComplete')); await progressUpdater.complete(translate('settings.metadataArchive.downloadComplete'));
showToast('settings.metadataArchive.downloadSuccess', 'success'); showToast('settings.metadataArchive.downloadSuccess', 'success');
// Update settings using universal save method // Update settings using universal save method
await this.saveSetting('enable_metadata_archive_db', true); await this.saveSetting('enable_metadata_archive_db', true);
// Update UI // Update UI
const enableCheckbox = document.getElementById('enableMetadataArchive'); const enableCheckbox = document.getElementById('enableMetadataArchive');
if (enableCheckbox) { if (enableCheckbox) {
enableCheckbox.checked = true; enableCheckbox.checked = true;
} }
await this.updateMetadataArchiveStatus(); await this.updateMetadataArchiveStatus();
} else { } else {
// Hide loading on error // Hide loading on error
@@ -1575,7 +1552,7 @@ export class SettingsManager {
} }
} catch (error) { } catch (error) {
console.error('Error downloading metadata archive:', error); console.error('Error downloading metadata archive:', error);
// Hide loading on error // Hide loading on error
state.loadingManager.hide(); state.loadingManager.hide();
@@ -1615,13 +1592,13 @@ export class SettingsManager {
// Update settings using universal save method // Update settings using universal save method
await this.saveSetting('enable_metadata_archive_db', false); await this.saveSetting('enable_metadata_archive_db', false);
// Update UI // Update UI
const enableCheckbox = document.getElementById('enableMetadataArchive'); const enableCheckbox = document.getElementById('enableMetadataArchive');
if (enableCheckbox) { if (enableCheckbox) {
enableCheckbox.checked = false; enableCheckbox.checked = false;
} }
await this.updateMetadataArchiveStatus(); await this.updateMetadataArchiveStatus();
} else { } else {
showToast('settings.metadataArchive.removeError' + ': ' + data.error, 'error'); showToast('settings.metadataArchive.removeError' + ': ' + data.error, 'error');
@@ -1695,23 +1672,23 @@ export class SettingsManager {
if (!element) return; if (!element) return;
const value = element.value.trim(); // Trim whitespace const value = element.value.trim(); // Trim whitespace
try { try {
// Check if value has changed from existing value // Check if value has changed from existing value
const currentValue = state.global.settings[settingKey] || ''; const currentValue = state.global.settings[settingKey] || '';
if (value === currentValue) { if (value === currentValue) {
return; // No change, exit early return; // No change, exit early
} }
// For username and password, handle empty values specially // For username and password, handle empty values specially
if ((settingKey === 'proxy_username' || settingKey === 'proxy_password') && value === '') { if ((settingKey === 'proxy_username' || settingKey === 'proxy_password') && value === '') {
// Remove from state instead of setting to empty string // Remove from state instead of setting to empty string
delete state.global.settings[settingKey]; delete state.global.settings[settingKey];
// Send delete flag to backend // Send delete flag to backend
const payload = {}; const payload = {};
payload[settingKey] = '__DELETE__'; payload[settingKey] = '__DELETE__';
const response = await fetch('/api/lm/settings', { const response = await fetch('/api/lm/settings', {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -1727,9 +1704,9 @@ export class SettingsManager {
// Use the universal save method // Use the universal save method
await this.saveSetting(settingKey, value); await this.saveSetting(settingKey, value);
} }
showToast('toast.settings.settingsUpdated', { setting: settingKey.replace(/_/g, ' ') }, 'success'); showToast('toast.settings.settingsUpdated', { setting: settingKey.replace(/_/g, ' ') }, 'success');
} catch (error) { } catch (error) {
showToast('toast.settings.settingSaveFailed', { message: error.message }, 'error'); showToast('toast.settings.settingSaveFailed', { message: error.message }, 'error');
} }
@@ -1756,7 +1733,7 @@ export class SettingsManager {
toggleInputVisibility(button) { toggleInputVisibility(button) {
const input = button.parentElement.querySelector('input'); const input = button.parentElement.querySelector('input');
const icon = button.querySelector('i'); const icon = button.querySelector('i');
if (input.type === 'password') { if (input.type === 'password') {
input.type = 'text'; input.type = 'text';
icon.className = 'fas fa-eye-slash'; icon.className = 'fas fa-eye-slash';
@@ -1793,7 +1770,7 @@ export class SettingsManager {
document.querySelectorAll('.card-preview video').forEach(video => { document.querySelectorAll('.card-preview video').forEach(video => {
configureModelCardVideo(video, autoplayOnHover); configureModelCardVideo(video, autoplayOnHover);
}); });
// Apply display density class to grid // Apply display density class to grid
const grid = document.querySelector('.card-grid'); const grid = document.querySelector('.card-grid');
if (grid) { if (grid) {

View File

@@ -7,7 +7,6 @@
<button <button
type="button" type="button"
class="settings-action-link settings-open-location-trigger" class="settings-action-link settings-open-location-trigger"
data-settings-path="{{ settings.settings_file }}"
aria-label="{{ t('settings.openSettingsFileLocation.tooltip') }}" aria-label="{{ t('settings.openSettingsFileLocation.tooltip') }}"
title="{{ t('settings.openSettingsFileLocation.tooltip') }}"> title="{{ t('settings.openSettingsFileLocation.tooltip') }}">
<i class="fas fa-external-link-alt" aria-hidden="true"></i> <i class="fas fa-external-link-alt" aria-hidden="true"></i>