mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 23:25:43 -03:00
feat: implement download path templates configuration with support for multiple model types and custom templates
This commit is contained in:
@@ -184,7 +184,7 @@ class MiscRoutes:
|
|||||||
logger.info(f"Example images path changed to {value} - server restart required")
|
logger.info(f"Example images path changed to {value} - server restart required")
|
||||||
|
|
||||||
# Special handling for base_model_path_mappings - parse JSON string
|
# Special handling for base_model_path_mappings - parse JSON string
|
||||||
if key == 'base_model_path_mappings' and value:
|
if (key == 'base_model_path_mappings' or key == 'download_path_templates') and value:
|
||||||
try:
|
try:
|
||||||
value = json.loads(value)
|
value = json.loads(value)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
|
|||||||
@@ -483,3 +483,98 @@ input:checked + .toggle-slider:before {
|
|||||||
background-color: #2d2d2d;
|
background-color: #2d2d2d;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Template Configuration Styles */
|
||||||
|
.placeholder-info {
|
||||||
|
margin-top: var(--space-1);
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-tag {
|
||||||
|
display: inline-block;
|
||||||
|
background: var(--lora-accent);
|
||||||
|
color: white;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-custom-row {
|
||||||
|
margin-top: 8px;
|
||||||
|
animation: slideDown 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-custom-input {
|
||||||
|
width: 96%;
|
||||||
|
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.95em;
|
||||||
|
font-family: monospace;
|
||||||
|
height: 24px;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-custom-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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-custom-input::placeholder {
|
||||||
|
color: var(--text-color);
|
||||||
|
opacity: 0.5;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-validation {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
min-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-validation.valid {
|
||||||
|
color: var(--lora-success, #22c55e);
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-validation.invalid {
|
||||||
|
color: var(--lora-error, #ef4444);
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-validation i {
|
||||||
|
width: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark theme specific adjustments */
|
||||||
|
[data-theme="dark"] .template-custom-input {
|
||||||
|
background-color: rgba(30, 30, 30, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.placeholder-info {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ import { showToast } from '../utils/uiHelpers.js';
|
|||||||
import { state } from '../state/index.js';
|
import { state } from '../state/index.js';
|
||||||
import { resetAndReload } from '../api/modelApiFactory.js';
|
import { resetAndReload } from '../api/modelApiFactory.js';
|
||||||
import { setStorageItem, getStorageItem } from '../utils/storageHelpers.js';
|
import { setStorageItem, getStorageItem } from '../utils/storageHelpers.js';
|
||||||
import { DOWNLOAD_PATH_TEMPLATES, MAPPABLE_BASE_MODELS } from '../utils/constants.js';
|
import { DOWNLOAD_PATH_TEMPLATES, MAPPABLE_BASE_MODELS, PATH_TEMPLATE_PLACEHOLDERS, DEFAULT_PATH_TEMPLATES } from '../utils/constants.js';
|
||||||
|
|
||||||
export class SettingsManager {
|
export class SettingsManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -73,11 +73,30 @@ export class SettingsManager {
|
|||||||
// We can delete the old setting, but keeping it for backwards compatibility
|
// We can delete the old setting, but keeping it for backwards compatibility
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set default for download path template if undefined
|
// Migrate legacy download_path_template to new structure
|
||||||
if (state.global.settings.download_path_template === undefined) {
|
if (state.global.settings.download_path_template && !state.global.settings.download_path_templates) {
|
||||||
state.global.settings.download_path_template = DOWNLOAD_PATH_TEMPLATES.BASE_MODEL_TAG.value;
|
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 (!state.global.settings.download_path_templates[modelType]) {
|
||||||
|
state.global.settings.download_path_templates[modelType] = DEFAULT_PATH_TEMPLATES[modelType];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Set default for base model path mappings if undefined
|
// Set default for base model path mappings if undefined
|
||||||
if (state.global.settings.base_model_path_mappings === undefined) {
|
if (state.global.settings.base_model_path_mappings === undefined) {
|
||||||
state.global.settings.base_model_path_mappings = {};
|
state.global.settings.base_model_path_mappings = {};
|
||||||
@@ -105,7 +124,7 @@ export class SettingsManager {
|
|||||||
'default_checkpoint_root',
|
'default_checkpoint_root',
|
||||||
'default_embedding_root',
|
'default_embedding_root',
|
||||||
'base_model_path_mappings',
|
'base_model_path_mappings',
|
||||||
'download_path_template'
|
'download_path_templates'
|
||||||
];
|
];
|
||||||
|
|
||||||
// Build payload for syncing
|
// Build payload for syncing
|
||||||
@@ -113,7 +132,7 @@ export class SettingsManager {
|
|||||||
|
|
||||||
fieldsToSync.forEach(key => {
|
fieldsToSync.forEach(key => {
|
||||||
if (localSettings[key] !== undefined) {
|
if (localSettings[key] !== undefined) {
|
||||||
if (key === 'base_model_path_mappings') {
|
if (key === 'base_model_path_mappings' || key === 'download_path_templates') {
|
||||||
payload[key] = JSON.stringify(localSettings[key]);
|
payload[key] = JSON.stringify(localSettings[key]);
|
||||||
} else {
|
} else {
|
||||||
payload[key] = localSettings[key];
|
payload[key] = localSettings[key];
|
||||||
@@ -165,6 +184,30 @@ export class SettingsManager {
|
|||||||
button.addEventListener('click', () => this.toggleInputVisibility(button));
|
button.addEventListener('click', () => this.toggleInputVisibility(button));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
['lora', 'checkpoint', 'embedding'].forEach(modelType => {
|
||||||
|
const customInput = document.getElementById(`${modelType}CustomTemplate`);
|
||||||
|
if (customInput) {
|
||||||
|
customInput.addEventListener('input', (e) => {
|
||||||
|
const template = e.target.value;
|
||||||
|
settingsManager.validateTemplate(modelType, template);
|
||||||
|
settingsManager.updateTemplatePreview(modelType, template);
|
||||||
|
});
|
||||||
|
|
||||||
|
customInput.addEventListener('blur', (e) => {
|
||||||
|
const template = e.target.value;
|
||||||
|
if (settingsManager.validateTemplate(modelType, template)) {
|
||||||
|
settingsManager.updateTemplate(modelType, template);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
customInput.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.target.blur();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,12 +254,8 @@ export class SettingsManager {
|
|||||||
autoDownloadExampleImagesCheckbox.checked = state.global.settings.autoDownloadExampleImages || false;
|
autoDownloadExampleImagesCheckbox.checked = state.global.settings.autoDownloadExampleImages || false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set download path template setting
|
// Load download path templates
|
||||||
const downloadPathTemplateSelect = document.getElementById('downloadPathTemplate');
|
this.loadDownloadPathTemplates();
|
||||||
if (downloadPathTemplateSelect) {
|
|
||||||
downloadPathTemplateSelect.value = state.global.settings.download_path_template || '';
|
|
||||||
this.updatePathTemplatePreview();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set include trigger words setting
|
// Set include trigger words setting
|
||||||
const includeTriggerWordsCheckbox = document.getElementById('includeTriggerWords');
|
const includeTriggerWordsCheckbox = document.getElementById('includeTriggerWords');
|
||||||
@@ -529,19 +568,184 @@ export class SettingsManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePathTemplatePreview() {
|
loadDownloadPathTemplates() {
|
||||||
const templateSelect = document.getElementById('downloadPathTemplate');
|
const templates = state.global.settings.download_path_templates || DEFAULT_PATH_TEMPLATES;
|
||||||
const previewElement = document.getElementById('pathTemplatePreview');
|
|
||||||
if (!templateSelect || !previewElement) return;
|
|
||||||
|
|
||||||
const template = templateSelect.value;
|
Object.keys(templates).forEach(modelType => {
|
||||||
const templateInfo = Object.values(DOWNLOAD_PATH_TEMPLATES).find(t => t.value === template);
|
this.loadTemplateForModelType(modelType, templates[modelType]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (templateInfo) {
|
loadTemplateForModelType(modelType, template) {
|
||||||
previewElement.textContent = templateInfo.example;
|
const presetSelect = document.getElementById(`${modelType}TemplatePreset`);
|
||||||
previewElement.style.display = 'block';
|
const customRow = document.getElementById(`${modelType}CustomRow`);
|
||||||
|
const customInput = document.getElementById(`${modelType}CustomTemplate`);
|
||||||
|
|
||||||
|
if (!presetSelect) return;
|
||||||
|
|
||||||
|
// Find matching preset
|
||||||
|
const matchingPreset = this.findMatchingPreset(template);
|
||||||
|
|
||||||
|
if (matchingPreset) {
|
||||||
|
presetSelect.value = matchingPreset;
|
||||||
|
if (customRow) customRow.style.display = 'none';
|
||||||
} else {
|
} else {
|
||||||
previewElement.style.display = 'none';
|
// Custom template
|
||||||
|
presetSelect.value = 'custom';
|
||||||
|
if (customRow) customRow.style.display = 'block';
|
||||||
|
if (customInput) {
|
||||||
|
customInput.value = template;
|
||||||
|
this.validateTemplate(modelType, template);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateTemplatePreview(modelType, template);
|
||||||
|
}
|
||||||
|
|
||||||
|
findMatchingPreset(template) {
|
||||||
|
const presetValues = Object.values(DOWNLOAD_PATH_TEMPLATES)
|
||||||
|
.map(t => t.value)
|
||||||
|
.filter(v => v !== 'custom');
|
||||||
|
|
||||||
|
return presetValues.includes(template) ? template : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTemplatePreset(modelType, value) {
|
||||||
|
const customRow = document.getElementById(`${modelType}CustomRow`);
|
||||||
|
const customInput = document.getElementById(`${modelType}CustomTemplate`);
|
||||||
|
|
||||||
|
if (value === 'custom') {
|
||||||
|
if (customRow) customRow.style.display = 'block';
|
||||||
|
if (customInput) customInput.focus();
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
if (customRow) customRow.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update template
|
||||||
|
this.updateTemplate(modelType, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTemplate(modelType, template) {
|
||||||
|
// Validate template if it's custom
|
||||||
|
if (document.getElementById(`${modelType}TemplatePreset`).value === 'custom') {
|
||||||
|
if (!this.validateTemplate(modelType, template)) {
|
||||||
|
return; // Don't save invalid templates
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update state
|
||||||
|
if (!state.global.settings.download_path_templates) {
|
||||||
|
state.global.settings.download_path_templates = { ...DEFAULT_PATH_TEMPLATES };
|
||||||
|
}
|
||||||
|
state.global.settings.download_path_templates[modelType] = template;
|
||||||
|
|
||||||
|
// Update preview
|
||||||
|
this.updateTemplatePreview(modelType, template);
|
||||||
|
|
||||||
|
// Save settings
|
||||||
|
this.saveDownloadPathTemplates();
|
||||||
|
}
|
||||||
|
|
||||||
|
validateTemplate(modelType, template) {
|
||||||
|
const validationElement = document.getElementById(`${modelType}Validation`);
|
||||||
|
if (!validationElement) return true;
|
||||||
|
|
||||||
|
// Reset validation state
|
||||||
|
validationElement.innerHTML = '';
|
||||||
|
validationElement.className = 'template-validation';
|
||||||
|
|
||||||
|
if (!template) {
|
||||||
|
validationElement.innerHTML = '<i class="fas fa-check"></i> Valid (flat structure)';
|
||||||
|
validationElement.classList.add('valid');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for invalid characters
|
||||||
|
const invalidChars = /[<>:"|?*]/;
|
||||||
|
if (invalidChars.test(template)) {
|
||||||
|
validationElement.innerHTML = '<i class="fas fa-times"></i> Invalid characters detected';
|
||||||
|
validationElement.classList.add('invalid');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for double slashes
|
||||||
|
if (template.includes('//')) {
|
||||||
|
validationElement.innerHTML = '<i class="fas fa-times"></i> Double slashes not allowed';
|
||||||
|
validationElement.classList.add('invalid');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it starts or ends with slash
|
||||||
|
if (template.startsWith('/') || template.endsWith('/')) {
|
||||||
|
validationElement.innerHTML = '<i class="fas fa-times"></i> Cannot start or end with slash';
|
||||||
|
validationElement.classList.add('invalid');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract placeholders
|
||||||
|
const placeholderRegex = /\{([^}]+)\}/g;
|
||||||
|
const matches = template.match(placeholderRegex) || [];
|
||||||
|
|
||||||
|
// Check for invalid placeholders
|
||||||
|
const invalidPlaceholders = matches.filter(match =>
|
||||||
|
!PATH_TEMPLATE_PLACEHOLDERS.includes(match)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (invalidPlaceholders.length > 0) {
|
||||||
|
validationElement.innerHTML = `<i class="fas fa-times"></i> Invalid placeholder: ${invalidPlaceholders[0]}`;
|
||||||
|
validationElement.classList.add('invalid');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Template is valid
|
||||||
|
validationElement.innerHTML = '<i class="fas fa-check"></i> Valid template';
|
||||||
|
validationElement.classList.add('valid');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTemplatePreview(modelType, template) {
|
||||||
|
const previewElement = document.getElementById(`${modelType}Preview`);
|
||||||
|
if (!previewElement) return;
|
||||||
|
|
||||||
|
if (!template) {
|
||||||
|
previewElement.textContent = 'model-name.safetensors';
|
||||||
|
} else {
|
||||||
|
// Generate example preview
|
||||||
|
const exampleTemplate = template
|
||||||
|
.replace('{base_model}', 'Flux.1 D')
|
||||||
|
.replace('{author}', 'authorname')
|
||||||
|
.replace('{first_tag}', 'style');
|
||||||
|
previewElement.textContent = `${exampleTemplate}/model-name.safetensors`;
|
||||||
|
}
|
||||||
|
previewElement.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
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: JSON.stringify(state.global.settings.download_path_templates)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to save download path templates');
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast('Download path templates updated', 'success');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving download path templates:', error);
|
||||||
|
showToast('Failed to save download path templates: ' + error.message, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -651,9 +855,6 @@ export class SettingsManager {
|
|||||||
state.global.settings.compactMode = (value !== 'default');
|
state.global.settings.compactMode = (value !== 'default');
|
||||||
} else if (settingKey === 'card_info_display') {
|
} else if (settingKey === 'card_info_display') {
|
||||||
state.global.settings.cardInfoDisplay = value;
|
state.global.settings.cardInfoDisplay = value;
|
||||||
} else if (settingKey === 'download_path_template') {
|
|
||||||
state.global.settings.download_path_template = value;
|
|
||||||
this.updatePathTemplatePreview();
|
|
||||||
} else {
|
} else {
|
||||||
// For any other settings that might be added in the future
|
// For any other settings that might be added in the future
|
||||||
state.global.settings[settingKey] = value;
|
state.global.settings[settingKey] = value;
|
||||||
@@ -664,9 +865,13 @@ export class SettingsManager {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// For backend settings, make API call
|
// For backend settings, make API call
|
||||||
if (settingKey === 'default_lora_root' || settingKey === 'default_checkpoint_root' || settingKey === 'default_embedding_root' || settingKey === 'download_path_template') {
|
if (settingKey === 'default_lora_root' || settingKey === 'default_checkpoint_root' || settingKey === 'default_embedding_root' || settingKey === 'download_path_templates') {
|
||||||
const payload = {};
|
const payload = {};
|
||||||
payload[settingKey] = value;
|
if (settingKey === 'download_path_templates') {
|
||||||
|
payload[settingKey] = JSON.stringify(state.global.settings.download_path_templates);
|
||||||
|
} else {
|
||||||
|
payload[settingKey] = value;
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch('/api/settings', {
|
const response = await fetch('/api/settings', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
@@ -112,6 +112,12 @@ export const DOWNLOAD_PATH_TEMPLATES = {
|
|||||||
description: 'Organize by base model type',
|
description: 'Organize by base model type',
|
||||||
example: 'Flux.1 D/model-name.safetensors'
|
example: 'Flux.1 D/model-name.safetensors'
|
||||||
},
|
},
|
||||||
|
AUTHOR: {
|
||||||
|
value: '{author}',
|
||||||
|
label: 'By Author',
|
||||||
|
description: 'Organize by model author',
|
||||||
|
example: 'authorname/model-name.safetensors'
|
||||||
|
},
|
||||||
FIRST_TAG: {
|
FIRST_TAG: {
|
||||||
value: '{first_tag}',
|
value: '{first_tag}',
|
||||||
label: 'By First Tag',
|
label: 'By First Tag',
|
||||||
@@ -123,9 +129,48 @@ export const DOWNLOAD_PATH_TEMPLATES = {
|
|||||||
label: 'Base Model + First Tag',
|
label: 'Base Model + First Tag',
|
||||||
description: 'Organize by base model and primary tag',
|
description: 'Organize by base model and primary tag',
|
||||||
example: 'Flux.1 D/style/model-name.safetensors'
|
example: 'Flux.1 D/style/model-name.safetensors'
|
||||||
|
},
|
||||||
|
BASE_MODEL_AUTHOR: {
|
||||||
|
value: '{base_model}/{author}',
|
||||||
|
label: 'Base Model + Author',
|
||||||
|
description: 'Organize by base model and author',
|
||||||
|
example: 'Flux.1 D/authorname/model-name.safetensors'
|
||||||
|
},
|
||||||
|
AUTHOR_TAG: {
|
||||||
|
value: '{author}/{first_tag}',
|
||||||
|
label: 'Author + First Tag',
|
||||||
|
description: 'Organize by author and primary tag',
|
||||||
|
example: 'authorname/style/model-name.safetensors'
|
||||||
|
},
|
||||||
|
CUSTOM: {
|
||||||
|
value: 'custom',
|
||||||
|
label: 'Custom Template',
|
||||||
|
description: 'Create your own path structure',
|
||||||
|
example: 'Enter custom template...'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Valid placeholders for path templates
|
||||||
|
export const PATH_TEMPLATE_PLACEHOLDERS = [
|
||||||
|
'{base_model}',
|
||||||
|
'{author}',
|
||||||
|
'{first_tag}'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Default templates for each model type
|
||||||
|
export const DEFAULT_PATH_TEMPLATES = {
|
||||||
|
lora: '{base_model}/{first_tag}',
|
||||||
|
checkpoint: '{base_model}',
|
||||||
|
embedding: '{first_tag}'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Model type labels for UI
|
||||||
|
export const MODEL_TYPE_LABELS = {
|
||||||
|
lora: 'LoRA Models',
|
||||||
|
checkpoint: 'Checkpoint Models',
|
||||||
|
embedding: 'Embedding Models'
|
||||||
|
};
|
||||||
|
|
||||||
// Base models available for path mapping (for UI selection)
|
// Base models available for path mapping (for UI selection)
|
||||||
export const MAPPABLE_BASE_MODELS = Object.values(BASE_MODELS).sort();
|
export const MAPPABLE_BASE_MODELS = Object.values(BASE_MODELS).sort();
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,57 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Layout Settings Section -->
|
||||||
|
<div class="settings-section">
|
||||||
|
<h3>Layout Settings</h3>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="setting-row">
|
||||||
|
<div class="setting-info">
|
||||||
|
<label for="displayDensity">Display Density</label>
|
||||||
|
</div>
|
||||||
|
<div class="setting-control select-control">
|
||||||
|
<select id="displayDensity" onchange="settingsManager.saveSelectSetting('displayDensity', 'display_density')">
|
||||||
|
<option value="default">Default</option>
|
||||||
|
<option value="medium">Medium</option>
|
||||||
|
<option value="compact">Compact</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="input-help">
|
||||||
|
Choose how many cards to display per row:
|
||||||
|
<ul class="list-description">
|
||||||
|
<li><strong>Default:</strong> 5 (1080p), 6 (2K), 8 (4K)</li>
|
||||||
|
<li><strong>Medium:</strong> 6 (1080p), 7 (2K), 9 (4K)</li>
|
||||||
|
<li><strong>Compact:</strong> 7 (1080p), 8 (2K), 10 (4K)</li>
|
||||||
|
</ul>
|
||||||
|
<span class="warning-text">Warning: Higher densities may cause performance issues on systems with limited resources.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Card Info Display setting -->
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="setting-row">
|
||||||
|
<div class="setting-info">
|
||||||
|
<label for="cardInfoDisplay">Card Info Display</label>
|
||||||
|
</div>
|
||||||
|
<div class="setting-control select-control">
|
||||||
|
<select id="cardInfoDisplay" onchange="settingsManager.saveSelectSetting('cardInfoDisplay', 'card_info_display')">
|
||||||
|
<option value="always">Always Visible</option>
|
||||||
|
<option value="hover">Reveal on Hover</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="input-help">
|
||||||
|
Choose when to display model information and action buttons:
|
||||||
|
<ul class="list-description">
|
||||||
|
<li><strong>Always Visible:</strong> Headers and footers are always visible</li>
|
||||||
|
<li><strong>Reveal on Hover:</strong> Headers and footers only appear when hovering over a card</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Add Folder Settings Section -->
|
<!-- Add Folder Settings Section -->
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h3>Folder Settings</h3>
|
<h3>Folder Settings</h3>
|
||||||
@@ -149,104 +200,117 @@
|
|||||||
|
|
||||||
<!-- Default Path Customization Section -->
|
<!-- Default Path Customization Section -->
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h3>Default Path Customization</h3>
|
<h3>Download Path Templates</h3>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="input-help">
|
||||||
|
Configure folder structures for different model types when downloading from Civitai.
|
||||||
|
<div class="placeholder-info">
|
||||||
|
<strong>Available placeholders:</strong>
|
||||||
|
<span class="placeholder-tag">{base_model}</span>
|
||||||
|
<span class="placeholder-tag">{author}</span>
|
||||||
|
<span class="placeholder-tag">{first_tag}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- LoRA Template Configuration -->
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<div class="setting-row">
|
<div class="setting-row">
|
||||||
<div class="setting-info">
|
<div class="setting-info">
|
||||||
<label for="downloadPathTemplate">Download Path Template</label>
|
<label for="loraTemplatePreset">LoRA</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-control select-control">
|
<div class="setting-control select-control">
|
||||||
<select id="downloadPathTemplate" onchange="settingsManager.saveSelectSetting('downloadPathTemplate', 'download_path_template')">
|
<select id="loraTemplatePreset" onchange="settingsManager.updateTemplatePreset('lora', this.value)">
|
||||||
<option value="">Flat Structure</option>
|
<option value="">Flat Structure</option>
|
||||||
<option value="{base_model}">By Base Model</option>
|
<option value="{base_model}">By Base Model</option>
|
||||||
|
<option value="{author}">By Author</option>
|
||||||
<option value="{first_tag}">By First Tag</option>
|
<option value="{first_tag}">By First Tag</option>
|
||||||
<option value="{base_model}/{first_tag}">Base Model + First Tag</option>
|
<option value="{base_model}/{first_tag}">Base Model + First Tag</option>
|
||||||
|
<option value="{base_model}/{author}">Base Model + Author</option>
|
||||||
|
<option value="{author}/{first_tag}">Author + First Tag</option>
|
||||||
|
<option value="custom">Custom Template</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-help">
|
<div class="template-custom-row" id="loraCustomRow" style="display: none;">
|
||||||
Configure path structure for default download locations
|
<input type="text" id="loraCustomTemplate" class="template-custom-input" placeholder="Enter custom template (e.g., {base_model}/{author}/{first_tag})" />
|
||||||
<ul class="list-description">
|
<div class="template-validation" id="loraValidation"></div>
|
||||||
<li><strong>Flat:</strong> All models in root folder</li>
|
|
||||||
<li><strong>Base Model:</strong> Organized by model type (e.g., Flux.1 D, SDXL)</li>
|
|
||||||
<li><strong>First Tag:</strong> Organized by primary tag (e.g., style, character)</li>
|
|
||||||
<li><strong>Base Model + Tag:</strong> Two-level organization for better structure</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="pathTemplatePreview" class="template-preview"></div>
|
<div class="template-preview" id="loraPreview"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Checkpoint Template Configuration -->
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<div class="setting-row">
|
<div class="setting-row">
|
||||||
<div class="setting-info">
|
<div class="setting-info">
|
||||||
<label>Base Model Path Mappings</label>
|
<label for="checkpointTemplatePreset">Checkpoint</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-control">
|
<div class="setting-control select-control">
|
||||||
<button type="button" class="add-mapping-btn" onclick="settingsManager.addMappingRow()">
|
<select id="checkpointTemplatePreset" onchange="settingsManager.updateTemplatePreset('checkpoint', this.value)">
|
||||||
<i class="fas fa-plus"></i>
|
<option value="">Flat Structure</option>
|
||||||
<span>Add Mapping</span>
|
<option value="{base_model}">By Base Model</option>
|
||||||
</button>
|
<option value="{author}">By Author</option>
|
||||||
|
<option value="{first_tag}">By First Tag</option>
|
||||||
|
<option value="{base_model}/{first_tag}">Base Model + First Tag</option>
|
||||||
|
<option value="{base_model}/{author}">Base Model + Author</option>
|
||||||
|
<option value="{author}/{first_tag}">Author + First Tag</option>
|
||||||
|
<option value="custom">Custom Template</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-help">
|
<div class="template-custom-row" id="checkpointCustomRow" style="display: none;">
|
||||||
Customize folder names for specific base models (e.g., "Flux.1 D" → "flux")
|
<input type="text" id="checkpointCustomTemplate" class="template-custom-input" placeholder="Enter custom template (e.g., {base_model}/{author}/{first_tag})" />
|
||||||
|
<div class="template-validation" id="checkpointValidation"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mappings-container">
|
<div class="template-preview" id="checkpointPreview"></div>
|
||||||
<div id="baseModelMappingsContainer">
|
</div>
|
||||||
<!-- Mapping rows will be added dynamically -->
|
|
||||||
|
<!-- Embedding Template Configuration -->
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="setting-row">
|
||||||
|
<div class="setting-info">
|
||||||
|
<label for="embeddingTemplatePreset">Embedding</label>
|
||||||
|
</div>
|
||||||
|
<div class="setting-control select-control">
|
||||||
|
<select id="embeddingTemplatePreset" onchange="settingsManager.updateTemplatePreset('embedding', this.value)">
|
||||||
|
<option value="">Flat Structure</option>
|
||||||
|
<option value="{base_model}">By Base Model</option>
|
||||||
|
<option value="{author}">By Author</option>
|
||||||
|
<option value="{first_tag}">By First Tag</option>
|
||||||
|
<option value="{base_model}/{first_tag}">Base Model + First Tag</option>
|
||||||
|
<option value="{base_model}/{author}">Base Model + Author</option>
|
||||||
|
<option value="{author}/{first_tag}">Author + First Tag</option>
|
||||||
|
<option value="custom">Custom Template</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="template-custom-row" id="embeddingCustomRow" style="display: none;">
|
||||||
|
<input type="text" id="embeddingCustomTemplate" class="template-custom-input" placeholder="Enter custom template (e.g., {base_model}/{author}/{first_tag})" />
|
||||||
|
<div class="template-validation" id="embeddingValidation"></div>
|
||||||
|
</div>
|
||||||
|
<div class="template-preview" id="embeddingPreview"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Add Layout Settings Section -->
|
<div class="setting-item">
|
||||||
<div class="settings-section">
|
<div class="setting-row">
|
||||||
<h3>Layout Settings</h3>
|
<div class="setting-info">
|
||||||
|
<label>Base Model Path Mappings</label>
|
||||||
<div class="setting-item">
|
|
||||||
<div class="setting-row">
|
|
||||||
<div class="setting-info">
|
|
||||||
<label for="displayDensity">Display Density</label>
|
|
||||||
</div>
|
|
||||||
<div class="setting-control select-control">
|
|
||||||
<select id="displayDensity" onchange="settingsManager.saveSelectSetting('displayDensity', 'display_density')">
|
|
||||||
<option value="default">Default</option>
|
|
||||||
<option value="medium">Medium</option>
|
|
||||||
<option value="compact">Compact</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="input-help">
|
<div class="setting-control">
|
||||||
Choose how many cards to display per row:
|
<button type="button" class="add-mapping-btn" onclick="settingsManager.addMappingRow()">
|
||||||
<ul class="list-description">
|
<i class="fas fa-plus"></i>
|
||||||
<li><strong>Default:</strong> 5 (1080p), 6 (2K), 8 (4K)</li>
|
<span>Add Mapping</span>
|
||||||
<li><strong>Medium:</strong> 6 (1080p), 7 (2K), 9 (4K)</li>
|
</button>
|
||||||
<li><strong>Compact:</strong> 7 (1080p), 8 (2K), 10 (4K)</li>
|
|
||||||
</ul>
|
|
||||||
<span class="warning-text">Warning: Higher densities may cause performance issues on systems with limited resources.</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="input-help">
|
||||||
<!-- Add Card Info Display setting -->
|
Customize folder names for specific base models (e.g., "Flux.1 D" → "flux")
|
||||||
<div class="setting-item">
|
</div>
|
||||||
<div class="setting-row">
|
<div class="mappings-container">
|
||||||
<div class="setting-info">
|
<div id="baseModelMappingsContainer">
|
||||||
<label for="cardInfoDisplay">Card Info Display</label>
|
<!-- Mapping rows will be added dynamically -->
|
||||||
</div>
|
|
||||||
<div class="setting-control select-control">
|
|
||||||
<select id="cardInfoDisplay" onchange="settingsManager.saveSelectSetting('cardInfoDisplay', 'card_info_display')">
|
|
||||||
<option value="always">Always Visible</option>
|
|
||||||
<option value="hover">Reveal on Hover</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="input-help">
|
|
||||||
Choose when to display model information and action buttons:
|
|
||||||
<ul class="list-description">
|
|
||||||
<li><strong>Always Visible:</strong> Headers and footers are always visible</li>
|
|
||||||
<li><strong>Reveal on Hover:</strong> Headers and footers only appear when hovering over a card</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user