mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -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")
|
||||
|
||||
# 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:
|
||||
value = json.loads(value)
|
||||
except json.JSONDecodeError:
|
||||
|
||||
@@ -482,4 +482,99 @@ input:checked + .toggle-slider:before {
|
||||
[data-theme="dark"] .base-model-select option {
|
||||
background-color: #2d2d2d;
|
||||
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 { resetAndReload } from '../api/modelApiFactory.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 {
|
||||
constructor() {
|
||||
@@ -73,11 +73,30 @@ 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;
|
||||
// Migrate legacy download_path_template to new structure
|
||||
if (state.global.settings.download_path_template && !state.global.settings.download_path_templates) {
|
||||
const legacyTemplate = state.global.settings.download_path_template;
|
||||
state.global.settings.download_path_templates = {
|
||||
lora: legacyTemplate,
|
||||
checkpoint: legacyTemplate,
|
||||
embedding: legacyTemplate
|
||||
};
|
||||
delete state.global.settings.download_path_template;
|
||||
setStorageItem('settings', state.global.settings);
|
||||
}
|
||||
|
||||
// Set default for download path templates if undefined
|
||||
if (state.global.settings.download_path_templates === undefined) {
|
||||
state.global.settings.download_path_templates = { ...DEFAULT_PATH_TEMPLATES };
|
||||
}
|
||||
|
||||
// Ensure all model types have templates
|
||||
Object.keys(DEFAULT_PATH_TEMPLATES).forEach(modelType => {
|
||||
if (!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
|
||||
if (state.global.settings.base_model_path_mappings === undefined) {
|
||||
state.global.settings.base_model_path_mappings = {};
|
||||
@@ -105,7 +124,7 @@ export class SettingsManager {
|
||||
'default_checkpoint_root',
|
||||
'default_embedding_root',
|
||||
'base_model_path_mappings',
|
||||
'download_path_template'
|
||||
'download_path_templates'
|
||||
];
|
||||
|
||||
// Build payload for syncing
|
||||
@@ -113,7 +132,7 @@ export class SettingsManager {
|
||||
|
||||
fieldsToSync.forEach(key => {
|
||||
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]);
|
||||
} else {
|
||||
payload[key] = localSettings[key];
|
||||
@@ -164,6 +183,30 @@ export class SettingsManager {
|
||||
document.querySelectorAll('.toggle-visibility').forEach(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;
|
||||
}
|
||||
@@ -211,12 +254,8 @@ export class SettingsManager {
|
||||
autoDownloadExampleImagesCheckbox.checked = state.global.settings.autoDownloadExampleImages || false;
|
||||
}
|
||||
|
||||
// Set download path template setting
|
||||
const downloadPathTemplateSelect = document.getElementById('downloadPathTemplate');
|
||||
if (downloadPathTemplateSelect) {
|
||||
downloadPathTemplateSelect.value = state.global.settings.download_path_template || '';
|
||||
this.updatePathTemplatePreview();
|
||||
}
|
||||
// Load download path templates
|
||||
this.loadDownloadPathTemplates();
|
||||
|
||||
// Set include trigger words setting
|
||||
const includeTriggerWordsCheckbox = document.getElementById('includeTriggerWords');
|
||||
@@ -529,19 +568,184 @@ export class SettingsManager {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
loadDownloadPathTemplates() {
|
||||
const templates = state.global.settings.download_path_templates || DEFAULT_PATH_TEMPLATES;
|
||||
|
||||
if (templateInfo) {
|
||||
previewElement.textContent = templateInfo.example;
|
||||
previewElement.style.display = 'block';
|
||||
Object.keys(templates).forEach(modelType => {
|
||||
this.loadTemplateForModelType(modelType, templates[modelType]);
|
||||
});
|
||||
}
|
||||
|
||||
loadTemplateForModelType(modelType, template) {
|
||||
const presetSelect = document.getElementById(`${modelType}TemplatePreset`);
|
||||
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 {
|
||||
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');
|
||||
} 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;
|
||||
@@ -664,9 +865,13 @@ export class SettingsManager {
|
||||
|
||||
try {
|
||||
// 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 = {};
|
||||
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', {
|
||||
method: 'POST',
|
||||
|
||||
@@ -112,6 +112,12 @@ export const DOWNLOAD_PATH_TEMPLATES = {
|
||||
description: 'Organize by base model type',
|
||||
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: {
|
||||
value: '{first_tag}',
|
||||
label: 'By First Tag',
|
||||
@@ -123,9 +129,48 @@ export const DOWNLOAD_PATH_TEMPLATES = {
|
||||
label: 'Base Model + First Tag',
|
||||
description: 'Organize by base model and primary tag',
|
||||
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)
|
||||
export const MAPPABLE_BASE_MODELS = Object.values(BASE_MODELS).sort();
|
||||
|
||||
|
||||
@@ -90,6 +90,57 @@
|
||||
</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 -->
|
||||
<div class="settings-section">
|
||||
@@ -149,108 +200,121 @@
|
||||
|
||||
<!-- Default Path Customization Section -->
|
||||
<div class="settings-section">
|
||||
<h3>Default Path Customization</h3>
|
||||
<h3>Download Path Templates</h3>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<label for="downloadPathTemplate">Download Path Template</label>
|
||||
</div>
|
||||
<div class="setting-control select-control">
|
||||
<select id="downloadPathTemplate" onchange="settingsManager.saveSelectSetting('downloadPathTemplate', 'download_path_template')">
|
||||
<option value="">Flat Structure</option>
|
||||
<option value="{base_model}">By Base Model</option>
|
||||
<option value="{first_tag}">By First Tag</option>
|
||||
<option value="{base_model}/{first_tag}">Base Model + First Tag</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-help">
|
||||
Configure path structure for default download locations
|
||||
<ul class="list-description">
|
||||
<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>
|
||||
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 id="pathTemplatePreview" class="template-preview"></div>
|
||||
</div>
|
||||
|
||||
<!-- LoRA Template Configuration -->
|
||||
<div class="setting-item">
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<label>Base Model Path Mappings</label>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<button type="button" class="add-mapping-btn" onclick="settingsManager.addMappingRow()">
|
||||
<i class="fas fa-plus"></i>
|
||||
<span>Add Mapping</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-help">
|
||||
Customize folder names for specific base models (e.g., "Flux.1 D" → "flux")
|
||||
</div>
|
||||
<div class="mappings-container">
|
||||
<div id="baseModelMappingsContainer">
|
||||
<!-- Mapping rows will be added dynamically -->
|
||||
</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>
|
||||
<label for="loraTemplatePreset">LoRA</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 id="loraTemplatePreset" onchange="settingsManager.updateTemplatePreset('lora', 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 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 class="template-custom-row" id="loraCustomRow" style="display: none;">
|
||||
<input type="text" id="loraCustomTemplate" class="template-custom-input" placeholder="Enter custom template (e.g., {base_model}/{author}/{first_tag})" />
|
||||
<div class="template-validation" id="loraValidation"></div>
|
||||
</div>
|
||||
<div class="template-preview" id="loraPreview"></div>
|
||||
</div>
|
||||
|
||||
<!-- Add Card Info Display setting -->
|
||||
|
||||
<!-- Checkpoint Template Configuration -->
|
||||
<div class="setting-item">
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<label for="cardInfoDisplay">Card Info Display</label>
|
||||
<label for="checkpointTemplatePreset">Checkpoint</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 id="checkpointTemplatePreset" onchange="settingsManager.updateTemplatePreset('checkpoint', 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 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 class="template-custom-row" id="checkpointCustomRow" style="display: none;">
|
||||
<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 class="template-preview" id="checkpointPreview"></div>
|
||||
</div>
|
||||
|
||||
<!-- 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 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 class="setting-item">
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<label>Base Model Path Mappings</label>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<button type="button" class="add-mapping-btn" onclick="settingsManager.addMappingRow()">
|
||||
<i class="fas fa-plus"></i>
|
||||
<span>Add Mapping</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-help">
|
||||
Customize folder names for specific base models (e.g., "Flux.1 D" → "flux")
|
||||
</div>
|
||||
<div class="mappings-container">
|
||||
<div id="baseModelMappingsContainer">
|
||||
<!-- Mapping rows will be added dynamically -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Add Example Images Settings Section -->
|
||||
<div class="settings-section">
|
||||
<h3>Example Images</h3>
|
||||
|
||||
Reference in New Issue
Block a user