diff --git a/static/css/components/modal.css b/static/css/components/modal.css index 8ac0f56d..500afa08 100644 --- a/static/css/components/modal.css +++ b/static/css/components/modal.css @@ -691,14 +691,14 @@ input:checked + .toggle-slider:before { color: var(--lora-warning, #f39c12); } -/* Add styles for density description list */ -.density-description { +/* Add styles for list description */ +.list-description { margin: 8px 0; padding-left: 20px; font-size: 0.9em; } -.density-description li { +.list-description li { margin-bottom: 4px; } @@ -1135,4 +1135,150 @@ input:checked + .toggle-slider:before { /* Dark theme adjustments */ [data-theme="dark"] .example-option-btn:hover { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25); +} + +/* Path Template Settings Styles */ +.template-preview { + background: rgba(0, 0, 0, 0.03); + border: 1px solid rgba(0, 0, 0, 0.1); + border-radius: var(--border-radius-xs); + padding: var(--space-1); + margin-top: 8px; + font-family: monospace; + font-size: 1.1em; + color: var(--lora-accent); + display: none; +} + +[data-theme="dark"] .template-preview { + background: rgba(255, 255, 255, 0.03); + border: 1px solid var(--lora-border); +} + +.template-preview:before { + content: "Preview: "; + opacity: 0.7; + color: var(--text-color); + font-family: inherit; +} + +/* Base Model Mappings Styles - Updated to match other settings */ +.mappings-container { + border: 1px solid var(--lora-border); + border-radius: var(--border-radius-sm); + padding: var(--space-2); + background: rgba(0, 0, 0, 0.02); + margin-top: 8px; /* Add consistent spacing */ +} + +[data-theme="dark"] .mappings-container { + background: rgba(255, 255, 255, 0.02); +} + +.add-mapping-btn { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + background: var(--lora-accent); + color: white; + border: none; + border-radius: var(--border-radius-xs); + cursor: pointer; + font-size: 0.9em; + transition: all 0.2s; + height: 32px; /* Match other control heights */ +} + +.add-mapping-btn:hover { + background: oklch(from var(--lora-accent) l c h / 85%); +} + +.mapping-row { + margin-bottom: var(--space-2); +} + +.mapping-row:last-child { + margin-bottom: 0; +} + +.mapping-controls { + display: grid; + grid-template-columns: 1fr 1fr auto; + gap: var(--space-1); + align-items: center; +} + +.base-model-select, +.path-value-input { + padding: 6px 10px; + border-radius: var(--border-radius-xs); + border: 1px solid var(--border-color); + background-color: var(--lora-surface); + color: var(--text-color); + font-size: 0.9em; + height: 32px; +} + +.path-value-input { + height: 18px; +} + +.base-model-select:focus, +.path-value-input:focus { + border-color: var(--lora-accent); + outline: none; + box-shadow: 0 0 0 2px rgba(var(--lora-accent-rgb, 79, 70, 229), 0.1); +} + +.remove-mapping-btn { + width: 32px; + height: 32px; + border-radius: var(--border-radius-xs); + border: 1px solid var(--lora-error); + background: transparent; + color: var(--lora-error); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s; +} + +.remove-mapping-btn:hover { + background: var(--lora-error); + color: white; +} + +.mapping-empty-state { + text-align: center; + padding: var(--space-3); + color: var(--text-color); + opacity: 0.6; + font-style: italic; +} + +/* Responsive adjustments for mapping controls */ +@media (max-width: 768px) { + .mapping-controls { + grid-template-columns: 1fr; + gap: 8px; + } + + .remove-mapping-btn { + width: 100%; + height: 36px; + justify-self: stretch; + } +} + +/* Dark theme specific adjustments */ +[data-theme="dark"] .base-model-select, +[data-theme="dark"] .path-value-input { + background-color: rgba(30, 30, 30, 0.9); +} + +[data-theme="dark"] .base-model-select option { + background-color: #2d2d2d; + color: var(--text-color); } \ No newline at end of file diff --git a/static/js/managers/SettingsManager.js b/static/js/managers/SettingsManager.js index a7a802ec..828cb461 100644 --- a/static/js/managers/SettingsManager.js +++ b/static/js/managers/SettingsManager.js @@ -3,6 +3,7 @@ import { showToast } from '../utils/uiHelpers.js'; import { state } from '../state/index.js'; import { resetAndReload } from '../api/loraApi.js'; import { setStorageItem, getStorageItem } from '../utils/storageHelpers.js'; +import { DOWNLOAD_PATH_TEMPLATES, MAPPABLE_BASE_MODELS } from '../utils/constants.js'; export class SettingsManager { constructor() { @@ -56,6 +57,16 @@ export class SettingsManager { } // We can delete the old setting, but keeping it for backwards compatibility } + + // Set default for download path template if undefined + if (state.global.settings.download_path_template === undefined) { + state.global.settings.download_path_template = DOWNLOAD_PATH_TEMPLATES.BASE_MODEL_TAG.value; + } + + // 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 = {}; + } } initialize() { @@ -125,6 +136,16 @@ export class SettingsManager { optimizeExampleImagesCheckbox.checked = state.global.settings.optimizeExampleImages || false; } + // Set download path template setting + const downloadPathTemplateSelect = document.getElementById('downloadPathTemplate'); + if (downloadPathTemplateSelect) { + downloadPathTemplateSelect.value = state.global.settings.download_path_template || ''; + this.updatePathTemplatePreview(); + } + + // Load base model path mappings + this.loadBaseModelMappings(); + // Load default lora root await this.loadLoraRoots(); @@ -212,6 +233,197 @@ export class SettingsManager { } } + loadBaseModelMappings() { + const mappingsContainer = document.getElementById('baseModelMappingsContainer'); + if (!mappingsContainer) return; + + const mappings = state.global.settings.base_model_path_mappings || {}; + + // Clear existing mappings + mappingsContainer.innerHTML = ''; + + // Add existing mappings + Object.entries(mappings).forEach(([baseModel, pathValue]) => { + this.addMappingRow(baseModel, pathValue); + }); + + // Add empty row for new mappings if none exist + if (Object.keys(mappings).length === 0) { + this.addMappingRow('', ''); + } + } + + addMappingRow(baseModel = '', pathValue = '') { + const mappingsContainer = document.getElementById('baseModelMappingsContainer'); + if (!mappingsContainer) return; + + const row = document.createElement('div'); + row.className = 'mapping-row'; + + const availableModels = MAPPABLE_BASE_MODELS.filter(model => { + const existingMappings = state.global.settings.base_model_path_mappings || {}; + return !existingMappings.hasOwnProperty(model) || model === baseModel; + }); + + row.innerHTML = ` +
+ + + +
+ `; + + // Add event listeners + const baseModelSelect = row.querySelector('.base-model-select'); + const pathValueInput = row.querySelector('.path-value-input'); + const removeBtn = row.querySelector('.remove-mapping-btn'); + + // Save on select change immediately + baseModelSelect.addEventListener('change', () => this.updateBaseModelMappings()); + + // Save on input blur or Enter key + pathValueInput.addEventListener('blur', () => this.updateBaseModelMappings()); + pathValueInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + e.target.blur(); + } + }); + + removeBtn.addEventListener('click', () => { + row.remove(); + this.updateBaseModelMappings(); + }); + + mappingsContainer.appendChild(row); + } + + updateBaseModelMappings() { + const mappingsContainer = document.getElementById('baseModelMappingsContainer'); + if (!mappingsContainer) return; + + const rows = mappingsContainer.querySelectorAll('.mapping-row'); + const newMappings = {}; + let hasValidMapping = false; + + rows.forEach(row => { + const baseModelSelect = row.querySelector('.base-model-select'); + const pathValueInput = row.querySelector('.path-value-input'); + + const baseModel = baseModelSelect.value.trim(); + const pathValue = pathValueInput.value.trim(); + + if (baseModel && pathValue) { + newMappings[baseModel] = pathValue; + hasValidMapping = true; + } + }); + + // Check if mappings have actually changed + const currentMappings = state.global.settings.base_model_path_mappings || {}; + const mappingsChanged = JSON.stringify(currentMappings) !== JSON.stringify(newMappings); + + if (mappingsChanged) { + // Update state and save + state.global.settings.base_model_path_mappings = newMappings; + this.saveBaseModelMappings(); + } + + // Add empty row if no valid mappings exist + const hasEmptyRow = Array.from(rows).some(row => { + const baseModelSelect = row.querySelector('.base-model-select'); + const pathValueInput = row.querySelector('.path-value-input'); + return !baseModelSelect.value && !pathValueInput.value; + }); + + if (!hasEmptyRow) { + this.addMappingRow('', ''); + } + + // Update available options in all selects + this.updateAvailableBaseModels(); + } + + updateAvailableBaseModels() { + const mappingsContainer = document.getElementById('baseModelMappingsContainer'); + if (!mappingsContainer) return; + + const existingMappings = state.global.settings.base_model_path_mappings || {}; + const rows = mappingsContainer.querySelectorAll('.mapping-row'); + + rows.forEach(row => { + const select = row.querySelector('.base-model-select'); + const currentValue = select.value; + + // Get available models (not already mapped, except current) + const availableModels = MAPPABLE_BASE_MODELS.filter(model => + !existingMappings.hasOwnProperty(model) || model === currentValue + ); + + // Rebuild options + select.innerHTML = '' + + availableModels.map(model => + `` + ).join(''); + }); + } + + async saveBaseModelMappings() { + try { + // Save to 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: JSON.stringify(state.global.settings.base_model_path_mappings) + }) + }); + + if (!response.ok) { + throw new Error('Failed to save base model mappings'); + } + + // Show success toast + const mappingCount = Object.keys(state.global.settings.base_model_path_mappings).length; + if (mappingCount > 0) { + showToast(`Base model path mappings updated (${mappingCount} mapping${mappingCount !== 1 ? 's' : ''})`, 'success'); + } else { + showToast('Base model path mappings cleared', 'success'); + } + + } catch (error) { + console.error('Error saving base model mappings:', error); + showToast('Failed to save base model mappings: ' + error.message, 'error'); + } + } + + updatePathTemplatePreview() { + const templateSelect = document.getElementById('downloadPathTemplate'); + const previewElement = document.getElementById('pathTemplatePreview'); + if (!templateSelect || !previewElement) return; + + const template = templateSelect.value; + const templateInfo = Object.values(DOWNLOAD_PATH_TEMPLATES).find(t => t.value === template); + + if (templateInfo) { + previewElement.textContent = templateInfo.example; + previewElement.style.display = 'block'; + } else { + previewElement.style.display = 'none'; + } + } + toggleSettings() { if (this.isOpen) { modalManager.closeModal('settingsModal'); @@ -221,9 +433,6 @@ export class SettingsManager { this.isOpen = !this.isOpen; } - // Auto-save methods for different control types - - // For toggle switches async saveToggleSetting(elementId, settingKey) { const element = document.getElementById(elementId); if (!element) return; @@ -288,7 +497,6 @@ export class SettingsManager { } } - // For select dropdowns async saveSelectSetting(elementId, settingKey) { const element = document.getElementById(elementId); if (!element) return; @@ -307,6 +515,9 @@ export class SettingsManager { state.global.settings.compactMode = (value !== 'default'); } else if (settingKey === 'card_info_display') { state.global.settings.cardInfoDisplay = value; + } else if (settingKey === 'download_path_template') { + state.global.settings.download_path_template = value; + this.updatePathTemplatePreview(); } else { // For any other settings that might be added in the future state.global.settings[settingKey] = value; @@ -317,7 +528,7 @@ export class SettingsManager { try { // For backend settings, make API call - if (settingKey === 'default_lora_root' || settingKey === 'default_checkpoint_root') { + if (settingKey === 'default_lora_root' || settingKey === 'default_checkpoint_root' || settingKey === 'download_path_template') { const payload = {}; payload[settingKey] = value; @@ -355,7 +566,6 @@ export class SettingsManager { } } - // For input fields async saveInputSetting(elementId, settingKey) { const element = document.getElementById(elementId); if (!element) return; diff --git a/static/js/utils/constants.js b/static/js/utils/constants.js index 7b84b61a..4c7520f9 100644 --- a/static/js/utils/constants.js +++ b/static/js/utils/constants.js @@ -40,10 +40,13 @@ export const BASE_MODELS = { SVD: "SVD", LTXV: "LTXV", WAN_VIDEO: "Wan Video", + WAN_VIDEO_1_3B_T2V: "Wan Video 1.3B t2v", + WAN_VIDEO_14B_T2V: "Wan Video 14B t2v", + WAN_VIDEO_14B_I2V_480P: "Wan Video 14B i2v 480p", + WAN_VIDEO_14B_I2V_720P: "Wan Video 14B i2v 720p", HUNYUAN_VIDEO: "Hunyuan Video", - // Default - UNKNOWN: "Unknown" + UNKNOWN: "Other" }; // Base model display names and their corresponding class names (for styling) @@ -95,6 +98,37 @@ export const BASE_MODEL_CLASSES = { [BASE_MODELS.UNKNOWN]: "unknown" }; +// Path template constants for download organization +export const DOWNLOAD_PATH_TEMPLATES = { + FLAT: { + value: '', + label: 'Flat Structure', + description: 'Download directly to root folder', + example: 'model-name.safetensors' + }, + BASE_MODEL: { + value: '{base_model}', + label: 'By Base Model', + description: 'Organize by base model type', + example: 'Flux.1 D/model-name.safetensors' + }, + FIRST_TAG: { + value: '{first_tag}', + label: 'By First Tag', + description: 'Organize by primary tag/category', + example: 'style/model-name.safetensors' + }, + BASE_MODEL_TAG: { + value: '{base_model}/{first_tag}', + label: 'Base Model + First Tag', + description: 'Organize by base model and primary tag', + example: 'Flux.1 D/style/model-name.safetensors' + } +}; + +// Base models available for path mapping (for UI selection) +export const MAPPABLE_BASE_MODELS = Object.values(BASE_MODELS).sort(); + export const NSFW_LEVELS = { UNKNOWN: 0, PG: 1, diff --git a/templates/components/modals.html b/templates/components/modals.html index 71c85d67..d3d7df1e 100644 --- a/templates/components/modals.html +++ b/templates/components/modals.html @@ -177,6 +177,52 @@ + +
+

Video Settings

+ +
+
+
+ +
+
+ +
+
+
+ Only play video previews when hovering over them +
+
+
+ + +
+

Video Settings

+ +
+
+
+ +
+
+ +
+
+
+ Only play video previews when hovering over them +
+
+
+

Folder Settings

@@ -215,6 +261,59 @@
+ + +
+

Default Path Customization

+ +
+
+
+ +
+
+ +
+
+
+ "Configure path structure for default download locations +
    +
  • Flat: All models in root folder
  • +
  • Base Model: Organized by model type (e.g., Flux.1 D, SDXL)
  • +
  • First Tag: Organized by primary tag (e.g., style, character)
  • +
  • Base Model + Tag: Two-level organization for better structure
  • +
+
+
+
+ +
+
+
+ +
+
+ +
+
+
+ Customize folder names for specific base models (e.g., "Flux.1 D" → "flux") +
+
+
+ +
+
+
+
@@ -235,7 +334,7 @@
Choose how many cards to display per row: -
Choose when to display model information and action buttons: -
- - -

Example Images