Merge branch 'main' into main

This commit is contained in:
start-life
2025-09-25 09:28:30 +03:00
committed by GitHub
200 changed files with 18778 additions and 10106 deletions

View File

@@ -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);
}
}

View File

@@ -233,7 +233,7 @@
}
.sidebar-tree-children.expanded {
max-height: 9999px;
max-height: 50000px;
}
.sidebar-tree-children .sidebar-tree-node-content {

View File

@@ -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

View File

@@ -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, {

View File

@@ -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',

View File

@@ -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);

View 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');
}
}
}

View File

@@ -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');
}
}

View File

@@ -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',

View File

@@ -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;

View File

@@ -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();
}

View File

@@ -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'

View File

@@ -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'

View File

@@ -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');

View File

@@ -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',

View File

@@ -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}`);

View File

@@ -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);

View File

@@ -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();
}
}
}

View File

@@ -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 })

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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' : ''}">

View File

@@ -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'

View File

@@ -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) {

View File

@@ -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();

View File

@@ -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 };

View File

@@ -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

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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)) {

View File

@@ -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();

View File

@@ -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)

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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',

View File

@@ -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');
}
}

View File

@@ -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) {

View File

@@ -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
});

View File

@@ -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>`;
}
}

View File

@@ -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
});

View File

@@ -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,
},

View File

@@ -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 = {

View File

@@ -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;

View File

@@ -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'
];

View File

@@ -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
}, {

View File

@@ -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

View File

@@ -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'