mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-23 14:12:11 -03:00
Merge branch 'main' into main
This commit is contained in:
@@ -101,7 +101,7 @@
|
||||
.api-key-input input {
|
||||
width: 100%;
|
||||
padding: 6px 40px 6px 10px; /* Add left padding */
|
||||
height: 32px;
|
||||
height: 20px;
|
||||
border-radius: var(--border-radius-xs);
|
||||
border: 1px solid var(--border-color);
|
||||
background-color: var(--lora-surface);
|
||||
@@ -123,6 +123,36 @@
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Text input wrapper styles for consistent input styling */
|
||||
.text-input-wrapper {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.text-input-wrapper input {
|
||||
width: 100%;
|
||||
padding: 6px 10px;
|
||||
height: 20px;
|
||||
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;
|
||||
}
|
||||
|
||||
.text-input-wrapper 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);
|
||||
}
|
||||
|
||||
/* Dark theme specific adjustments */
|
||||
[data-theme="dark"] .text-input-wrapper input {
|
||||
background-color: rgba(30, 30, 30, 0.9);
|
||||
}
|
||||
|
||||
.input-help {
|
||||
font-size: 0.85em;
|
||||
color: var(--text-color);
|
||||
@@ -312,7 +342,7 @@ input:checked + .toggle-slider:before {
|
||||
border-radius: var(--border-radius-xs);
|
||||
border: 1px solid var(--border-color);
|
||||
background-color: var(--lora-surface);
|
||||
color: var (--text-color);
|
||||
color: var(--text-color);
|
||||
font-size: 0.95em;
|
||||
height: 32px;
|
||||
}
|
||||
@@ -346,7 +376,7 @@ input:checked + .toggle-slider:before {
|
||||
padding: var(--space-1);
|
||||
margin-top: 8px;
|
||||
font-family: monospace;
|
||||
font-size: 1.1em;
|
||||
font-size: 0.9em;
|
||||
color: var(--lora-accent);
|
||||
display: none;
|
||||
}
|
||||
@@ -571,10 +601,31 @@ input:checked + .toggle-slider:before {
|
||||
background-color: rgba(30, 30, 30, 0.9);
|
||||
}
|
||||
|
||||
/* Proxy Settings Styles */
|
||||
.proxy-settings-group {
|
||||
margin-left: var(--space-1);
|
||||
padding-left: var(--space-1);
|
||||
border-left: 2px solid var(--lora-border);
|
||||
animation: slideDown 0.3s ease-out;
|
||||
}
|
||||
|
||||
.proxy-settings-group .setting-item {
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.placeholder-info {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.proxy-settings-group {
|
||||
margin-left: 0;
|
||||
padding-left: var(--space-1);
|
||||
border-left: none;
|
||||
border-top: 1px solid var(--lora-border);
|
||||
padding-top: var(--space-2);
|
||||
margin-top: var(--space-2);
|
||||
}
|
||||
}
|
||||
@@ -233,7 +233,7 @@
|
||||
}
|
||||
|
||||
.sidebar-tree-children.expanded {
|
||||
max-height: 9999px;
|
||||
max-height: 50000px;
|
||||
}
|
||||
|
||||
.sidebar-tree-children .sidebar-tree-node-content {
|
||||
|
||||
@@ -55,48 +55,48 @@ export function getApiEndpoints(modelType) {
|
||||
|
||||
return {
|
||||
// Base CRUD operations
|
||||
list: `/api/${modelType}/list`,
|
||||
delete: `/api/${modelType}/delete`,
|
||||
exclude: `/api/${modelType}/exclude`,
|
||||
rename: `/api/${modelType}/rename`,
|
||||
save: `/api/${modelType}/save-metadata`,
|
||||
list: `/api/lm/${modelType}/list`,
|
||||
delete: `/api/lm/${modelType}/delete`,
|
||||
exclude: `/api/lm/${modelType}/exclude`,
|
||||
rename: `/api/lm/${modelType}/rename`,
|
||||
save: `/api/lm/${modelType}/save-metadata`,
|
||||
|
||||
// Bulk operations
|
||||
bulkDelete: `/api/${modelType}/bulk-delete`,
|
||||
bulkDelete: `/api/lm/${modelType}/bulk-delete`,
|
||||
|
||||
// Tag operations
|
||||
addTags: `/api/${modelType}/add-tags`,
|
||||
addTags: `/api/lm/${modelType}/add-tags`,
|
||||
|
||||
// Move operations (now common for all model types that support move)
|
||||
moveModel: `/api/${modelType}/move_model`,
|
||||
moveBulk: `/api/${modelType}/move_models_bulk`,
|
||||
moveModel: `/api/lm/${modelType}/move_model`,
|
||||
moveBulk: `/api/lm/${modelType}/move_models_bulk`,
|
||||
|
||||
// CivitAI integration
|
||||
fetchCivitai: `/api/${modelType}/fetch-civitai`,
|
||||
fetchAllCivitai: `/api/${modelType}/fetch-all-civitai`,
|
||||
relinkCivitai: `/api/${modelType}/relink-civitai`,
|
||||
civitaiVersions: `/api/${modelType}/civitai/versions`,
|
||||
fetchCivitai: `/api/lm/${modelType}/fetch-civitai`,
|
||||
fetchAllCivitai: `/api/lm/${modelType}/fetch-all-civitai`,
|
||||
relinkCivitai: `/api/lm/${modelType}/relink-civitai`,
|
||||
civitaiVersions: `/api/lm/${modelType}/civitai/versions`,
|
||||
|
||||
// Preview management
|
||||
replacePreview: `/api/${modelType}/replace-preview`,
|
||||
replacePreview: `/api/lm/${modelType}/replace-preview`,
|
||||
|
||||
// Query operations
|
||||
scan: `/api/${modelType}/scan`,
|
||||
topTags: `/api/${modelType}/top-tags`,
|
||||
baseModels: `/api/${modelType}/base-models`,
|
||||
roots: `/api/${modelType}/roots`,
|
||||
folders: `/api/${modelType}/folders`,
|
||||
folderTree: `/api/${modelType}/folder-tree`,
|
||||
unifiedFolderTree: `/api/${modelType}/unified-folder-tree`,
|
||||
duplicates: `/api/${modelType}/find-duplicates`,
|
||||
conflicts: `/api/${modelType}/find-filename-conflicts`,
|
||||
verify: `/api/${modelType}/verify-duplicates`,
|
||||
metadata: `/api/${modelType}/metadata`,
|
||||
modelDescription: `/api/${modelType}/model-description`,
|
||||
scan: `/api/lm/${modelType}/scan`,
|
||||
topTags: `/api/lm/${modelType}/top-tags`,
|
||||
baseModels: `/api/lm/${modelType}/base-models`,
|
||||
roots: `/api/lm/${modelType}/roots`,
|
||||
folders: `/api/lm/${modelType}/folders`,
|
||||
folderTree: `/api/lm/${modelType}/folder-tree`,
|
||||
unifiedFolderTree: `/api/lm/${modelType}/unified-folder-tree`,
|
||||
duplicates: `/api/lm/${modelType}/find-duplicates`,
|
||||
conflicts: `/api/lm/${modelType}/find-filename-conflicts`,
|
||||
verify: `/api/lm/${modelType}/verify-duplicates`,
|
||||
metadata: `/api/lm/${modelType}/metadata`,
|
||||
modelDescription: `/api/lm/${modelType}/model-description`,
|
||||
|
||||
// Auto-organize operations
|
||||
autoOrganize: `/api/${modelType}/auto-organize`,
|
||||
autoOrganizeProgress: `/api/${modelType}/auto-organize-progress`,
|
||||
autoOrganize: `/api/lm/${modelType}/auto-organize`,
|
||||
autoOrganizeProgress: `/api/lm/${modelType}/auto-organize-progress`,
|
||||
|
||||
// Model-specific endpoints (will be merged with specific configs)
|
||||
specific: {}
|
||||
@@ -108,24 +108,24 @@ export function getApiEndpoints(modelType) {
|
||||
*/
|
||||
export const MODEL_SPECIFIC_ENDPOINTS = {
|
||||
[MODEL_TYPES.LORA]: {
|
||||
letterCounts: `/api/${MODEL_TYPES.LORA}/letter-counts`,
|
||||
notes: `/api/${MODEL_TYPES.LORA}/get-notes`,
|
||||
triggerWords: `/api/${MODEL_TYPES.LORA}/get-trigger-words`,
|
||||
previewUrl: `/api/${MODEL_TYPES.LORA}/preview-url`,
|
||||
civitaiUrl: `/api/${MODEL_TYPES.LORA}/civitai-url`,
|
||||
metadata: `/api/${MODEL_TYPES.LORA}/metadata`,
|
||||
getTriggerWordsPost: `/api/${MODEL_TYPES.LORA}/get_trigger_words`,
|
||||
civitaiModelByVersion: `/api/${MODEL_TYPES.LORA}/civitai/model/version`,
|
||||
civitaiModelByHash: `/api/${MODEL_TYPES.LORA}/civitai/model/hash`,
|
||||
letterCounts: `/api/lm/${MODEL_TYPES.LORA}/letter-counts`,
|
||||
notes: `/api/lm/${MODEL_TYPES.LORA}/get-notes`,
|
||||
triggerWords: `/api/lm/${MODEL_TYPES.LORA}/get-trigger-words`,
|
||||
previewUrl: `/api/lm/${MODEL_TYPES.LORA}/preview-url`,
|
||||
civitaiUrl: `/api/lm/${MODEL_TYPES.LORA}/civitai-url`,
|
||||
metadata: `/api/lm/${MODEL_TYPES.LORA}/metadata`,
|
||||
getTriggerWordsPost: `/api/lm/${MODEL_TYPES.LORA}/get_trigger_words`,
|
||||
civitaiModelByVersion: `/api/lm/${MODEL_TYPES.LORA}/civitai/model/version`,
|
||||
civitaiModelByHash: `/api/lm/${MODEL_TYPES.LORA}/civitai/model/hash`,
|
||||
},
|
||||
[MODEL_TYPES.CHECKPOINT]: {
|
||||
info: `/api/${MODEL_TYPES.CHECKPOINT}/info`,
|
||||
checkpoints_roots: `/api/${MODEL_TYPES.CHECKPOINT}/checkpoints_roots`,
|
||||
unet_roots: `/api/${MODEL_TYPES.CHECKPOINT}/unet_roots`,
|
||||
metadata: `/api/${MODEL_TYPES.CHECKPOINT}/metadata`,
|
||||
info: `/api/lm/${MODEL_TYPES.CHECKPOINT}/info`,
|
||||
checkpoints_roots: `/api/lm/${MODEL_TYPES.CHECKPOINT}/checkpoints_roots`,
|
||||
unet_roots: `/api/lm/${MODEL_TYPES.CHECKPOINT}/unet_roots`,
|
||||
metadata: `/api/lm/${MODEL_TYPES.CHECKPOINT}/metadata`,
|
||||
},
|
||||
[MODEL_TYPES.EMBEDDING]: {
|
||||
metadata: `/api/${MODEL_TYPES.EMBEDDING}/metadata`,
|
||||
metadata: `/api/lm/${MODEL_TYPES.EMBEDDING}/metadata`,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -173,11 +173,11 @@ export function getCurrentModelType(explicitType = null) {
|
||||
|
||||
// Download API endpoints (shared across all model types)
|
||||
export const DOWNLOAD_ENDPOINTS = {
|
||||
download: '/api/download-model',
|
||||
downloadGet: '/api/download-model-get',
|
||||
cancelGet: '/api/cancel-download-get',
|
||||
progress: '/api/download-progress',
|
||||
exampleImages: '/api/force-download-example-images' // New endpoint for downloading example images
|
||||
download: '/api/lm/download-model',
|
||||
downloadGet: '/api/lm/download-model-get',
|
||||
cancelGet: '/api/lm/cancel-download-get',
|
||||
progress: '/api/lm/download-progress',
|
||||
exampleImages: '/api/lm/force-download-example-images' // New endpoint for downloading example images
|
||||
};
|
||||
|
||||
// WebSocket endpoints
|
||||
|
||||
@@ -538,13 +538,13 @@ export class BaseModelApiClient {
|
||||
completionMessage = translate('toast.api.bulkMetadataCompletePartial', { success: successCount, total: totalItems, type: this.apiConfig.config.displayName }, `Refreshed ${successCount} of ${totalItems} ${this.apiConfig.config.displayName}s`);
|
||||
showToast('toast.api.bulkMetadataCompletePartial', { success: successCount, total: totalItems, type: this.apiConfig.config.displayName }, 'warning');
|
||||
|
||||
if (failedItems.length > 0) {
|
||||
const failureMessage = failedItems.length <= 3
|
||||
? failedItems.map(item => `${item.fileName}: ${item.error}`).join('\n')
|
||||
: failedItems.slice(0, 3).map(item => `${item.fileName}: ${item.error}`).join('\n') +
|
||||
`\n(and ${failedItems.length - 3} more)`;
|
||||
showToast('toast.api.bulkMetadataFailureDetails', { failures: failureMessage }, 'warning', 6000);
|
||||
}
|
||||
// if (failedItems.length > 0) {
|
||||
// const failureMessage = failedItems.length <= 3
|
||||
// ? failedItems.map(item => `${item.fileName}: ${item.error}`).join('\n')
|
||||
// : failedItems.slice(0, 3).map(item => `${item.fileName}: ${item.error}`).join('\n') +
|
||||
// `\n(and ${failedItems.length - 3} more)`;
|
||||
// showToast('toast.api.bulkMetadataFailureDetails', { failures: failureMessage }, 'warning', 6000);
|
||||
// }
|
||||
} else {
|
||||
completionMessage = translate('toast.api.bulkMetadataCompleteNone', { type: this.apiConfig.config.displayName }, `Failed to refresh metadata for any ${this.apiConfig.config.displayName}s`);
|
||||
showToast('toast.api.bulkMetadataCompleteNone', { type: this.apiConfig.config.displayName }, 'error');
|
||||
@@ -938,14 +938,14 @@ export class BaseModelApiClient {
|
||||
ws.onerror = reject;
|
||||
});
|
||||
|
||||
// Get the output directory from storage
|
||||
const outputDir = getStorageItem('example_images_path', '');
|
||||
// Get the output directory from state
|
||||
const outputDir = state.global?.settings?.example_images_path || '';
|
||||
if (!outputDir) {
|
||||
throw new Error('Please set the example images path in the settings first.');
|
||||
}
|
||||
|
||||
// Determine optimize setting
|
||||
const optimize = state.global?.settings?.optimizeExampleImages ?? true;
|
||||
const optimize = state.global?.settings?.optimize_example_images ?? true;
|
||||
|
||||
// Make the API request to start the download process
|
||||
const response = await fetch(DOWNLOAD_ENDPOINTS.exampleImages, {
|
||||
|
||||
@@ -21,7 +21,7 @@ export async function fetchRecipesPage(page = 1, pageSize = 100) {
|
||||
// If we have a specific recipe ID to load
|
||||
if (pageState.customFilter?.active && pageState.customFilter?.recipeId) {
|
||||
// Special case: load specific recipe
|
||||
const response = await fetch(`/api/recipe/${pageState.customFilter.recipeId}`);
|
||||
const response = await fetch(`/api/lm/recipe/${pageState.customFilter.recipeId}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load recipe: ${response.statusText}`);
|
||||
@@ -72,7 +72,7 @@ export async function fetchRecipesPage(page = 1, pageSize = 100) {
|
||||
}
|
||||
|
||||
// Fetch recipes
|
||||
const response = await fetch(`/api/recipes?${params.toString()}`);
|
||||
const response = await fetch(`/api/lm/recipes?${params.toString()}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load recipes: ${response.statusText}`);
|
||||
@@ -207,7 +207,7 @@ export async function refreshRecipes() {
|
||||
state.loadingManager.showSimpleLoading('Refreshing recipes...');
|
||||
|
||||
// Call the API endpoint to rebuild the recipe cache
|
||||
const response = await fetch('/api/recipes/scan');
|
||||
const response = await fetch('/api/lm/recipes/scan');
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json();
|
||||
@@ -274,7 +274,7 @@ export async function updateRecipeMetadata(filePath, updates) {
|
||||
const basename = filePath.split('/').pop().split('\\').pop();
|
||||
const recipeId = basename.substring(0, basename.lastIndexOf('.'));
|
||||
|
||||
const response = await fetch(`/api/recipe/${recipeId}/update`, {
|
||||
const response = await fetch(`/api/lm/recipe/${recipeId}/update`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ModelDuplicatesManager } from './components/ModelDuplicatesManager.js';
|
||||
import { MODEL_TYPES } from './api/apiConfig.js';
|
||||
|
||||
// Initialize the Checkpoints page
|
||||
class CheckpointsPageManager {
|
||||
export class CheckpointsPageManager {
|
||||
constructor() {
|
||||
// Initialize page controls
|
||||
this.pageControls = createPageControls(MODEL_TYPES.CHECKPOINT);
|
||||
@@ -31,17 +31,21 @@ class CheckpointsPageManager {
|
||||
async initialize() {
|
||||
// Initialize common page features (including context menus)
|
||||
appCore.initializePageFeatures();
|
||||
|
||||
|
||||
console.log('Checkpoints Manager initialized');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize everything when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
export async function initializeCheckpointsPage() {
|
||||
// Initialize core application
|
||||
await appCore.initialize();
|
||||
|
||||
|
||||
// Initialize checkpoints page
|
||||
const checkpointsPage = new CheckpointsPageManager();
|
||||
await checkpointsPage.initialize();
|
||||
});
|
||||
|
||||
return checkpointsPage;
|
||||
}
|
||||
|
||||
// Initialize everything when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', initializeCheckpointsPage);
|
||||
104
static/js/components/ContextMenu/GlobalContextMenu.js
Normal file
104
static/js/components/ContextMenu/GlobalContextMenu.js
Normal file
@@ -0,0 +1,104 @@
|
||||
import { BaseContextMenu } from './BaseContextMenu.js';
|
||||
import { showToast } from '../../utils/uiHelpers.js';
|
||||
import { state } from '../../state/index.js';
|
||||
|
||||
export class GlobalContextMenu extends BaseContextMenu {
|
||||
constructor() {
|
||||
super('globalContextMenu');
|
||||
this._cleanupInProgress = false;
|
||||
}
|
||||
|
||||
showMenu(x, y, origin = null) {
|
||||
const contextOrigin = origin || { type: 'global' };
|
||||
super.showMenu(x, y, contextOrigin);
|
||||
}
|
||||
|
||||
handleMenuAction(action, menuItem) {
|
||||
switch (action) {
|
||||
case 'cleanup-example-images-folders':
|
||||
this.cleanupExampleImagesFolders(menuItem).catch((error) => {
|
||||
console.error('Failed to trigger example images cleanup:', error);
|
||||
});
|
||||
break;
|
||||
case 'download-example-images':
|
||||
this.downloadExampleImages(menuItem).catch((error) => {
|
||||
console.error('Failed to trigger example images download:', error);
|
||||
});
|
||||
break;
|
||||
default:
|
||||
console.warn(`Unhandled global context menu action: ${action}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async downloadExampleImages(menuItem) {
|
||||
const exampleImagesManager = window.exampleImagesManager;
|
||||
|
||||
if (!exampleImagesManager) {
|
||||
showToast('globalContextMenu.downloadExampleImages.unavailable', {}, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const downloadPath = state?.global?.settings?.example_images_path;
|
||||
if (!downloadPath) {
|
||||
showToast('globalContextMenu.downloadExampleImages.missingPath', {}, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
menuItem?.classList.add('disabled');
|
||||
|
||||
try {
|
||||
await exampleImagesManager.handleDownloadButton();
|
||||
} finally {
|
||||
menuItem?.classList.remove('disabled');
|
||||
}
|
||||
}
|
||||
|
||||
async cleanupExampleImagesFolders(menuItem) {
|
||||
if (this._cleanupInProgress) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._cleanupInProgress = true;
|
||||
menuItem?.classList.add('disabled');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/lm/cleanup-example-image-folders', {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
let payload;
|
||||
try {
|
||||
payload = await response.json();
|
||||
} catch (parseError) {
|
||||
payload = { error: 'Unexpected response format.' };
|
||||
}
|
||||
|
||||
if (response.ok && (payload.success || payload.partial_success)) {
|
||||
const movedTotal = payload.moved_total || 0;
|
||||
|
||||
if (movedTotal > 0) {
|
||||
showToast('globalContextMenu.cleanupExampleImages.success', { count: movedTotal }, 'success');
|
||||
} else {
|
||||
showToast('globalContextMenu.cleanupExampleImages.none', {}, 'info');
|
||||
}
|
||||
|
||||
if (payload.partial_success) {
|
||||
showToast(
|
||||
'globalContextMenu.cleanupExampleImages.partial',
|
||||
{ failures: payload.move_failures ?? 0 },
|
||||
'warning',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const message = payload?.error || 'Unknown error';
|
||||
showToast('globalContextMenu.cleanupExampleImages.error', { message }, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('globalContextMenu.cleanupExampleImages.error', { message: error.message || 'Unknown error' }, 'error');
|
||||
} finally {
|
||||
this._cleanupInProgress = false;
|
||||
menuItem?.classList.remove('disabled');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BaseContextMenu } from './BaseContextMenu.js';
|
||||
import { ModelContextMenuMixin } from './ModelContextMenuMixin.js';
|
||||
import { getModelApiClient, resetAndReload } from '../../api/modelApiFactory.js';
|
||||
import { copyLoraSyntax, sendLoraToWorkflow } from '../../utils/uiHelpers.js';
|
||||
import { copyLoraSyntax, sendLoraToWorkflow, buildLoraSyntax } from '../../utils/uiHelpers.js';
|
||||
import { showExcludeModal, showDeleteModal } from '../../utils/modalUtils.js';
|
||||
import { moveManager } from '../../managers/MoveManager.js';
|
||||
|
||||
@@ -70,9 +70,8 @@ export class LoraContextMenu extends BaseContextMenu {
|
||||
sendLoraToWorkflow(replaceMode) {
|
||||
const card = this.currentCard;
|
||||
const usageTips = JSON.parse(card.dataset.usage_tips || '{}');
|
||||
const strength = usageTips.strength || 1;
|
||||
const loraSyntax = `<lora:${card.dataset.file_name}:${strength}>`;
|
||||
|
||||
const loraSyntax = buildLoraSyntax(card.dataset.file_name, usageTips);
|
||||
|
||||
sendLoraToWorkflow(loraSyntax, replaceMode, 'lora');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,8 +125,8 @@ export const ModelContextMenuMixin = {
|
||||
state.loadingManager.showSimpleLoading('Re-linking to Civitai...');
|
||||
|
||||
const endpoint = this.modelType === 'checkpoint' ?
|
||||
'/api/checkpoints/relink-civitai' :
|
||||
'/api/loras/relink-civitai';
|
||||
'/api/lm/checkpoints/relink-civitai' :
|
||||
'/api/lm/loras/relink-civitai';
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
|
||||
@@ -103,7 +103,7 @@ export class RecipeContextMenu extends BaseContextMenu {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/api/recipe/${recipeId}/syntax`)
|
||||
fetch(`/api/lm/recipe/${recipeId}/syntax`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success && data.syntax) {
|
||||
@@ -126,7 +126,7 @@ export class RecipeContextMenu extends BaseContextMenu {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/api/recipe/${recipeId}/syntax`)
|
||||
fetch(`/api/lm/recipe/${recipeId}/syntax`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success && data.syntax) {
|
||||
@@ -149,7 +149,7 @@ export class RecipeContextMenu extends BaseContextMenu {
|
||||
}
|
||||
|
||||
// First get the recipe details to access its LoRAs
|
||||
fetch(`/api/recipe/${recipeId}`)
|
||||
fetch(`/api/lm/recipe/${recipeId}`)
|
||||
.then(response => response.json())
|
||||
.then(recipe => {
|
||||
// Clear any previous filters first
|
||||
@@ -189,7 +189,7 @@ export class RecipeContextMenu extends BaseContextMenu {
|
||||
|
||||
try {
|
||||
// First get the recipe details
|
||||
const response = await fetch(`/api/recipe/${recipeId}`);
|
||||
const response = await fetch(`/api/lm/recipe/${recipeId}`);
|
||||
const recipe = await response.json();
|
||||
|
||||
// Get missing LoRAs
|
||||
@@ -209,9 +209,9 @@ export class RecipeContextMenu extends BaseContextMenu {
|
||||
|
||||
// Determine which endpoint to use based on available data
|
||||
if (lora.modelVersionId) {
|
||||
endpoint = `/api/loras/civitai/model/version/${lora.modelVersionId}`;
|
||||
endpoint = `/api/lm/loras/civitai/model/version/${lora.modelVersionId}`;
|
||||
} else if (lora.hash) {
|
||||
endpoint = `/api/loras/civitai/model/hash/${lora.hash}`;
|
||||
endpoint = `/api/lm/loras/civitai/model/hash/${lora.hash}`;
|
||||
} else {
|
||||
console.error("Missing both hash and modelVersionId for lora:", lora);
|
||||
return null;
|
||||
|
||||
@@ -2,12 +2,14 @@ export { LoraContextMenu } from './LoraContextMenu.js';
|
||||
export { RecipeContextMenu } from './RecipeContextMenu.js';
|
||||
export { CheckpointContextMenu } from './CheckpointContextMenu.js';
|
||||
export { EmbeddingContextMenu } from './EmbeddingContextMenu.js';
|
||||
export { GlobalContextMenu } from './GlobalContextMenu.js';
|
||||
export { ModelContextMenuMixin } from './ModelContextMenuMixin.js';
|
||||
|
||||
import { LoraContextMenu } from './LoraContextMenu.js';
|
||||
import { RecipeContextMenu } from './RecipeContextMenu.js';
|
||||
import { CheckpointContextMenu } from './CheckpointContextMenu.js';
|
||||
import { EmbeddingContextMenu } from './EmbeddingContextMenu.js';
|
||||
import { GlobalContextMenu } from './GlobalContextMenu.js';
|
||||
|
||||
// Factory method to create page-specific context menu instances
|
||||
export function createPageContextMenu(pageType) {
|
||||
@@ -23,4 +25,8 @@ export function createPageContextMenu(pageType) {
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function createGlobalContextMenu() {
|
||||
return new GlobalContextMenu();
|
||||
}
|
||||
@@ -13,7 +13,7 @@ export class DuplicatesManager {
|
||||
|
||||
async findDuplicates() {
|
||||
try {
|
||||
const response = await fetch('/api/recipes/find-duplicates');
|
||||
const response = await fetch('/api/lm/recipes/find-duplicates');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to find duplicates');
|
||||
}
|
||||
@@ -354,7 +354,7 @@ export class DuplicatesManager {
|
||||
const recipeIds = Array.from(this.selectedForDeletion);
|
||||
|
||||
// Call API to bulk delete
|
||||
const response = await fetch('/api/recipes/bulk-delete', {
|
||||
const response = await fetch('/api/lm/recipes/bulk-delete', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
|
||||
@@ -48,7 +48,7 @@ export class ModelDuplicatesManager {
|
||||
// Method to check for duplicates count using existing endpoint
|
||||
async checkDuplicatesCount() {
|
||||
try {
|
||||
const endpoint = `/api/${this.modelType}/find-duplicates`;
|
||||
const endpoint = `/api/lm/${this.modelType}/find-duplicates`;
|
||||
const response = await fetch(endpoint);
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -104,7 +104,7 @@ export class ModelDuplicatesManager {
|
||||
async findDuplicates() {
|
||||
try {
|
||||
// Determine API endpoint based on model type
|
||||
const endpoint = `/api/${this.modelType}/find-duplicates`;
|
||||
const endpoint = `/api/lm/${this.modelType}/find-duplicates`;
|
||||
|
||||
const response = await fetch(endpoint);
|
||||
if (!response.ok) {
|
||||
@@ -623,7 +623,7 @@ export class ModelDuplicatesManager {
|
||||
const filePaths = Array.from(this.selectedForDeletion);
|
||||
|
||||
// Call API to bulk delete
|
||||
const response = await fetch(`/api/${this.modelType}/bulk-delete`, {
|
||||
const response = await fetch(`/api/lm/${this.modelType}/bulk-delete`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
@@ -648,7 +648,7 @@ export class ModelDuplicatesManager {
|
||||
|
||||
// Check if there are still duplicates
|
||||
try {
|
||||
const endpoint = `/api/${this.modelType}/find-duplicates`;
|
||||
const endpoint = `/api/lm/${this.modelType}/find-duplicates`;
|
||||
const dupResponse = await fetch(endpoint);
|
||||
|
||||
if (!dupResponse.ok) {
|
||||
@@ -756,7 +756,7 @@ export class ModelDuplicatesManager {
|
||||
const filePaths = group.models.map(model => model.file_path);
|
||||
|
||||
// Make API request to verify hashes
|
||||
const response = await fetch(`/api/${this.modelType}/verify-duplicates`, {
|
||||
const response = await fetch(`/api/lm/${this.modelType}/verify-duplicates`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
|
||||
@@ -46,7 +46,7 @@ class RecipeCard {
|
||||
|
||||
// NSFW blur logic - similar to LoraCard
|
||||
const nsfwLevel = this.recipe.preview_nsfw_level !== undefined ? this.recipe.preview_nsfw_level : 0;
|
||||
const shouldBlur = state.settings.blurMatureContent && nsfwLevel > NSFW_LEVELS.PG13;
|
||||
const shouldBlur = state.settings.blur_mature_content && nsfwLevel > NSFW_LEVELS.PG13;
|
||||
|
||||
if (shouldBlur) {
|
||||
card.classList.add('nsfw-content');
|
||||
@@ -203,7 +203,7 @@ class RecipeCard {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/api/recipe/${recipeId}/syntax`)
|
||||
fetch(`/api/lm/recipe/${recipeId}/syntax`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success && data.syntax) {
|
||||
@@ -299,7 +299,7 @@ class RecipeCard {
|
||||
deleteBtn.disabled = true;
|
||||
|
||||
// Call API to delete the recipe
|
||||
fetch(`/api/recipe/${recipeId}`, {
|
||||
fetch(`/api/lm/recipe/${recipeId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
@@ -341,7 +341,7 @@ class RecipeCard {
|
||||
showToast('toast.recipes.preparingForSharing', {}, 'info');
|
||||
|
||||
// Call the API to process the image with metadata
|
||||
fetch(`/api/recipe/${recipeId}/share`)
|
||||
fetch(`/api/lm/recipe/${recipeId}/share`)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to prepare recipe for sharing');
|
||||
|
||||
@@ -784,7 +784,7 @@ class RecipeModal {
|
||||
|
||||
try {
|
||||
// Fetch recipe syntax from backend
|
||||
const response = await fetch(`/api/recipe/${this.recipeId}/syntax`);
|
||||
const response = await fetch(`/api/lm/recipe/${this.recipeId}/syntax`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to get recipe syntax: ${response.statusText}`);
|
||||
@@ -830,9 +830,9 @@ class RecipeModal {
|
||||
|
||||
// Determine which endpoint to use based on available data
|
||||
if (lora.modelVersionId) {
|
||||
endpoint = `/api/loras/civitai/model/version/${lora.modelVersionId}`;
|
||||
endpoint = `/api/lm/loras/civitai/model/version/${lora.modelVersionId}`;
|
||||
} else if (lora.hash) {
|
||||
endpoint = `/api/loras/civitai/model/hash/${lora.hash}`;
|
||||
endpoint = `/api/lm/loras/civitai/model/hash/${lora.hash}`;
|
||||
} else {
|
||||
console.error("Missing both hash and modelVersionId for lora:", lora);
|
||||
return null;
|
||||
@@ -1003,7 +1003,7 @@ class RecipeModal {
|
||||
state.loadingManager.showSimpleLoading('Reconnecting LoRA...');
|
||||
|
||||
// Call API to reconnect the LoRA
|
||||
const response = await fetch('/api/recipe/lora/reconnect', {
|
||||
const response = await fetch('/api/lm/recipe/lora/reconnect', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -46,7 +46,7 @@ export class AlphabetBar {
|
||||
*/
|
||||
async fetchLetterCounts() {
|
||||
try {
|
||||
const response = await fetch('/api/loras/letter-counts');
|
||||
const response = await fetch('/api/lm/loras/letter-counts');
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch letter counts: ${response.statusText}`);
|
||||
|
||||
@@ -169,7 +169,7 @@ class InitializationManager {
|
||||
*/
|
||||
pollProgress() {
|
||||
const checkProgress = () => {
|
||||
fetch('/api/init-status')
|
||||
fetch('/api/lm/init-status')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
this.handleProgressUpdate(data);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { showToast, openCivitai, copyToClipboard, copyLoraSyntax, sendLoraToWorkflow, openExampleImagesFolder } from '../../utils/uiHelpers.js';
|
||||
import { showToast, openCivitai, copyToClipboard, copyLoraSyntax, sendLoraToWorkflow, openExampleImagesFolder, buildLoraSyntax } from '../../utils/uiHelpers.js';
|
||||
import { state, getCurrentPageState } from '../../state/index.js';
|
||||
import { showModelModal } from './ModelModal.js';
|
||||
import { toggleShowcase } from './showcase/ShowcaseView.js';
|
||||
@@ -155,8 +155,7 @@ async function toggleFavorite(card) {
|
||||
function handleSendToWorkflow(card, replaceMode, modelType) {
|
||||
if (modelType === MODEL_TYPES.LORA) {
|
||||
const usageTips = JSON.parse(card.dataset.usage_tips || '{}');
|
||||
const strength = usageTips.strength || 1;
|
||||
const loraSyntax = `<lora:${card.dataset.file_name}:${strength}>`;
|
||||
const loraSyntax = buildLoraSyntax(card.dataset.file_name, usageTips);
|
||||
sendLoraToWorkflow(loraSyntax, replaceMode, 'lora');
|
||||
} else {
|
||||
// Checkpoint send functionality - to be implemented
|
||||
@@ -186,7 +185,7 @@ async function handleExampleImagesAccess(card, modelType) {
|
||||
const modelHash = card.dataset.sha256;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/has-example-images?model_hash=${modelHash}`);
|
||||
const response = await fetch(`/api/lm/has-example-images?model_hash=${modelHash}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.has_images) {
|
||||
@@ -216,13 +215,6 @@ function handleCardClick(card, modelType) {
|
||||
}
|
||||
|
||||
async function showModelModalFromCard(card, modelType) {
|
||||
// Get the appropriate preview versions map
|
||||
const previewVersionsKey = modelType;
|
||||
const previewVersions = state.pages[previewVersionsKey]?.previewVersions || new Map();
|
||||
const version = previewVersions.get(card.dataset.filepath);
|
||||
const previewUrl = card.dataset.preview_url || '/loras_static/images/no-preview.png';
|
||||
const versionedPreviewUrl = version ? `${previewUrl}?t=${version}` : previewUrl;
|
||||
|
||||
// Create model metadata object
|
||||
const modelMeta = {
|
||||
sha256: card.dataset.sha256,
|
||||
@@ -235,7 +227,6 @@ async function showModelModalFromCard(card, modelType) {
|
||||
from_civitai: card.dataset.from_civitai === 'true',
|
||||
base_model: card.dataset.base_model,
|
||||
notes: card.dataset.notes || '',
|
||||
preview_url: versionedPreviewUrl,
|
||||
favorite: card.dataset.favorite === 'true',
|
||||
// Parse civitai metadata from the card's dataset
|
||||
civitai: JSON.parse(card.dataset.meta || '{}'),
|
||||
@@ -414,7 +405,7 @@ export function createModelCard(model, modelType) {
|
||||
card.dataset.nsfwLevel = nsfwLevel;
|
||||
|
||||
// Determine if the preview should be blurred based on NSFW level and user settings
|
||||
const shouldBlur = state.settings.blurMatureContent && nsfwLevel > NSFW_LEVELS.PG13;
|
||||
const shouldBlur = state.settings.blur_mature_content && nsfwLevel > NSFW_LEVELS.PG13;
|
||||
if (shouldBlur) {
|
||||
card.classList.add('nsfw-content');
|
||||
}
|
||||
@@ -442,9 +433,20 @@ export function createModelCard(model, modelType) {
|
||||
}
|
||||
|
||||
// Check if autoplayOnHover is enabled for video previews
|
||||
const autoplayOnHover = state.global?.settings?.autoplayOnHover || false;
|
||||
const autoplayOnHover = state.global?.settings?.autoplay_on_hover || false;
|
||||
const isVideo = previewUrl.endsWith('.mp4');
|
||||
const videoAttrs = autoplayOnHover ? 'controls muted loop' : 'controls autoplay muted loop';
|
||||
const videoAttrs = [
|
||||
'controls',
|
||||
'muted',
|
||||
'loop',
|
||||
'playsinline',
|
||||
'preload="none"',
|
||||
`data-src="${versionedPreviewUrl}"`
|
||||
];
|
||||
|
||||
if (!autoplayOnHover) {
|
||||
videoAttrs.push('data-autoplay="true"');
|
||||
}
|
||||
|
||||
// Get favorite status from model data
|
||||
const isFavorite = model.favorite === true;
|
||||
@@ -482,9 +484,7 @@ export function createModelCard(model, modelType) {
|
||||
card.innerHTML = `
|
||||
<div class="card-preview ${shouldBlur ? 'blurred' : ''}">
|
||||
${isVideo ?
|
||||
`<video ${videoAttrs} style="pointer-events: none;">
|
||||
<source src="${versionedPreviewUrl}" type="video/mp4">
|
||||
</video>` :
|
||||
`<video ${videoAttrs.join(' ')} style="pointer-events: none;"></video>` :
|
||||
`<img src="${versionedPreviewUrl}" alt="${model.model_name}">`
|
||||
}
|
||||
<div class="card-header">
|
||||
@@ -523,21 +523,155 @@ export function createModelCard(model, modelType) {
|
||||
|
||||
// Add video auto-play on hover functionality if needed
|
||||
const videoElement = card.querySelector('video');
|
||||
if (videoElement && autoplayOnHover) {
|
||||
const cardPreview = card.querySelector('.card-preview');
|
||||
|
||||
// Remove autoplay attribute and pause initially
|
||||
videoElement.removeAttribute('autoplay');
|
||||
videoElement.pause();
|
||||
|
||||
// Add mouse events to trigger play/pause using event attributes
|
||||
cardPreview.setAttribute('onmouseenter', 'this.querySelector("video")?.play()');
|
||||
cardPreview.setAttribute('onmouseleave', 'const v=this.querySelector("video"); if(v){v.pause();v.currentTime=0;}');
|
||||
if (videoElement) {
|
||||
configureModelCardVideo(videoElement, autoplayOnHover);
|
||||
}
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
const VIDEO_LAZY_ROOT_MARGIN = '200px 0px';
|
||||
let videoLazyObserver = null;
|
||||
|
||||
function ensureVideoLazyObserver() {
|
||||
if (videoLazyObserver) {
|
||||
return videoLazyObserver;
|
||||
}
|
||||
|
||||
videoLazyObserver = new IntersectionObserver((entries, observer) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const target = entry.target;
|
||||
observer.unobserve(target);
|
||||
loadVideoSource(target);
|
||||
}
|
||||
});
|
||||
}, {
|
||||
root: null,
|
||||
rootMargin: VIDEO_LAZY_ROOT_MARGIN,
|
||||
threshold: 0.01
|
||||
});
|
||||
|
||||
return videoLazyObserver;
|
||||
}
|
||||
|
||||
function cleanupHoverHandlers(videoElement) {
|
||||
const handlers = videoElement._hoverHandlers;
|
||||
if (!handlers) return;
|
||||
|
||||
const { cardPreview, mouseEnter, mouseLeave } = handlers;
|
||||
if (cardPreview) {
|
||||
cardPreview.removeEventListener('mouseenter', mouseEnter);
|
||||
cardPreview.removeEventListener('mouseleave', mouseLeave);
|
||||
}
|
||||
|
||||
delete videoElement._hoverHandlers;
|
||||
}
|
||||
|
||||
function requestSafePlay(videoElement) {
|
||||
const playPromise = videoElement.play();
|
||||
if (playPromise && typeof playPromise.catch === 'function') {
|
||||
playPromise.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
function loadVideoSource(videoElement) {
|
||||
if (!videoElement || videoElement.dataset.loaded === 'true') {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceElement = videoElement.querySelector('source');
|
||||
const dataSrc = videoElement.dataset.src || sourceElement?.dataset?.src;
|
||||
|
||||
if (!dataSrc) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure src attributes are reset before applying
|
||||
videoElement.removeAttribute('src');
|
||||
if (sourceElement) {
|
||||
sourceElement.src = dataSrc;
|
||||
} else {
|
||||
videoElement.src = dataSrc;
|
||||
}
|
||||
|
||||
videoElement.load();
|
||||
videoElement.dataset.loaded = 'true';
|
||||
|
||||
if (videoElement.dataset.autoplay === 'true') {
|
||||
videoElement.setAttribute('autoplay', '');
|
||||
requestSafePlay(videoElement);
|
||||
}
|
||||
}
|
||||
|
||||
export function configureModelCardVideo(videoElement, autoplayOnHover) {
|
||||
if (!videoElement) return;
|
||||
|
||||
cleanupHoverHandlers(videoElement);
|
||||
|
||||
const sourceElement = videoElement.querySelector('source');
|
||||
const existingSrc = videoElement.dataset.src || sourceElement?.dataset?.src || videoElement.currentSrc;
|
||||
|
||||
if (existingSrc && !videoElement.dataset.src) {
|
||||
videoElement.dataset.src = existingSrc;
|
||||
}
|
||||
|
||||
if (sourceElement && !sourceElement.dataset.src) {
|
||||
sourceElement.dataset.src = videoElement.dataset.src || sourceElement.src;
|
||||
}
|
||||
|
||||
videoElement.removeAttribute('autoplay');
|
||||
videoElement.removeAttribute('src');
|
||||
videoElement.setAttribute('preload', 'none');
|
||||
videoElement.setAttribute('muted', '');
|
||||
videoElement.setAttribute('loop', '');
|
||||
videoElement.setAttribute('playsinline', '');
|
||||
videoElement.setAttribute('controls', '');
|
||||
videoElement.dataset.loaded = 'false';
|
||||
|
||||
if (sourceElement) {
|
||||
sourceElement.removeAttribute('src');
|
||||
if (videoElement.dataset.src) {
|
||||
sourceElement.dataset.src = videoElement.dataset.src;
|
||||
}
|
||||
}
|
||||
|
||||
if (!autoplayOnHover) {
|
||||
videoElement.dataset.autoplay = 'true';
|
||||
} else {
|
||||
delete videoElement.dataset.autoplay;
|
||||
}
|
||||
|
||||
const observer = ensureVideoLazyObserver();
|
||||
observer.observe(videoElement);
|
||||
|
||||
// Pause the video until it is either hovered or autoplay kicks in
|
||||
try {
|
||||
videoElement.pause();
|
||||
} catch (err) {
|
||||
// Ignore pause errors (e.g., if not loaded yet)
|
||||
}
|
||||
|
||||
if (autoplayOnHover) {
|
||||
const cardPreview = videoElement.closest('.card-preview');
|
||||
if (cardPreview) {
|
||||
const mouseEnter = () => {
|
||||
loadVideoSource(videoElement);
|
||||
requestSafePlay(videoElement);
|
||||
};
|
||||
const mouseLeave = () => {
|
||||
videoElement.pause();
|
||||
videoElement.currentTime = 0;
|
||||
};
|
||||
|
||||
cardPreview.addEventListener('mouseenter', mouseEnter);
|
||||
cardPreview.addEventListener('mouseleave', mouseLeave);
|
||||
|
||||
videoElement._hoverHandlers = { cardPreview, mouseEnter, mouseLeave };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add a method to update card appearance based on bulk mode (LoRA only)
|
||||
export function updateCardsForBulkMode(isBulkMode) {
|
||||
// Update the state
|
||||
@@ -576,4 +710,6 @@ export function updateCardsForBulkMode(isBulkMode) {
|
||||
if (isBulkMode) {
|
||||
bulkManager.applySelectionState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -271,6 +271,7 @@ function renderLoraSpecificContent(lora, escapedWords) {
|
||||
<option value="strength_min">${translate('modals.model.usageTips.strengthMin', {}, 'Strength Min')}</option>
|
||||
<option value="strength_max">${translate('modals.model.usageTips.strengthMax', {}, 'Strength Max')}</option>
|
||||
<option value="strength">${translate('modals.model.usageTips.strength', {}, 'Strength')}</option>
|
||||
<option value="clip_strength">${translate('modals.model.usageTips.clipStrength', {}, 'Clip Strength')}</option>
|
||||
<option value="clip_skip">${translate('modals.model.usageTips.clipSkip', {}, 'Clip Skip')}</option>
|
||||
</select>
|
||||
<input type="number" id="preset-value" step="0.01" placeholder="${translate('modals.model.usageTips.valuePlaceholder', {}, 'Value')}" style="display:none;">
|
||||
@@ -460,7 +461,7 @@ async function saveNotes(filePath) {
|
||||
*/
|
||||
async function openFileLocation(filePath) {
|
||||
try {
|
||||
const resp = await fetch('/api/open-file-location', {
|
||||
const resp = await fetch('/api/lm/open-file-location', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ 'file_path': filePath })
|
||||
|
||||
@@ -22,7 +22,7 @@ export function loadRecipesForLora(loraName, sha256) {
|
||||
`;
|
||||
|
||||
// Fetch recipes that use this Lora by hash
|
||||
fetch(`/api/recipes/for-lora?hash=${encodeURIComponent(sha256.toLowerCase())}`)
|
||||
fetch(`/api/lm/recipes/for-lora?hash=${encodeURIComponent(sha256.toLowerCase())}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (!data.success) {
|
||||
@@ -166,7 +166,7 @@ function copyRecipeSyntax(recipeId) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/api/recipe/${recipeId}/syntax`)
|
||||
fetch(`/api/lm/recipe/${recipeId}/syntax`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success && data.syntax) {
|
||||
|
||||
@@ -14,7 +14,7 @@ import { getModelApiClient } from '../../api/modelApiFactory.js';
|
||||
*/
|
||||
async function fetchTrainedWords(filePath) {
|
||||
try {
|
||||
const response = await fetch(`/api/trained-words?file_path=${encodeURIComponent(filePath)}`);
|
||||
const response = await fetch(`/api/lm/trained-words?file_path=${encodeURIComponent(filePath)}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
|
||||
@@ -75,8 +75,6 @@ export function generateImageWrapper(media, heightPercent, shouldBlur, nsfwText,
|
||||
data-remote-src="${remoteUrl}"
|
||||
data-nsfw-level="${nsfwLevel}"
|
||||
alt="Preview"
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
width="${media.width}"
|
||||
height="${media.height}"
|
||||
class="lazy ${shouldBlur ? 'blurred' : ''}">
|
||||
|
||||
@@ -408,7 +408,7 @@ export function initMediaControlHandlers(container) {
|
||||
|
||||
try {
|
||||
// Call the API to delete the custom example
|
||||
const response = await fetch('/api/delete-example-image', {
|
||||
const response = await fetch('/api/lm/delete-example-image', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
|
||||
@@ -29,7 +29,7 @@ export async function loadExampleImages(images, modelHash) {
|
||||
let localFiles = [];
|
||||
|
||||
try {
|
||||
const endpoint = '/api/example-image-files';
|
||||
const endpoint = '/api/lm/example-image-files';
|
||||
const params = `model_hash=${modelHash}`;
|
||||
|
||||
const response = await fetch(`${endpoint}?${params}`);
|
||||
@@ -155,7 +155,7 @@ function renderMediaItem(img, index, exampleFiles) {
|
||||
|
||||
// Check if media should be blurred
|
||||
const nsfwLevel = img.nsfwLevel !== undefined ? img.nsfwLevel : 0;
|
||||
const shouldBlur = state.settings.blurMatureContent && nsfwLevel > NSFW_LEVELS.PG13;
|
||||
const shouldBlur = state.settings.blur_mature_content && nsfwLevel > NSFW_LEVELS.PG13;
|
||||
|
||||
// Determine NSFW warning text based on level
|
||||
let nsfwText = "Mature Content";
|
||||
@@ -191,7 +191,7 @@ function renderMediaItem(img, index, exampleFiles) {
|
||||
);
|
||||
|
||||
// Determine if this is a custom image (has id property)
|
||||
const isCustomImage = Boolean(img.id);
|
||||
const isCustomImage = Boolean(typeof img.id === 'string' && img.id);
|
||||
|
||||
// Create the media control buttons HTML
|
||||
const mediaControlsHtml = `
|
||||
@@ -235,7 +235,7 @@ function findLocalFile(img, index, exampleFiles) {
|
||||
|
||||
let localFile = null;
|
||||
|
||||
if (img.id) {
|
||||
if (typeof img.id === 'string' && img.id) {
|
||||
// This is a custom image, find by custom_<id>
|
||||
const customPrefix = `custom_${img.id}`;
|
||||
localFile = exampleFiles.find(file => file.name.startsWith(customPrefix));
|
||||
@@ -374,7 +374,7 @@ async function handleImportFiles(files, modelHash, importContainer) {
|
||||
});
|
||||
|
||||
// Call API to import files
|
||||
const response = await fetch('/api/import-example-images', {
|
||||
const response = await fetch('/api/lm/import-example-images', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
@@ -386,7 +386,7 @@ async function handleImportFiles(files, modelHash, importContainer) {
|
||||
}
|
||||
|
||||
// Get updated local files
|
||||
const updatedFilesResponse = await fetch(`/api/example-image-files?model_hash=${modelHash}`);
|
||||
const updatedFilesResponse = await fetch(`/api/lm/example-image-files?model_hash=${modelHash}`);
|
||||
const updatedFilesResult = await updatedFilesResponse.json();
|
||||
|
||||
if (!updatedFilesResult.success) {
|
||||
|
||||
@@ -7,16 +7,15 @@ import { HeaderManager } from './components/Header.js';
|
||||
import { settingsManager } from './managers/SettingsManager.js';
|
||||
import { moveManager } from './managers/MoveManager.js';
|
||||
import { bulkManager } from './managers/BulkManager.js';
|
||||
import { exampleImagesManager } from './managers/ExampleImagesManager.js';
|
||||
import { ExampleImagesManager } from './managers/ExampleImagesManager.js';
|
||||
import { helpManager } from './managers/HelpManager.js';
|
||||
import { bannerService } from './managers/BannerService.js';
|
||||
import { initTheme, initBackToTop } from './utils/uiHelpers.js';
|
||||
import { initializeInfiniteScroll } from './utils/infiniteScroll.js';
|
||||
import { migrateStorageItems } from './utils/storageHelpers.js';
|
||||
import { i18n } from './i18n/index.js';
|
||||
import { onboardingManager } from './managers/OnboardingManager.js';
|
||||
import { BulkContextMenu } from './components/ContextMenu/BulkContextMenu.js';
|
||||
import { createPageContextMenu } from './components/ContextMenu/index.js';
|
||||
import { createPageContextMenu, createGlobalContextMenu } from './components/ContextMenu/index.js';
|
||||
import { initializeEventManagement } from './utils/eventManagementInit.js';
|
||||
|
||||
// Core application class
|
||||
@@ -38,6 +37,11 @@ export class AppCore {
|
||||
|
||||
console.log(`AppCore: Language set: ${i18n.getCurrentLocale()}`);
|
||||
|
||||
// Initialize settings manager and wait for it to sync from backend
|
||||
console.log('AppCore: Initializing settings...');
|
||||
await settingsManager.waitForInitialization();
|
||||
console.log('AppCore: Settings initialized');
|
||||
|
||||
// Initialize managers
|
||||
state.loadingManager = new LoadingManager();
|
||||
modalManager.initialize();
|
||||
@@ -45,6 +49,7 @@ export class AppCore {
|
||||
bannerService.initialize();
|
||||
window.modalManager = modalManager;
|
||||
window.settingsManager = settingsManager;
|
||||
const exampleImagesManager = new ExampleImagesManager();
|
||||
window.exampleImagesManager = exampleImagesManager;
|
||||
window.helpManager = helpManager;
|
||||
window.moveManager = moveManager;
|
||||
@@ -69,7 +74,7 @@ export class AppCore {
|
||||
// Initialize the help manager
|
||||
helpManager.initialize();
|
||||
|
||||
const cardInfoDisplay = state.global.settings.cardInfoDisplay || 'always';
|
||||
const cardInfoDisplay = state.global.settings.card_info_display || 'always';
|
||||
document.body.classList.toggle('hover-reveal', cardInfoDisplay === 'hover');
|
||||
|
||||
initializeEventManagement();
|
||||
@@ -111,13 +116,12 @@ export class AppCore {
|
||||
initializeContextMenus(pageType) {
|
||||
// Create page-specific context menu
|
||||
window.pageContextMenu = createPageContextMenu(pageType);
|
||||
|
||||
if (!window.globalContextMenuInstance) {
|
||||
window.globalContextMenuInstance = createGlobalContextMenu();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Migrate localStorage items to use the namespace prefix
|
||||
migrateStorageItems();
|
||||
});
|
||||
|
||||
// Create and export a singleton instance
|
||||
export const appCore = new AppCore();
|
||||
@@ -36,12 +36,18 @@ class EmbeddingsPageManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize everything when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
async function initializeEmbeddingsPage() {
|
||||
// Initialize core application
|
||||
await appCore.initialize();
|
||||
|
||||
|
||||
// Initialize embeddings page
|
||||
const embeddingsPage = new EmbeddingsPageManager();
|
||||
await embeddingsPage.initialize();
|
||||
});
|
||||
|
||||
return embeddingsPage;
|
||||
}
|
||||
|
||||
// Initialize everything when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', initializeEmbeddingsPage);
|
||||
|
||||
export { EmbeddingsPageManager, initializeEmbeddingsPage };
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { state } from '../state/index.js';
|
||||
|
||||
/**
|
||||
* Internationalization (i18n) system for LoRA Manager
|
||||
* Uses user-selected language from settings with fallback to English
|
||||
@@ -124,26 +126,12 @@ class I18nManager {
|
||||
* @returns {string} Language code
|
||||
*/
|
||||
getLanguageFromSettings() {
|
||||
// Check localStorage for user-selected language
|
||||
const STORAGE_PREFIX = 'lora_manager_';
|
||||
let userLanguage = null;
|
||||
|
||||
try {
|
||||
const settings = localStorage.getItem(STORAGE_PREFIX + 'settings');
|
||||
if (settings) {
|
||||
const parsedSettings = JSON.parse(settings);
|
||||
userLanguage = parsedSettings.language;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse settings from localStorage:', e);
|
||||
const language = state?.global?.settings?.language;
|
||||
|
||||
if (language && this.availableLocales[language]) {
|
||||
return language;
|
||||
}
|
||||
|
||||
// If user has selected a language, use it
|
||||
if (userLanguage && this.availableLocales[userLanguage]) {
|
||||
return userLanguage;
|
||||
}
|
||||
|
||||
// Fallback to English
|
||||
|
||||
return 'en';
|
||||
}
|
||||
|
||||
@@ -166,18 +154,10 @@ class I18nManager {
|
||||
this.readyPromise = this.initializeWithLocale(languageCode);
|
||||
await this.readyPromise;
|
||||
|
||||
// Save to localStorage
|
||||
const STORAGE_PREFIX = 'lora_manager_';
|
||||
const currentSettings = localStorage.getItem(STORAGE_PREFIX + 'settings');
|
||||
let settings = {};
|
||||
|
||||
if (currentSettings) {
|
||||
settings = JSON.parse(currentSettings);
|
||||
if (state?.global?.settings) {
|
||||
state.global.settings.language = languageCode;
|
||||
}
|
||||
|
||||
settings.language = languageCode;
|
||||
localStorage.setItem(STORAGE_PREFIX + 'settings', JSON.stringify(settings));
|
||||
|
||||
console.log(`Language changed to: ${languageCode}`);
|
||||
|
||||
// Dispatch event to notify components of language change
|
||||
|
||||
@@ -6,7 +6,7 @@ import { confirmDelete, closeDeleteModal, confirmExclude, closeExcludeModal } fr
|
||||
import { ModelDuplicatesManager } from './components/ModelDuplicatesManager.js';
|
||||
|
||||
// Initialize the LoRA page
|
||||
class LoraPageManager {
|
||||
export class LoraPageManager {
|
||||
constructor() {
|
||||
// Add bulk mode to state
|
||||
state.bulkMode = false;
|
||||
@@ -38,18 +38,22 @@ class LoraPageManager {
|
||||
async initialize() {
|
||||
// Initialize cards for current bulk mode state (should be false initially)
|
||||
updateCardsForBulkMode(state.bulkMode);
|
||||
|
||||
|
||||
// Initialize common page features (including context menus and virtual scroll)
|
||||
appCore.initializePageFeatures();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize everything when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
export async function initializeLoraPage() {
|
||||
// Initialize core application
|
||||
await appCore.initialize();
|
||||
|
||||
|
||||
// Initialize page-specific functionality
|
||||
const loraPage = new LoraPageManager();
|
||||
await loraPage.initialize();
|
||||
});
|
||||
|
||||
return loraPage;
|
||||
}
|
||||
|
||||
// Initialize everything when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', initializeLoraPage);
|
||||
@@ -1,5 +1,5 @@
|
||||
import { state, getCurrentPageState } from '../state/index.js';
|
||||
import { showToast, copyToClipboard, sendLoraToWorkflow } from '../utils/uiHelpers.js';
|
||||
import { showToast, copyToClipboard, sendLoraToWorkflow, buildLoraSyntax } from '../utils/uiHelpers.js';
|
||||
import { updateCardsForBulkMode } from '../components/shared/ModelCard.js';
|
||||
import { modalManager } from './ModalManager.js';
|
||||
import { getModelApiClient, resetAndReload } from '../api/modelApiFactory.js';
|
||||
@@ -321,8 +321,7 @@ export class BulkManager {
|
||||
|
||||
if (metadata) {
|
||||
const usageTips = JSON.parse(metadata.usageTips || '{}');
|
||||
const strength = usageTips.strength || 1;
|
||||
loraSyntaxes.push(`<lora:${metadata.fileName}:${strength}>`);
|
||||
loraSyntaxes.push(buildLoraSyntax(metadata.fileName, usageTips));
|
||||
} else {
|
||||
missingLoras.push(filepath);
|
||||
}
|
||||
@@ -361,8 +360,7 @@ export class BulkManager {
|
||||
|
||||
if (metadata) {
|
||||
const usageTips = JSON.parse(metadata.usageTips || '{}');
|
||||
const strength = usageTips.strength || 1;
|
||||
loraSyntaxes.push(`<lora:${metadata.fileName}:${strength}>`);
|
||||
loraSyntaxes.push(buildLoraSyntax(metadata.fileName, usageTips));
|
||||
} else {
|
||||
missingLoras.push(filepath);
|
||||
}
|
||||
|
||||
@@ -308,7 +308,7 @@ export class DownloadManager {
|
||||
// Set default root if available
|
||||
const singularType = this.apiClient.modelType.replace(/s$/, '');
|
||||
const defaultRootKey = `default_${singularType}_root`;
|
||||
const defaultRoot = getStorageItem('settings', {})[defaultRootKey];
|
||||
const defaultRoot = state.global.settings[defaultRootKey];
|
||||
console.log(`Default root for ${this.apiClient.modelType}:`, defaultRoot);
|
||||
console.log('Available roots:', rootsData.roots);
|
||||
if (defaultRoot && rootsData.roots.includes(defaultRoot)) {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { showToast } from '../utils/uiHelpers.js';
|
||||
import { state } from '../state/index.js';
|
||||
import { getStorageItem, setStorageItem } from '../utils/storageHelpers.js';
|
||||
import { settingsManager } from './SettingsManager.js';
|
||||
|
||||
// ExampleImagesManager.js
|
||||
class ExampleImagesManager {
|
||||
export class ExampleImagesManager {
|
||||
constructor() {
|
||||
this.isDownloading = false;
|
||||
this.isPaused = false;
|
||||
@@ -27,7 +28,12 @@ class ExampleImagesManager {
|
||||
}
|
||||
|
||||
// Initialize the manager
|
||||
initialize() {
|
||||
async initialize() {
|
||||
// Wait for settings to be initialized before proceeding
|
||||
if (window.settingsManager) {
|
||||
await window.settingsManager.waitForInitialization();
|
||||
}
|
||||
|
||||
// Initialize event listeners
|
||||
this.initEventListeners();
|
||||
|
||||
@@ -57,7 +63,7 @@ class ExampleImagesManager {
|
||||
}
|
||||
|
||||
// Setup auto download if enabled
|
||||
if (state.global.settings.autoDownloadExampleImages) {
|
||||
if (state.global.settings.auto_download_example_images) {
|
||||
this.setupAutoDownload();
|
||||
}
|
||||
|
||||
@@ -78,86 +84,57 @@ class ExampleImagesManager {
|
||||
// Get custom path input element
|
||||
const pathInput = document.getElementById('exampleImagesPath');
|
||||
|
||||
// Set path from storage if available
|
||||
const savedPath = getStorageItem('example_images_path', '');
|
||||
if (savedPath) {
|
||||
// Set path from backend settings
|
||||
const savedPath = state.global.settings.example_images_path || '';
|
||||
if (pathInput) {
|
||||
pathInput.value = savedPath;
|
||||
// Enable download button if path is set
|
||||
this.updateDownloadButtonState(true);
|
||||
|
||||
// Sync the saved path with the backend
|
||||
try {
|
||||
const response = await fetch('/api/settings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
example_images_path: savedPath
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (!data.success) {
|
||||
console.error('Failed to sync example images path with backend:', data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to sync saved path with backend:', error);
|
||||
}
|
||||
} else {
|
||||
// Disable download button if no path is set
|
||||
this.updateDownloadButtonState(false);
|
||||
this.updateDownloadButtonState(!!savedPath);
|
||||
}
|
||||
|
||||
// Add event listener to validate path input
|
||||
pathInput.addEventListener('input', async () => {
|
||||
const hasPath = pathInput.value.trim() !== '';
|
||||
this.updateDownloadButtonState(hasPath);
|
||||
|
||||
// Save path to storage when changed
|
||||
if (hasPath) {
|
||||
setStorageItem('example_images_path', pathInput.value);
|
||||
|
||||
// Update path in backend settings
|
||||
if (pathInput) {
|
||||
// Save path on Enter key or blur
|
||||
const savePath = async () => {
|
||||
const hasPath = pathInput.value.trim() !== '';
|
||||
this.updateDownloadButtonState(hasPath);
|
||||
try {
|
||||
const response = await fetch('/api/settings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
example_images_path: pathInput.value
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (!data.success) {
|
||||
console.error('Failed to update example images path in backend:', data.error);
|
||||
} else {
|
||||
await settingsManager.saveSetting('example_images_path', pathInput.value);
|
||||
showToast('toast.exampleImages.pathUpdated', {}, 'success');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update example images path:', error);
|
||||
showToast('toast.exampleImages.pathUpdateFailed', { message: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Setup or clear auto download based on path availability
|
||||
if (state.global.settings.autoDownloadExampleImages) {
|
||||
if (hasPath) {
|
||||
this.setupAutoDownload();
|
||||
} else {
|
||||
this.clearAutoDownload();
|
||||
// Setup or clear auto download based on path availability
|
||||
if (state.global.settings.auto_download_example_images) {
|
||||
if (hasPath) {
|
||||
this.setupAutoDownload();
|
||||
} else {
|
||||
this.clearAutoDownload();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
let ignoreNextBlur = false;
|
||||
pathInput.addEventListener('keydown', async (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
ignoreNextBlur = true;
|
||||
await savePath();
|
||||
pathInput.blur(); // Remove focus from the input after saving
|
||||
}
|
||||
});
|
||||
pathInput.addEventListener('blur', async () => {
|
||||
if (ignoreNextBlur) {
|
||||
ignoreNextBlur = false;
|
||||
return;
|
||||
}
|
||||
await savePath();
|
||||
});
|
||||
// Still update button state on input, but don't save
|
||||
pathInput.addEventListener('input', () => {
|
||||
const hasPath = pathInput.value.trim() !== '';
|
||||
this.updateDownloadButtonState(hasPath);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize path options:', error);
|
||||
}
|
||||
@@ -193,7 +170,7 @@ class ExampleImagesManager {
|
||||
|
||||
async checkDownloadStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/example-images-status');
|
||||
const response = await fetch('/api/lm/example-images-status');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
@@ -248,22 +225,14 @@ class ExampleImagesManager {
|
||||
}
|
||||
|
||||
try {
|
||||
const outputDir = document.getElementById('exampleImagesPath').value || '';
|
||||
const optimize = state.global.settings.optimize_example_images;
|
||||
|
||||
if (!outputDir) {
|
||||
showToast('toast.exampleImages.enterLocationFirst', {}, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const optimize = document.getElementById('optimizeExampleImages').checked;
|
||||
|
||||
const response = await fetch('/api/download-example-images', {
|
||||
const response = await fetch('/api/lm/download-example-images', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
output_dir: outputDir,
|
||||
optimize: optimize,
|
||||
model_types: ['lora', 'checkpoint', 'embedding'] // Example types, adjust as needed
|
||||
})
|
||||
@@ -299,7 +268,7 @@ class ExampleImagesManager {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/pause-example-images', {
|
||||
const response = await fetch('/api/lm/pause-example-images', {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
@@ -335,7 +304,7 @@ class ExampleImagesManager {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/resume-example-images', {
|
||||
const response = await fetch('/api/lm/resume-example-images', {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
@@ -379,7 +348,7 @@ class ExampleImagesManager {
|
||||
|
||||
async updateProgress() {
|
||||
try {
|
||||
const response = await fetch('/api/example-images-status');
|
||||
const response = await fetch('/api/lm/example-images-status');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
@@ -708,13 +677,12 @@ class ExampleImagesManager {
|
||||
|
||||
canAutoDownload() {
|
||||
// Check if auto download is enabled
|
||||
if (!state.global.settings.autoDownloadExampleImages) {
|
||||
if (!state.global.settings.auto_download_example_images) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if download path is set
|
||||
const pathInput = document.getElementById('exampleImagesPath');
|
||||
if (!pathInput || !pathInput.value.trim()) {
|
||||
// Check if download path is set in settings
|
||||
if (!state.global.settings.example_images_path) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -745,16 +713,14 @@ class ExampleImagesManager {
|
||||
try {
|
||||
console.log('Performing auto download check...');
|
||||
|
||||
const outputDir = document.getElementById('exampleImagesPath').value;
|
||||
const optimize = document.getElementById('optimizeExampleImages').checked;
|
||||
const optimize = state.global.settings.optimize_example_images;
|
||||
|
||||
const response = await fetch('/api/download-example-images', {
|
||||
const response = await fetch('/api/lm/download-example-images', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
output_dir: outputDir,
|
||||
optimize: optimize,
|
||||
model_types: ['lora', 'checkpoint', 'embedding'],
|
||||
auto_mode: true // Flag to indicate this is an automatic download
|
||||
@@ -771,6 +737,3 @@ class ExampleImagesManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
export const exampleImagesManager = new ExampleImagesManager();
|
||||
|
||||
@@ -66,7 +66,7 @@ export class FilterManager {
|
||||
tagsContainer.innerHTML = '<div class="tags-loading">Loading tags...</div>';
|
||||
|
||||
// Determine the API endpoint based on the page type
|
||||
const tagsEndpoint = `/api/${this.currentPage}/top-tags?limit=20`;
|
||||
const tagsEndpoint = `/api/lm/${this.currentPage}/top-tags?limit=20`;
|
||||
|
||||
const response = await fetch(tagsEndpoint);
|
||||
if (!response.ok) throw new Error('Failed to fetch tags');
|
||||
@@ -134,7 +134,7 @@ export class FilterManager {
|
||||
if (!baseModelTagsContainer) return;
|
||||
|
||||
// Set the API endpoint based on current page
|
||||
const apiEndpoint = `/api/${this.currentPage}/base-models`;
|
||||
const apiEndpoint = `/api/lm/${this.currentPage}/base-models`;
|
||||
|
||||
// Fetch base models
|
||||
fetch(apiEndpoint)
|
||||
|
||||
@@ -228,7 +228,7 @@ export class ImportManager {
|
||||
|
||||
// Set default root if available
|
||||
const defaultRootKey = 'default_lora_root';
|
||||
const defaultRoot = getStorageItem('settings', {})[defaultRootKey];
|
||||
const defaultRoot = state.global.settings[defaultRootKey];
|
||||
if (defaultRoot && rootsData.roots.includes(defaultRoot)) {
|
||||
loraRoot.value = defaultRoot;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { showToast } from '../utils/uiHelpers.js';
|
||||
import { state, getCurrentPageState } from '../state/index.js';
|
||||
import { modalManager } from './ModalManager.js';
|
||||
import { bulkManager } from './BulkManager.js';
|
||||
import { getStorageItem } from '../utils/storageHelpers.js';
|
||||
import { getModelApiClient } from '../api/modelApiFactory.js';
|
||||
import { FolderTreeManager } from '../components/FolderTreeManager.js';
|
||||
import { sidebarManager } from '../components/SidebarManager.js';
|
||||
@@ -87,7 +86,7 @@ class MoveManager {
|
||||
|
||||
// Set default root if available
|
||||
const settingsKey = `default_${currentPageType.slice(0, -1)}_root`;
|
||||
const defaultRoot = getStorageItem('settings', {})[settingsKey];
|
||||
const defaultRoot = state.global.settings[settingsKey];
|
||||
if (defaultRoot && rootsData.roots.includes(defaultRoot)) {
|
||||
modelRootSelect.value = defaultRoot;
|
||||
}
|
||||
|
||||
@@ -182,11 +182,8 @@ export class OnboardingManager {
|
||||
// Update state
|
||||
state.global.settings.language = languageCode;
|
||||
|
||||
// Save to localStorage
|
||||
setStorageItem('settings', state.global.settings);
|
||||
|
||||
// Save to backend
|
||||
const response = await fetch('/api/settings', {
|
||||
const response = await fetch('/api/lm/settings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -1,157 +1,159 @@
|
||||
import { modalManager } from './ModalManager.js';
|
||||
import { showToast } from '../utils/uiHelpers.js';
|
||||
import { state } from '../state/index.js';
|
||||
import { state, createDefaultSettings } from '../state/index.js';
|
||||
import { resetAndReload } from '../api/modelApiFactory.js';
|
||||
import { setStorageItem, getStorageItem } from '../utils/storageHelpers.js';
|
||||
import { DOWNLOAD_PATH_TEMPLATES, MAPPABLE_BASE_MODELS, PATH_TEMPLATE_PLACEHOLDERS, DEFAULT_PATH_TEMPLATES } from '../utils/constants.js';
|
||||
import { translate } from '../utils/i18nHelpers.js';
|
||||
import { i18n } from '../i18n/index.js';
|
||||
import { configureModelCardVideo } from '../components/shared/ModelCard.js';
|
||||
|
||||
export class SettingsManager {
|
||||
constructor() {
|
||||
this.initialized = false;
|
||||
this.isOpen = false;
|
||||
this.initializationPromise = null;
|
||||
|
||||
// Add initialization to sync with modal state
|
||||
this.currentPage = document.body.dataset.page || 'loras';
|
||||
|
||||
// Ensure settings are loaded from localStorage
|
||||
this.loadSettingsFromStorage();
|
||||
|
||||
// Sync settings to backend if needed
|
||||
this.syncSettingsToBackendIfNeeded();
|
||||
this.backendSettingKeys = new Set(Object.keys(createDefaultSettings()));
|
||||
|
||||
// Start initialization but don't await here to avoid blocking constructor
|
||||
this.initializationPromise = this.initializeSettings();
|
||||
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
loadSettingsFromStorage() {
|
||||
// Get saved settings from localStorage
|
||||
const savedSettings = getStorageItem('settings');
|
||||
|
||||
// Migrate legacy default_loras_root to default_lora_root if present
|
||||
if (savedSettings && savedSettings.default_loras_root && !savedSettings.default_lora_root) {
|
||||
savedSettings.default_lora_root = savedSettings.default_loras_root;
|
||||
delete savedSettings.default_loras_root;
|
||||
setStorageItem('settings', savedSettings);
|
||||
}
|
||||
|
||||
// Apply saved settings to state if available
|
||||
if (savedSettings) {
|
||||
state.global.settings = { ...state.global.settings, ...savedSettings };
|
||||
}
|
||||
|
||||
// Initialize default values for new settings if they don't exist
|
||||
if (state.global.settings.compactMode === undefined) {
|
||||
state.global.settings.compactMode = false;
|
||||
}
|
||||
|
||||
// Set default for optimizeExampleImages if undefined
|
||||
if (state.global.settings.optimizeExampleImages === undefined) {
|
||||
state.global.settings.optimizeExampleImages = true;
|
||||
}
|
||||
|
||||
// Set default for autoDownloadExampleImages if undefined
|
||||
if (state.global.settings.autoDownloadExampleImages === undefined) {
|
||||
state.global.settings.autoDownloadExampleImages = true;
|
||||
}
|
||||
|
||||
// Set default for cardInfoDisplay if undefined
|
||||
if (state.global.settings.cardInfoDisplay === undefined) {
|
||||
state.global.settings.cardInfoDisplay = 'always';
|
||||
}
|
||||
|
||||
// Set default for defaultCheckpointRoot if undefined
|
||||
if (state.global.settings.default_checkpoint_root === undefined) {
|
||||
state.global.settings.default_checkpoint_root = '';
|
||||
}
|
||||
|
||||
// Convert old boolean compactMode to new displayDensity string
|
||||
if (typeof state.global.settings.displayDensity === 'undefined') {
|
||||
if (state.global.settings.compactMode === true) {
|
||||
state.global.settings.displayDensity = 'compact';
|
||||
} else {
|
||||
state.global.settings.displayDensity = 'default';
|
||||
}
|
||||
// We can delete the old setting, but keeping it for backwards compatibility
|
||||
}
|
||||
|
||||
// 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 (typeof state.global.settings.download_path_templates[modelType] === 'undefined') {
|
||||
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 = {};
|
||||
}
|
||||
|
||||
// Set default for defaultEmbeddingRoot if undefined
|
||||
if (state.global.settings.default_embedding_root === undefined) {
|
||||
state.global.settings.default_embedding_root = '';
|
||||
}
|
||||
|
||||
// Set default for includeTriggerWords if undefined
|
||||
if (state.global.settings.includeTriggerWords === undefined) {
|
||||
state.global.settings.includeTriggerWords = false;
|
||||
// Add method to wait for initialization to complete
|
||||
async waitForInitialization() {
|
||||
if (this.initializationPromise) {
|
||||
await this.initializationPromise;
|
||||
}
|
||||
}
|
||||
|
||||
async syncSettingsToBackendIfNeeded() {
|
||||
// Get local settings from storage
|
||||
const localSettings = getStorageItem('settings') || {};
|
||||
async initializeSettings() {
|
||||
// Reset to defaults before syncing
|
||||
state.global.settings = createDefaultSettings();
|
||||
|
||||
// Fields that need to be synced to backend
|
||||
const fieldsToSync = [
|
||||
'civitai_api_key',
|
||||
'default_lora_root',
|
||||
'default_checkpoint_root',
|
||||
'default_embedding_root',
|
||||
'base_model_path_mappings',
|
||||
'download_path_templates'
|
||||
];
|
||||
// Sync settings from backend to frontend
|
||||
await this.syncSettingsFromBackend();
|
||||
}
|
||||
|
||||
// Build payload for syncing
|
||||
const payload = {};
|
||||
|
||||
fieldsToSync.forEach(key => {
|
||||
if (localSettings[key] !== undefined) {
|
||||
payload[key] = localSettings[key];
|
||||
async syncSettingsFromBackend() {
|
||||
try {
|
||||
const response = await fetch('/api/lm/settings');
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Only send request if there is something to sync
|
||||
if (Object.keys(payload).length > 0) {
|
||||
const data = await response.json();
|
||||
if (data.success && data.settings) {
|
||||
state.global.settings = this.mergeSettingsWithDefaults(data.settings);
|
||||
console.log('Settings synced from backend');
|
||||
} else {
|
||||
console.error('Failed to sync settings from backend:', data.error);
|
||||
state.global.settings = this.mergeSettingsWithDefaults();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to sync settings from backend:', error);
|
||||
state.global.settings = this.mergeSettingsWithDefaults();
|
||||
}
|
||||
|
||||
await this.applyLanguageSetting();
|
||||
this.applyFrontendSettings();
|
||||
}
|
||||
|
||||
async applyLanguageSetting() {
|
||||
const desiredLanguage = state?.global?.settings?.language;
|
||||
|
||||
if (!desiredLanguage) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (i18n.getCurrentLocale() !== desiredLanguage) {
|
||||
await i18n.setLanguage(desiredLanguage);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to apply language from settings:', error);
|
||||
}
|
||||
}
|
||||
|
||||
mergeSettingsWithDefaults(backendSettings = {}) {
|
||||
const defaults = createDefaultSettings();
|
||||
const merged = { ...defaults, ...backendSettings };
|
||||
|
||||
const baseMappings = backendSettings?.base_model_path_mappings;
|
||||
if (baseMappings && typeof baseMappings === 'object' && !Array.isArray(baseMappings)) {
|
||||
merged.base_model_path_mappings = baseMappings;
|
||||
} else {
|
||||
merged.base_model_path_mappings = defaults.base_model_path_mappings;
|
||||
}
|
||||
|
||||
let templates = backendSettings?.download_path_templates;
|
||||
if (typeof templates === 'string') {
|
||||
try {
|
||||
await fetch('/api/settings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
// Log success to console
|
||||
console.log('Settings synced to backend');
|
||||
} catch (e) {
|
||||
// Log error to console
|
||||
console.error('Failed to sync settings to backend:', e);
|
||||
const parsed = JSON.parse(templates);
|
||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||
templates = parsed;
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.warn('Failed to parse download_path_templates string from backend, using defaults');
|
||||
templates = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!templates || typeof templates !== 'object' || Array.isArray(templates)) {
|
||||
templates = {};
|
||||
}
|
||||
|
||||
merged.download_path_templates = { ...DEFAULT_PATH_TEMPLATES, ...templates };
|
||||
|
||||
Object.keys(merged).forEach(key => this.backendSettingKeys.add(key));
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
// Helper method to determine if a setting should be saved to backend
|
||||
isBackendSetting(settingKey) {
|
||||
return this.backendSettingKeys.has(settingKey);
|
||||
}
|
||||
|
||||
// Helper method to save setting based on whether it's frontend or backend
|
||||
async saveSetting(settingKey, value) {
|
||||
// Update state
|
||||
state.global.settings[settingKey] = value;
|
||||
|
||||
if (!this.isBackendSetting(settingKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Save to backend
|
||||
try {
|
||||
const payload = {};
|
||||
payload[settingKey] = value;
|
||||
|
||||
const response = await fetch('/api/lm/settings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to save setting to backend');
|
||||
}
|
||||
|
||||
// Parse response and check for success
|
||||
const data = await response.json();
|
||||
if (data.success === false) {
|
||||
throw new Error(data.error || 'Failed to save setting to backend');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to save backend setting ${settingKey}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
initialize() {
|
||||
@@ -212,43 +214,42 @@ export class SettingsManager {
|
||||
// Set frontend settings from state
|
||||
const blurMatureContentCheckbox = document.getElementById('blurMatureContent');
|
||||
if (blurMatureContentCheckbox) {
|
||||
blurMatureContentCheckbox.checked = state.global.settings.blurMatureContent;
|
||||
blurMatureContentCheckbox.checked = state.global.settings.blur_mature_content ?? true;
|
||||
}
|
||||
|
||||
|
||||
const showOnlySFWCheckbox = document.getElementById('showOnlySFW');
|
||||
if (showOnlySFWCheckbox) {
|
||||
// Sync with state (backend will set this via template)
|
||||
state.global.settings.show_only_sfw = showOnlySFWCheckbox.checked;
|
||||
showOnlySFWCheckbox.checked = state.global.settings.show_only_sfw ?? false;
|
||||
}
|
||||
|
||||
|
||||
// Set video autoplay on hover setting
|
||||
const autoplayOnHoverCheckbox = document.getElementById('autoplayOnHover');
|
||||
if (autoplayOnHoverCheckbox) {
|
||||
autoplayOnHoverCheckbox.checked = state.global.settings.autoplayOnHover || false;
|
||||
autoplayOnHoverCheckbox.checked = state.global.settings.autoplay_on_hover || false;
|
||||
}
|
||||
|
||||
|
||||
// Set display density setting
|
||||
const displayDensitySelect = document.getElementById('displayDensity');
|
||||
if (displayDensitySelect) {
|
||||
displayDensitySelect.value = state.global.settings.displayDensity || 'default';
|
||||
displayDensitySelect.value = state.global.settings.display_density || 'default';
|
||||
}
|
||||
|
||||
|
||||
// Set card info display setting
|
||||
const cardInfoDisplaySelect = document.getElementById('cardInfoDisplay');
|
||||
if (cardInfoDisplaySelect) {
|
||||
cardInfoDisplaySelect.value = state.global.settings.cardInfoDisplay || 'always';
|
||||
cardInfoDisplaySelect.value = state.global.settings.card_info_display || 'always';
|
||||
}
|
||||
|
||||
// Set optimize example images setting
|
||||
const optimizeExampleImagesCheckbox = document.getElementById('optimizeExampleImages');
|
||||
if (optimizeExampleImagesCheckbox) {
|
||||
optimizeExampleImagesCheckbox.checked = state.global.settings.optimizeExampleImages || false;
|
||||
optimizeExampleImagesCheckbox.checked = state.global.settings.optimize_example_images ?? true;
|
||||
}
|
||||
|
||||
// Set auto download example images setting
|
||||
const autoDownloadExampleImagesCheckbox = document.getElementById('autoDownloadExampleImages');
|
||||
if (autoDownloadExampleImagesCheckbox) {
|
||||
autoDownloadExampleImagesCheckbox.checked = state.global.settings.autoDownloadExampleImages || false;
|
||||
autoDownloadExampleImagesCheckbox.checked = state.global.settings.auto_download_example_images || false;
|
||||
}
|
||||
|
||||
// Load download path templates
|
||||
@@ -257,7 +258,7 @@ export class SettingsManager {
|
||||
// Set include trigger words setting
|
||||
const includeTriggerWordsCheckbox = document.getElementById('includeTriggerWords');
|
||||
if (includeTriggerWordsCheckbox) {
|
||||
includeTriggerWordsCheckbox.checked = state.global.settings.includeTriggerWords || false;
|
||||
includeTriggerWordsCheckbox.checked = state.global.settings.include_trigger_words || false;
|
||||
}
|
||||
|
||||
// Load metadata archive settings
|
||||
@@ -281,6 +282,60 @@ export class SettingsManager {
|
||||
const currentLanguage = state.global.settings.language || 'en';
|
||||
languageSelect.value = currentLanguage;
|
||||
}
|
||||
|
||||
this.loadProxySettings();
|
||||
}
|
||||
|
||||
loadProxySettings() {
|
||||
// Load proxy enabled setting
|
||||
const proxyEnabledCheckbox = document.getElementById('proxyEnabled');
|
||||
if (proxyEnabledCheckbox) {
|
||||
proxyEnabledCheckbox.checked = state.global.settings.proxy_enabled || false;
|
||||
|
||||
// Add event listener for toggling proxy settings group visibility
|
||||
proxyEnabledCheckbox.addEventListener('change', () => {
|
||||
const proxySettingsGroup = document.getElementById('proxySettingsGroup');
|
||||
if (proxySettingsGroup) {
|
||||
proxySettingsGroup.style.display = proxyEnabledCheckbox.checked ? 'block' : 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Set initial visibility
|
||||
const proxySettingsGroup = document.getElementById('proxySettingsGroup');
|
||||
if (proxySettingsGroup) {
|
||||
proxySettingsGroup.style.display = proxyEnabledCheckbox.checked ? 'block' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Load proxy type
|
||||
const proxyTypeSelect = document.getElementById('proxyType');
|
||||
if (proxyTypeSelect) {
|
||||
proxyTypeSelect.value = state.global.settings.proxy_type || 'http';
|
||||
}
|
||||
|
||||
// Load proxy host
|
||||
const proxyHostInput = document.getElementById('proxyHost');
|
||||
if (proxyHostInput) {
|
||||
proxyHostInput.value = state.global.settings.proxy_host || '';
|
||||
}
|
||||
|
||||
// Load proxy port
|
||||
const proxyPortInput = document.getElementById('proxyPort');
|
||||
if (proxyPortInput) {
|
||||
proxyPortInput.value = state.global.settings.proxy_port || '';
|
||||
}
|
||||
|
||||
// Load proxy username
|
||||
const proxyUsernameInput = document.getElementById('proxyUsername');
|
||||
if (proxyUsernameInput) {
|
||||
proxyUsernameInput.value = state.global.settings.proxy_username || '';
|
||||
}
|
||||
|
||||
// Load proxy password
|
||||
const proxyPasswordInput = document.getElementById('proxyPassword');
|
||||
if (proxyPasswordInput) {
|
||||
proxyPasswordInput.value = state.global.settings.proxy_password || '';
|
||||
}
|
||||
}
|
||||
|
||||
async loadLoraRoots() {
|
||||
@@ -289,7 +344,7 @@ export class SettingsManager {
|
||||
if (!defaultLoraRootSelect) return;
|
||||
|
||||
// Fetch lora roots
|
||||
const response = await fetch('/api/loras/roots');
|
||||
const response = await fetch('/api/lm/loras/roots');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch LoRA roots');
|
||||
}
|
||||
@@ -328,7 +383,7 @@ export class SettingsManager {
|
||||
if (!defaultCheckpointRootSelect) return;
|
||||
|
||||
// Fetch checkpoint roots
|
||||
const response = await fetch('/api/checkpoints/roots');
|
||||
const response = await fetch('/api/lm/checkpoints/roots');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch checkpoint roots');
|
||||
}
|
||||
@@ -367,7 +422,7 @@ export class SettingsManager {
|
||||
if (!defaultEmbeddingRootSelect) return;
|
||||
|
||||
// Fetch embedding roots
|
||||
const response = await fetch('/api/embeddings/roots');
|
||||
const response = await fetch('/api/lm/embeddings/roots');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch embedding roots');
|
||||
}
|
||||
@@ -543,23 +598,8 @@ export class SettingsManager {
|
||||
|
||||
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: state.global.settings.base_model_path_mappings
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to save base model mappings');
|
||||
}
|
||||
// Save to backend using universal save method
|
||||
await this.saveSetting('base_model_path_mappings', state.global.settings.base_model_path_mappings);
|
||||
|
||||
// Show success toast
|
||||
const mappingCount = Object.keys(state.global.settings.base_model_path_mappings).length;
|
||||
@@ -733,23 +773,8 @@ export class SettingsManager {
|
||||
|
||||
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: state.global.settings.download_path_templates
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to save download path templates');
|
||||
}
|
||||
// Save to backend using universal save method
|
||||
await this.saveSetting('download_path_templates', state.global.settings.download_path_templates);
|
||||
|
||||
showToast('toast.settings.downloadTemplatesUpdated', {}, 'success');
|
||||
|
||||
@@ -773,55 +798,21 @@ export class SettingsManager {
|
||||
if (!element) return;
|
||||
|
||||
const value = element.checked;
|
||||
|
||||
// Update frontend state
|
||||
if (settingKey === 'blur_mature_content') {
|
||||
state.global.settings.blurMatureContent = value;
|
||||
} else if (settingKey === 'show_only_sfw') {
|
||||
state.global.settings.show_only_sfw = value;
|
||||
} else if (settingKey === 'autoplay_on_hover') {
|
||||
state.global.settings.autoplayOnHover = value;
|
||||
} else if (settingKey === 'optimize_example_images') {
|
||||
state.global.settings.optimizeExampleImages = value;
|
||||
} else if (settingKey === 'auto_download_example_images') {
|
||||
state.global.settings.autoDownloadExampleImages = value;
|
||||
} else if (settingKey === 'compact_mode') {
|
||||
state.global.settings.compactMode = value;
|
||||
} else if (settingKey === 'include_trigger_words') {
|
||||
state.global.settings.includeTriggerWords = value;
|
||||
} else if (settingKey === 'enable_metadata_archive_db') {
|
||||
state.global.settings.enable_metadata_archive_db = value;
|
||||
} else {
|
||||
// For any other settings that might be added in the future
|
||||
state.global.settings[settingKey] = value;
|
||||
}
|
||||
|
||||
// Save to localStorage
|
||||
setStorageItem('settings', state.global.settings);
|
||||
|
||||
try {
|
||||
// For backend settings, make API call
|
||||
if (['show_only_sfw', 'enable_metadata_archive_db'].includes(settingKey)) {
|
||||
const payload = {};
|
||||
payload[settingKey] = value;
|
||||
|
||||
const response = await fetch('/api/settings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to save setting');
|
||||
}
|
||||
|
||||
// Refresh metadata archive status when enable setting changes
|
||||
if (settingKey === 'enable_metadata_archive_db') {
|
||||
await this.updateMetadataArchiveStatus();
|
||||
try {
|
||||
await this.saveSetting(settingKey, value);
|
||||
|
||||
if (settingKey === 'proxy_enabled') {
|
||||
const proxySettingsGroup = document.getElementById('proxySettingsGroup');
|
||||
if (proxySettingsGroup) {
|
||||
proxySettingsGroup.style.display = value ? 'block' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh metadata archive status when enable setting changes
|
||||
if (settingKey === 'enable_metadata_archive_db') {
|
||||
await this.updateMetadataArchiveStatus();
|
||||
}
|
||||
|
||||
showToast('toast.settings.settingsUpdated', { setting: settingKey.replace(/_/g, ' ') }, 'success');
|
||||
|
||||
@@ -844,16 +835,11 @@ export class SettingsManager {
|
||||
// Recalculate layout when compact mode changes
|
||||
if (settingKey === 'compact_mode' && state.virtualScroller) {
|
||||
state.virtualScroller.calculateLayout();
|
||||
showToast('toast.settings.compactModeToggled', {
|
||||
state: value ? 'toast.settings.compactModeEnabled' : 'toast.settings.compactModeDisabled'
|
||||
showToast('toast.settings.compactModeToggled', {
|
||||
state: value ? 'toast.settings.compactModeEnabled' : 'toast.settings.compactModeDisabled'
|
||||
}, 'success');
|
||||
}
|
||||
|
||||
// Special handling for metadata archive settings
|
||||
if (settingKey === 'enable_metadata_archive_db') {
|
||||
await this.updateMetadataArchiveStatus();
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
showToast('toast.settings.settingSaveFailed', { message: error.message }, 'error');
|
||||
}
|
||||
@@ -865,53 +851,10 @@ export class SettingsManager {
|
||||
|
||||
const value = element.value;
|
||||
|
||||
// Update frontend state
|
||||
if (settingKey === 'default_lora_root') {
|
||||
state.global.settings.default_lora_root = value;
|
||||
} else if (settingKey === 'default_checkpoint_root') {
|
||||
state.global.settings.default_checkpoint_root = value;
|
||||
} else if (settingKey === 'default_embedding_root') {
|
||||
state.global.settings.default_embedding_root = value;
|
||||
} else if (settingKey === 'display_density') {
|
||||
state.global.settings.displayDensity = value;
|
||||
|
||||
// Also update compactMode for backwards compatibility
|
||||
state.global.settings.compactMode = (value !== 'default');
|
||||
} else if (settingKey === 'card_info_display') {
|
||||
state.global.settings.cardInfoDisplay = value;
|
||||
} else {
|
||||
// For any other settings that might be added in the future
|
||||
state.global.settings[settingKey] = value;
|
||||
}
|
||||
|
||||
// Save to localStorage
|
||||
setStorageItem('settings', state.global.settings);
|
||||
|
||||
try {
|
||||
// For backend settings, make API call
|
||||
if (settingKey === 'default_lora_root' || settingKey === 'default_checkpoint_root' || settingKey === 'default_embedding_root' || settingKey === 'download_path_templates') {
|
||||
const payload = {};
|
||||
if (settingKey === 'download_path_templates') {
|
||||
payload[settingKey] = state.global.settings.download_path_templates;
|
||||
} else {
|
||||
payload[settingKey] = value;
|
||||
}
|
||||
|
||||
const response = await fetch('/api/settings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
// Update frontend state with mapped keys
|
||||
await this.saveSetting(settingKey, value);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to save setting');
|
||||
}
|
||||
|
||||
showToast('toast.settings.settingsUpdated', { setting: settingKey.replace(/_/g, ' ') }, 'success');
|
||||
}
|
||||
|
||||
// Apply frontend settings immediately
|
||||
this.applyFrontendSettings();
|
||||
|
||||
@@ -924,8 +867,10 @@ export class SettingsManager {
|
||||
if (value === 'compact') densityName = "Compact";
|
||||
|
||||
showToast('toast.settings.displayDensitySet', { density: densityName }, 'success');
|
||||
return;
|
||||
}
|
||||
|
||||
showToast('toast.settings.settingsUpdated', { setting: settingKey.replace(/_/g, ' ') }, 'success');
|
||||
} catch (error) {
|
||||
showToast('toast.settings.settingSaveFailed', { message: error.message }, 'error');
|
||||
}
|
||||
@@ -948,7 +893,7 @@ export class SettingsManager {
|
||||
|
||||
async updateMetadataArchiveStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/metadata-archive-status');
|
||||
const response = await fetch('/api/lm/metadata-archive-status');
|
||||
const data = await response.json();
|
||||
|
||||
const statusContainer = document.getElementById('metadataArchiveStatus');
|
||||
@@ -1077,7 +1022,7 @@ export class SettingsManager {
|
||||
// Wait for WebSocket to be ready
|
||||
await wsReady;
|
||||
|
||||
const response = await fetch(`/api/download-metadata-archive?download_id=${encodeURIComponent(actualDownloadId)}`, {
|
||||
const response = await fetch(`/api/lm/download-metadata-archive?download_id=${encodeURIComponent(actualDownloadId)}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
@@ -1097,9 +1042,8 @@ export class SettingsManager {
|
||||
|
||||
showToast('settings.metadataArchive.downloadSuccess', 'success');
|
||||
|
||||
// Update settings in state
|
||||
state.global.settings.enable_metadata_archive_db = true;
|
||||
setStorageItem('settings', state.global.settings);
|
||||
// Update settings using universal save method
|
||||
await this.saveSetting('enable_metadata_archive_db', true);
|
||||
|
||||
// Update UI
|
||||
const enableCheckbox = document.getElementById('enableMetadataArchive');
|
||||
@@ -1141,7 +1085,7 @@ export class SettingsManager {
|
||||
removeBtn.textContent = translate('settings.metadataArchive.removingButton');
|
||||
}
|
||||
|
||||
const response = await fetch('/api/remove-metadata-archive', {
|
||||
const response = await fetch('/api/lm/remove-metadata-archive', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
@@ -1153,9 +1097,8 @@ export class SettingsManager {
|
||||
if (data.success) {
|
||||
showToast('settings.metadataArchive.removeSuccess', 'success');
|
||||
|
||||
// Update settings in state
|
||||
state.global.settings.enable_metadata_archive_db = false;
|
||||
setStorageItem('settings', state.global.settings);
|
||||
// Update settings using universal save method
|
||||
await this.saveSetting('enable_metadata_archive_db', false);
|
||||
|
||||
// Update UI
|
||||
const enableCheckbox = document.getElementById('enableMetadataArchive');
|
||||
@@ -1183,9 +1126,8 @@ export class SettingsManager {
|
||||
const element = document.getElementById(elementId);
|
||||
if (!element) return;
|
||||
|
||||
const value = element.value;
|
||||
const value = element.value.trim(); // Trim whitespace
|
||||
|
||||
// For API key or other inputs that need to be saved on backend
|
||||
try {
|
||||
// Check if value has changed from existing value
|
||||
const currentValue = state.global.settings[settingKey] || '';
|
||||
@@ -1193,25 +1135,29 @@ export class SettingsManager {
|
||||
return; // No change, exit early
|
||||
}
|
||||
|
||||
// Update state
|
||||
state.global.settings[settingKey] = value;
|
||||
|
||||
setStorageItem('settings', state.global.settings);
|
||||
|
||||
// For backend settings, make API call
|
||||
const payload = {};
|
||||
payload[settingKey] = value;
|
||||
|
||||
const response = await fetch('/api/settings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
// For username and password, handle empty values specially
|
||||
if ((settingKey === 'proxy_username' || settingKey === 'proxy_password') && value === '') {
|
||||
// Remove from state instead of setting to empty string
|
||||
delete state.global.settings[settingKey];
|
||||
|
||||
// Send delete flag to backend
|
||||
const payload = {};
|
||||
payload[settingKey] = '__DELETE__';
|
||||
|
||||
const response = await fetch('/api/lm/settings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to save setting');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete setting');
|
||||
}
|
||||
} else {
|
||||
// Use the universal save method
|
||||
await this.saveSetting(settingKey, value);
|
||||
}
|
||||
|
||||
showToast('toast.settings.settingsUpdated', { setting: settingKey.replace(/_/g, ' ') }, 'success');
|
||||
@@ -1224,31 +1170,13 @@ export class SettingsManager {
|
||||
async saveLanguageSetting() {
|
||||
const element = document.getElementById('languageSelect');
|
||||
if (!element) return;
|
||||
|
||||
const selectedLanguage = element.value;
|
||||
|
||||
try {
|
||||
// Update local state
|
||||
state.global.settings.language = selectedLanguage;
|
||||
|
||||
// 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({
|
||||
language: selectedLanguage
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to save language setting to backend');
|
||||
}
|
||||
|
||||
const selectedLanguage = element.value;
|
||||
|
||||
try {
|
||||
// Use the universal save method for language (frontend-only setting)
|
||||
await this.saveSetting('language', selectedLanguage);
|
||||
|
||||
// Reload the page to apply the new language
|
||||
window.location.reload();
|
||||
|
||||
@@ -1293,47 +1221,25 @@ export class SettingsManager {
|
||||
|
||||
applyFrontendSettings() {
|
||||
// Apply autoplay setting to existing videos in card previews
|
||||
const autoplayOnHover = state.global.settings.autoplayOnHover;
|
||||
const autoplayOnHover = state.global.settings.autoplay_on_hover;
|
||||
document.querySelectorAll('.card-preview video').forEach(video => {
|
||||
// Remove previous event listeners by cloning and replacing the element
|
||||
const videoParent = video.parentElement;
|
||||
const videoClone = video.cloneNode(true);
|
||||
|
||||
if (autoplayOnHover) {
|
||||
// Pause video initially and set up mouse events for hover playback
|
||||
videoClone.removeAttribute('autoplay');
|
||||
videoClone.pause();
|
||||
|
||||
// Add mouse events to the parent element
|
||||
videoParent.onmouseenter = () => videoClone.play();
|
||||
videoParent.onmouseleave = () => {
|
||||
videoClone.pause();
|
||||
videoClone.currentTime = 0;
|
||||
};
|
||||
} else {
|
||||
// Use default autoplay behavior
|
||||
videoClone.setAttribute('autoplay', '');
|
||||
videoParent.onmouseenter = null;
|
||||
videoParent.onmouseleave = null;
|
||||
}
|
||||
|
||||
videoParent.replaceChild(videoClone, video);
|
||||
configureModelCardVideo(video, autoplayOnHover);
|
||||
});
|
||||
|
||||
// Apply display density class to grid
|
||||
const grid = document.querySelector('.card-grid');
|
||||
if (grid) {
|
||||
const density = state.global.settings.displayDensity || 'default';
|
||||
|
||||
const density = state.global.settings.display_density || 'default';
|
||||
|
||||
// Remove all density classes first
|
||||
grid.classList.remove('default-density', 'medium-density', 'compact-density');
|
||||
|
||||
|
||||
// Add the appropriate density class
|
||||
grid.classList.add(`${density}-density`);
|
||||
}
|
||||
|
||||
|
||||
// Apply card info display setting
|
||||
const cardInfoDisplay = state.global.settings.cardInfoDisplay || 'always';
|
||||
const cardInfoDisplay = state.global.settings.card_info_display || 'always';
|
||||
document.body.classList.toggle('hover-reveal', cardInfoDisplay === 'hover');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ export class UpdateService {
|
||||
|
||||
try {
|
||||
// Call backend API to check for updates with nightly flag
|
||||
const response = await fetch(`/api/check-updates?nightly=${this.nightlyMode}`);
|
||||
const response = await fetch(`/api/lm/check-updates?nightly=${this.nightlyMode}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
@@ -280,7 +280,7 @@ export class UpdateService {
|
||||
// Update progress
|
||||
this.updateProgress(10, translate('update.updateProgress.preparing'));
|
||||
|
||||
const response = await fetch('/api/perform-update', {
|
||||
const response = await fetch('/api/lm/perform-update', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
@@ -444,7 +444,7 @@ export class UpdateService {
|
||||
async checkVersionInfo() {
|
||||
try {
|
||||
// Call API to get current version info
|
||||
const response = await fetch('/api/version-info');
|
||||
const response = await fetch('/api/lm/version-info');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
|
||||
@@ -68,7 +68,7 @@ export class DownloadManager {
|
||||
formData.append('metadata', JSON.stringify(completeMetadata));
|
||||
|
||||
// Send save request
|
||||
const response = await fetch('/api/recipes/save', {
|
||||
const response = await fetch('/api/lm/recipes/save', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
@@ -1,221 +0,0 @@
|
||||
import { showToast } from '../../utils/uiHelpers.js';
|
||||
import { translate } from '../../utils/i18nHelpers.js';
|
||||
import { getStorageItem } from '../../utils/storageHelpers.js';
|
||||
|
||||
export class FolderBrowser {
|
||||
constructor(importManager) {
|
||||
this.importManager = importManager;
|
||||
this.folderClickHandler = null;
|
||||
this.updateTargetPath = this.updateTargetPath.bind(this);
|
||||
}
|
||||
|
||||
async proceedToLocation() {
|
||||
// Show the location step with special handling
|
||||
this.importManager.stepManager.showStep('locationStep');
|
||||
|
||||
// Double-check after a short delay to ensure the step is visible
|
||||
setTimeout(() => {
|
||||
const locationStep = document.getElementById('locationStep');
|
||||
if (locationStep.style.display !== 'block' ||
|
||||
window.getComputedStyle(locationStep).display !== 'block') {
|
||||
// Force display again
|
||||
locationStep.style.display = 'block';
|
||||
|
||||
// If still not visible, try with injected style
|
||||
if (window.getComputedStyle(locationStep).display !== 'block') {
|
||||
this.importManager.stepManager.injectedStyles = document.createElement('style');
|
||||
this.importManager.stepManager.injectedStyles.innerHTML = `
|
||||
#locationStep {
|
||||
display: block !important;
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(this.importManager.stepManager.injectedStyles);
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
|
||||
try {
|
||||
// Display missing LoRAs that will be downloaded
|
||||
const missingLorasList = document.getElementById('missingLorasList');
|
||||
if (missingLorasList && this.importManager.downloadableLoRAs.length > 0) {
|
||||
// Calculate total size
|
||||
const totalSize = this.importManager.downloadableLoRAs.reduce((sum, lora) => {
|
||||
return sum + (lora.size ? parseInt(lora.size) : 0);
|
||||
}, 0);
|
||||
|
||||
// Update total size display
|
||||
const totalSizeDisplay = document.getElementById('totalDownloadSize');
|
||||
if (totalSizeDisplay) {
|
||||
totalSizeDisplay.textContent = this.importManager.formatFileSize(totalSize);
|
||||
}
|
||||
|
||||
// Update header to include count of missing LoRAs
|
||||
const missingLorasHeader = document.querySelector('.summary-header h3');
|
||||
if (missingLorasHeader) {
|
||||
missingLorasHeader.innerHTML = `Missing LoRAs <span class="lora-count-badge">(${this.importManager.downloadableLoRAs.length})</span> <span id="totalDownloadSize" class="total-size-badge">${this.importManager.formatFileSize(totalSize)}</span>`;
|
||||
}
|
||||
|
||||
// Generate missing LoRAs list
|
||||
missingLorasList.innerHTML = this.importManager.downloadableLoRAs.map(lora => {
|
||||
const sizeDisplay = lora.size ?
|
||||
this.importManager.formatFileSize(lora.size) : 'Unknown size';
|
||||
const baseModel = lora.baseModel ?
|
||||
`<span class="lora-base-model">${lora.baseModel}</span>` : '';
|
||||
const isEarlyAccess = lora.isEarlyAccess;
|
||||
|
||||
// Early access badge
|
||||
let earlyAccessBadge = '';
|
||||
if (isEarlyAccess) {
|
||||
earlyAccessBadge = `<span class="early-access-badge">
|
||||
<i class="fas fa-clock"></i> Early Access
|
||||
</span>`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="missing-lora-item ${isEarlyAccess ? 'is-early-access' : ''}">
|
||||
<div class="missing-lora-info">
|
||||
<div class="missing-lora-name">${lora.name}</div>
|
||||
${baseModel}
|
||||
${earlyAccessBadge}
|
||||
</div>
|
||||
<div class="missing-lora-size">${sizeDisplay}</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// Set up toggle for missing LoRAs list
|
||||
const toggleBtn = document.getElementById('toggleMissingLorasList');
|
||||
if (toggleBtn) {
|
||||
toggleBtn.addEventListener('click', () => {
|
||||
missingLorasList.classList.toggle('collapsed');
|
||||
const icon = toggleBtn.querySelector('i');
|
||||
if (icon) {
|
||||
icon.classList.toggle('fa-chevron-down');
|
||||
icon.classList.toggle('fa-chevron-up');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch LoRA roots
|
||||
const rootsResponse = await fetch('/api/loras/roots');
|
||||
if (!rootsResponse.ok) {
|
||||
throw new Error(`Failed to fetch LoRA roots: ${rootsResponse.status}`);
|
||||
}
|
||||
|
||||
const rootsData = await rootsResponse.json();
|
||||
const loraRoot = document.getElementById('importLoraRoot');
|
||||
if (loraRoot) {
|
||||
loraRoot.innerHTML = rootsData.roots.map(root =>
|
||||
`<option value="${root}">${root}</option>`
|
||||
).join('');
|
||||
|
||||
// Set default lora root if available
|
||||
const defaultRoot = getStorageItem('settings', {}).default_lora_root;
|
||||
if (defaultRoot && rootsData.roots.includes(defaultRoot)) {
|
||||
loraRoot.value = defaultRoot;
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch folders
|
||||
const foldersResponse = await fetch('/api/loras/folders');
|
||||
if (!foldersResponse.ok) {
|
||||
throw new Error(`Failed to fetch folders: ${foldersResponse.status}`);
|
||||
}
|
||||
|
||||
const foldersData = await foldersResponse.json();
|
||||
const folderBrowser = document.getElementById('importFolderBrowser');
|
||||
if (folderBrowser) {
|
||||
folderBrowser.innerHTML = foldersData.folders.map(folder =>
|
||||
folder ? `<div class="folder-item" data-folder="${folder}">${folder}</div>` : ''
|
||||
).join('');
|
||||
}
|
||||
|
||||
// Initialize folder browser after loading data
|
||||
this.initializeFolderBrowser();
|
||||
} catch (error) {
|
||||
console.error('Error in API calls:', error);
|
||||
showToast('toast.recipes.folderBrowserError', { message: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
initializeFolderBrowser() {
|
||||
const folderBrowser = document.getElementById('importFolderBrowser');
|
||||
if (!folderBrowser) return;
|
||||
|
||||
// Cleanup existing handler if any
|
||||
this.cleanup();
|
||||
|
||||
// Create new handler
|
||||
this.folderClickHandler = (event) => {
|
||||
const folderItem = event.target.closest('.folder-item');
|
||||
if (!folderItem) return;
|
||||
|
||||
if (folderItem.classList.contains('selected')) {
|
||||
folderItem.classList.remove('selected');
|
||||
this.importManager.selectedFolder = '';
|
||||
} else {
|
||||
folderBrowser.querySelectorAll('.folder-item').forEach(f =>
|
||||
f.classList.remove('selected'));
|
||||
folderItem.classList.add('selected');
|
||||
this.importManager.selectedFolder = folderItem.dataset.folder;
|
||||
}
|
||||
|
||||
// Update path display after folder selection
|
||||
this.updateTargetPath();
|
||||
};
|
||||
|
||||
// Add the new handler
|
||||
folderBrowser.addEventListener('click', this.folderClickHandler);
|
||||
|
||||
// Add event listeners for path updates
|
||||
const loraRoot = document.getElementById('importLoraRoot');
|
||||
const newFolder = document.getElementById('importNewFolder');
|
||||
|
||||
if (loraRoot) loraRoot.addEventListener('change', this.updateTargetPath);
|
||||
if (newFolder) newFolder.addEventListener('input', this.updateTargetPath);
|
||||
|
||||
// Update initial path
|
||||
this.updateTargetPath();
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if (this.folderClickHandler) {
|
||||
const folderBrowser = document.getElementById('importFolderBrowser');
|
||||
if (folderBrowser) {
|
||||
folderBrowser.removeEventListener('click', this.folderClickHandler);
|
||||
this.folderClickHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove path update listeners
|
||||
const loraRoot = document.getElementById('importLoraRoot');
|
||||
const newFolder = document.getElementById('importNewFolder');
|
||||
|
||||
if (loraRoot) loraRoot.removeEventListener('change', this.updateTargetPath);
|
||||
if (newFolder) newFolder.removeEventListener('input', this.updateTargetPath);
|
||||
}
|
||||
|
||||
updateTargetPath() {
|
||||
const pathDisplay = document.getElementById('importTargetPathDisplay');
|
||||
if (!pathDisplay) return;
|
||||
|
||||
const loraRoot = document.getElementById('importLoraRoot')?.value || '';
|
||||
const newFolder = document.getElementById('importNewFolder')?.value?.trim() || '';
|
||||
|
||||
let fullPath = loraRoot || translate('recipes.controls.import.selectLoraRoot', {}, 'Select a LoRA root directory');
|
||||
|
||||
if (loraRoot) {
|
||||
if (this.importManager.selectedFolder) {
|
||||
fullPath += '/' + this.importManager.selectedFolder;
|
||||
}
|
||||
if (newFolder) {
|
||||
fullPath += '/' + newFolder;
|
||||
}
|
||||
}
|
||||
|
||||
pathDisplay.innerHTML = `<span class="path-text">${fullPath}</span>`;
|
||||
}
|
||||
}
|
||||
@@ -62,7 +62,7 @@ export class ImageProcessor {
|
||||
async analyzeImageFromUrl(url) {
|
||||
try {
|
||||
// Call the API with URL data
|
||||
const response = await fetch('/api/recipes/analyze-image', {
|
||||
const response = await fetch('/api/lm/recipes/analyze-image', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
@@ -110,7 +110,7 @@ export class ImageProcessor {
|
||||
async analyzeImageFromLocalPath(path) {
|
||||
try {
|
||||
// Call the API with local path data
|
||||
const response = await fetch('/api/recipes/analyze-local-image', {
|
||||
const response = await fetch('/api/lm/recipes/analyze-local-image', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
@@ -169,7 +169,7 @@ export class ImageProcessor {
|
||||
formData.append('image', this.importManager.recipeImage);
|
||||
|
||||
// Upload image for analysis
|
||||
const response = await fetch('/api/recipes/analyze-image', {
|
||||
const response = await fetch('/api/lm/recipes/analyze-image', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
@@ -1,23 +1,52 @@
|
||||
// Create the new hierarchical state structure
|
||||
import { getStorageItem, getMapFromStorage } from '../utils/storageHelpers.js';
|
||||
import { MODEL_TYPES } from '../api/apiConfig.js';
|
||||
import { DEFAULT_PATH_TEMPLATES } from '../utils/constants.js';
|
||||
|
||||
// Load settings from localStorage or use defaults
|
||||
const savedSettings = getStorageItem('settings', {
|
||||
blurMatureContent: true,
|
||||
const DEFAULT_SETTINGS_BASE = Object.freeze({
|
||||
civitai_api_key: '',
|
||||
language: 'en',
|
||||
show_only_sfw: false,
|
||||
cardInfoDisplay: 'always'
|
||||
enable_metadata_archive_db: false,
|
||||
proxy_enabled: false,
|
||||
proxy_type: 'http',
|
||||
proxy_host: '',
|
||||
proxy_port: '',
|
||||
proxy_username: '',
|
||||
proxy_password: '',
|
||||
default_lora_root: '',
|
||||
default_checkpoint_root: '',
|
||||
default_embedding_root: '',
|
||||
base_model_path_mappings: {},
|
||||
download_path_templates: {},
|
||||
example_images_path: '',
|
||||
optimize_example_images: true,
|
||||
auto_download_example_images: false,
|
||||
blur_mature_content: true,
|
||||
autoplay_on_hover: false,
|
||||
display_density: 'default',
|
||||
card_info_display: 'always',
|
||||
include_trigger_words: false,
|
||||
compact_mode: false,
|
||||
});
|
||||
|
||||
export function createDefaultSettings() {
|
||||
return {
|
||||
...DEFAULT_SETTINGS_BASE,
|
||||
base_model_path_mappings: {},
|
||||
download_path_templates: { ...DEFAULT_PATH_TEMPLATES },
|
||||
};
|
||||
}
|
||||
|
||||
// Load preview versions from localStorage for each model type
|
||||
const loraPreviewVersions = getMapFromStorage('lora_preview_versions');
|
||||
const checkpointPreviewVersions = getMapFromStorage('checkpoint_preview_versions');
|
||||
const embeddingPreviewVersions = getMapFromStorage('embedding_preview_versions');
|
||||
const loraPreviewVersions = getMapFromStorage('loras_preview_versions');
|
||||
const checkpointPreviewVersions = getMapFromStorage('checkpoints_preview_versions');
|
||||
const embeddingPreviewVersions = getMapFromStorage('embeddings_preview_versions');
|
||||
|
||||
export const state = {
|
||||
// Global state
|
||||
global: {
|
||||
settings: savedSettings,
|
||||
settings: createDefaultSettings(),
|
||||
loadingManager: null,
|
||||
observer: null,
|
||||
},
|
||||
|
||||
@@ -65,12 +65,12 @@ class StatisticsManager {
|
||||
storageAnalytics,
|
||||
insights
|
||||
] = await Promise.all([
|
||||
this.fetchData('/api/stats/collection-overview'),
|
||||
this.fetchData('/api/stats/usage-analytics'),
|
||||
this.fetchData('/api/stats/base-model-distribution'),
|
||||
this.fetchData('/api/stats/tag-analytics'),
|
||||
this.fetchData('/api/stats/storage-analytics'),
|
||||
this.fetchData('/api/stats/insights')
|
||||
this.fetchData('/api/lm/stats/collection-overview'),
|
||||
this.fetchData('/api/lm/stats/usage-analytics'),
|
||||
this.fetchData('/api/lm/stats/base-model-distribution'),
|
||||
this.fetchData('/api/lm/stats/tag-analytics'),
|
||||
this.fetchData('/api/lm/stats/storage-analytics'),
|
||||
this.fetchData('/api/lm/stats/insights')
|
||||
]);
|
||||
|
||||
this.data = {
|
||||
|
||||
@@ -102,7 +102,7 @@ export class VirtualScroller {
|
||||
const availableContentWidth = containerWidth - paddingLeft - paddingRight;
|
||||
|
||||
// Get display density setting
|
||||
const displayDensity = state.global.settings?.displayDensity || 'default';
|
||||
const displayDensity = state.global.settings?.display_density || 'default';
|
||||
|
||||
// Set exact column counts and grid widths to match CSS container widths
|
||||
let maxColumns, maxGridWidth;
|
||||
|
||||
@@ -92,6 +92,12 @@ export const DOWNLOAD_PATH_TEMPLATES = {
|
||||
description: 'Organize by base model and author',
|
||||
example: 'Flux.1 D/authorname/model-name.safetensors'
|
||||
},
|
||||
BASE_MODEL_AUTHOR_TAG: {
|
||||
value: '{base_model}/{author}/{first_tag}',
|
||||
label: 'Base Model + Author + First Tag',
|
||||
description: 'Organize by base model, author, and primary tag',
|
||||
example: 'Flux.1 D/authorname/style/model-name.safetensors'
|
||||
},
|
||||
AUTHOR_TAG: {
|
||||
value: '{author}/{first_tag}',
|
||||
label: 'Author + First Tag',
|
||||
@@ -189,8 +195,8 @@ export const BASE_MODEL_CATEGORIES = {
|
||||
|
||||
// Preset tag suggestions
|
||||
export const PRESET_TAGS = [
|
||||
'character', 'style', 'concept', 'clothing',
|
||||
'realistic', 'anime', 'toon', 'furry',
|
||||
'character', 'concept', 'clothing',
|
||||
'realistic', 'anime', 'toon', 'furry', 'style',
|
||||
'poses', 'background', 'vehicle', 'buildings',
|
||||
'objects', 'animal'
|
||||
];
|
||||
|
||||
@@ -86,29 +86,39 @@ function setupPageUnloadCleanup() {
|
||||
function registerContextMenuEvents() {
|
||||
eventManager.addHandler('contextmenu', 'contextMenu-coordination', (e) => {
|
||||
const card = e.target.closest('.model-card');
|
||||
if (!card) {
|
||||
// Hide all menus if not right-clicking on a card
|
||||
window.pageContextMenu?.hideMenu();
|
||||
window.bulkManager?.bulkContextMenu?.hideMenu();
|
||||
const pageContent = e.target.closest('.page-content');
|
||||
|
||||
if (!pageContent) {
|
||||
window.globalContextMenuInstance?.hideMenu();
|
||||
return false;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
// Hide all menus first
|
||||
window.pageContextMenu?.hideMenu();
|
||||
window.bulkManager?.bulkContextMenu?.hideMenu();
|
||||
|
||||
// Determine which menu to show based on bulk mode and selection state
|
||||
if (state.bulkMode && card.classList.contains('selected')) {
|
||||
// Show bulk menu for selected cards in bulk mode
|
||||
window.bulkManager?.bulkContextMenu?.showMenu(e.clientX, e.clientY, card);
|
||||
} else if (!state.bulkMode) {
|
||||
// Show regular menu when not in bulk mode
|
||||
window.pageContextMenu?.showMenu(e.clientX, e.clientY, card);
|
||||
|
||||
if (card) {
|
||||
e.preventDefault();
|
||||
|
||||
// Hide all menus first
|
||||
window.pageContextMenu?.hideMenu();
|
||||
window.bulkManager?.bulkContextMenu?.hideMenu();
|
||||
window.globalContextMenuInstance?.hideMenu();
|
||||
|
||||
// Determine which menu to show based on bulk mode and selection state
|
||||
if (state.bulkMode && card.classList.contains('selected')) {
|
||||
// Show bulk menu for selected cards in bulk mode
|
||||
window.bulkManager?.bulkContextMenu?.showMenu(e.clientX, e.clientY, card);
|
||||
} else if (!state.bulkMode) {
|
||||
// Show regular menu when not in bulk mode
|
||||
window.pageContextMenu?.showMenu(e.clientX, e.clientY, card);
|
||||
}
|
||||
} else {
|
||||
e.preventDefault();
|
||||
|
||||
window.pageContextMenu?.hideMenu();
|
||||
window.bulkManager?.bulkContextMenu?.hideMenu();
|
||||
window.globalContextMenuInstance?.hideMenu();
|
||||
|
||||
window.globalContextMenuInstance?.showMenu(e.clientX, e.clientY, null);
|
||||
}
|
||||
// Don't show any menu for unselected cards in bulk mode
|
||||
|
||||
|
||||
return true; // Stop propagation
|
||||
}, {
|
||||
priority: 200, // Higher priority than bulk manager events
|
||||
@@ -125,6 +135,7 @@ function registerGlobalClickHandlers() {
|
||||
if (!e.target.closest('.context-menu')) {
|
||||
window.pageContextMenu?.hideMenu();
|
||||
window.bulkManager?.bulkContextMenu?.hideMenu();
|
||||
window.globalContextMenuInstance?.hideMenu();
|
||||
}
|
||||
return false; // Allow other handlers to process
|
||||
}, {
|
||||
|
||||
@@ -116,64 +116,6 @@ export function removeSessionItem(key) {
|
||||
sessionStorage.removeItem(STORAGE_PREFIX + key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate all existing localStorage items to use the prefix
|
||||
* This should be called once during application initialization
|
||||
*/
|
||||
export function migrateStorageItems() {
|
||||
// Check if migration has already been performed
|
||||
if (localStorage.getItem(STORAGE_PREFIX + 'migration_completed')) {
|
||||
console.log('Lora Manager: Storage migration already completed');
|
||||
return;
|
||||
}
|
||||
|
||||
// List of known keys used in the application
|
||||
const knownKeys = [
|
||||
'nsfwBlurLevel',
|
||||
'theme',
|
||||
'activeFolder',
|
||||
'folderTagsCollapsed',
|
||||
'settings',
|
||||
'loras_filters',
|
||||
'recipes_filters',
|
||||
'checkpoints_filters',
|
||||
'loras_search_prefs',
|
||||
'recipes_search_prefs',
|
||||
'checkpoints_search_prefs',
|
||||
'show_update_notifications',
|
||||
'last_update_check',
|
||||
'dismissed_banners'
|
||||
];
|
||||
|
||||
// Migrate each known key
|
||||
knownKeys.forEach(key => {
|
||||
const prefixedKey = STORAGE_PREFIX + key;
|
||||
|
||||
// Only migrate if the prefixed key doesn't already exist
|
||||
if (localStorage.getItem(prefixedKey) === null) {
|
||||
const value = localStorage.getItem(key);
|
||||
if (value !== null) {
|
||||
try {
|
||||
// Try to parse as JSON first
|
||||
const parsedValue = JSON.parse(value);
|
||||
setStorageItem(key, parsedValue);
|
||||
} catch (e) {
|
||||
// If not JSON, store as is
|
||||
setStorageItem(key, value);
|
||||
}
|
||||
|
||||
// We can optionally remove the old key after migration
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Mark migration as completed
|
||||
localStorage.setItem(STORAGE_PREFIX + 'migration_completed', 'true');
|
||||
|
||||
console.log('Lora Manager: Storage migration completed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a Map to localStorage
|
||||
* @param {string} key - The localStorage key
|
||||
|
||||
@@ -295,13 +295,51 @@ export function getNSFWLevelName(level) {
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
function parseUsageTipNumber(value) {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
const parsed = parseFloat(value);
|
||||
if (Number.isFinite(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getLoraStrengthsFromUsageTips(usageTips = {}) {
|
||||
const parsedStrength = parseUsageTipNumber(usageTips.strength);
|
||||
const clipStrengthSource = usageTips.clip_strength ?? usageTips.clipStrength;
|
||||
const parsedClipStrength = parseUsageTipNumber(clipStrengthSource);
|
||||
|
||||
return {
|
||||
strength: parsedStrength !== null ? parsedStrength : 1,
|
||||
hasStrength: parsedStrength !== null,
|
||||
clipStrength: parsedClipStrength,
|
||||
hasClipStrength: parsedClipStrength !== null,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildLoraSyntax(fileName, usageTips = {}) {
|
||||
const { strength, hasStrength, clipStrength, hasClipStrength } = getLoraStrengthsFromUsageTips(usageTips);
|
||||
|
||||
if (hasClipStrength) {
|
||||
const modelStrength = hasStrength ? strength : 1;
|
||||
return `<lora:${fileName}:${modelStrength}:${clipStrength}>`;
|
||||
}
|
||||
|
||||
return `<lora:${fileName}:${strength}>`;
|
||||
}
|
||||
|
||||
export function copyLoraSyntax(card) {
|
||||
const usageTips = JSON.parse(card.dataset.usage_tips || "{}");
|
||||
const strength = usageTips.strength || 1;
|
||||
const baseSyntax = `<lora:${card.dataset.file_name}:${strength}>`;
|
||||
const baseSyntax = buildLoraSyntax(card.dataset.file_name, usageTips);
|
||||
|
||||
// Check if trigger words should be included
|
||||
const includeTriggerWords = state.global.settings.includeTriggerWords;
|
||||
const includeTriggerWords = state.global.settings.include_trigger_words;
|
||||
|
||||
if (!includeTriggerWords) {
|
||||
const message = translate('uiHelpers.lora.syntaxCopied', {}, 'LoRA syntax copied to clipboard');
|
||||
@@ -370,7 +408,7 @@ export function copyLoraSyntax(card) {
|
||||
export async function sendLoraToWorkflow(loraSyntax, replaceMode = false, syntaxType = 'lora') {
|
||||
try {
|
||||
// Get registry information from the new endpoint
|
||||
const registryResponse = await fetch('/api/get-registry');
|
||||
const registryResponse = await fetch('/api/lm/get-registry');
|
||||
const registryData = await registryResponse.json();
|
||||
|
||||
if (!registryData.success) {
|
||||
@@ -417,7 +455,7 @@ export async function sendLoraToWorkflow(loraSyntax, replaceMode = false, syntax
|
||||
async function sendToSpecificNode(nodeIds, loraSyntax, replaceMode, syntaxType) {
|
||||
try {
|
||||
// Call the backend API to update the lora code
|
||||
const response = await fetch('/api/update-lora-code', {
|
||||
const response = await fetch('/api/lm/update-lora-code', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
@@ -676,7 +714,7 @@ initializeMouseTracking();
|
||||
*/
|
||||
export async function openExampleImagesFolder(modelHash) {
|
||||
try {
|
||||
const response = await fetch('/api/open-example-images-folder', {
|
||||
const response = await fetch('/api/lm/open-example-images-folder', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
|
||||
Reference in New Issue
Block a user