From 0c883433c11b479cae578e717f90fc5a926a0e40 Mon Sep 17 00:00:00 2001
From: Will Miao <13051207myq@gmail.com>
Date: Sun, 20 Jul 2025 17:51:08 +0800
Subject: [PATCH] feat: Implement download path template settings and base
model path mappings in UI
---
static/css/components/modal.css | 152 +++++++++++++++++-
static/js/managers/SettingsManager.js | 222 +++++++++++++++++++++++++-
static/js/utils/constants.js | 38 ++++-
templates/components/modals.html | 126 ++++++++++++---
4 files changed, 502 insertions(+), 36 deletions(-)
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Customize folder names for specific base models (e.g., "Flux.1 D" → "flux")
+
+
+
+
@@ -235,7 +334,7 @@
-
-
-
Example Images