mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-26 07:35:44 -03:00
Refactor API structure to unify model operations
- Introduced MODEL_TYPES and MODEL_CONFIG for centralized model type management. - Created a unified API client for checkpoints and loras to streamline operations. - Updated all API calls in checkpointApi.js and loraApi.js to use the new client. - Simplified context menus and model card operations to leverage the unified API client. - Enhanced state management to accommodate new model types and their configurations. - Added virtual scrolling functions for recipes and improved loading states. - Refactored modal utilities to handle model exclusion and deletion generically. - Improved error handling and user feedback across various operations.
This commit is contained in:
@@ -1038,6 +1038,7 @@ class ModelRouteUtils:
|
|||||||
return web.json_response({
|
return web.json_response({
|
||||||
'success': True,
|
'success': True,
|
||||||
'new_file_path': new_file_path,
|
'new_file_path': new_file_path,
|
||||||
|
'new_preview_path': config.get_preview_static_url(new_preview),
|
||||||
'renamed_files': renamed_files,
|
'renamed_files': renamed_files,
|
||||||
'reload_required': False
|
'reload_required': False
|
||||||
})
|
})
|
||||||
|
|||||||
169
static/js/api/apiConfig.js
Normal file
169
static/js/api/apiConfig.js
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import { state } from '../state/index.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API Configuration
|
||||||
|
* Centralized configuration for all model types and their endpoints
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Model type definitions
|
||||||
|
export const MODEL_TYPES = {
|
||||||
|
LORA: 'loras',
|
||||||
|
CHECKPOINT: 'checkpoints',
|
||||||
|
EMBEDDING: 'embeddings' // Future model type
|
||||||
|
};
|
||||||
|
|
||||||
|
// Base API configuration for each model type
|
||||||
|
export const MODEL_CONFIG = {
|
||||||
|
[MODEL_TYPES.LORA]: {
|
||||||
|
displayName: 'LoRA',
|
||||||
|
singularName: 'lora',
|
||||||
|
defaultPageSize: 100,
|
||||||
|
supportsLetterFilter: true,
|
||||||
|
supportsBulkOperations: true,
|
||||||
|
supportsMove: true,
|
||||||
|
templateName: 'loras.html'
|
||||||
|
},
|
||||||
|
[MODEL_TYPES.CHECKPOINT]: {
|
||||||
|
displayName: 'Checkpoint',
|
||||||
|
singularName: 'checkpoint',
|
||||||
|
defaultPageSize: 50,
|
||||||
|
supportsLetterFilter: false,
|
||||||
|
supportsBulkOperations: true,
|
||||||
|
supportsMove: false,
|
||||||
|
templateName: 'checkpoints.html'
|
||||||
|
},
|
||||||
|
[MODEL_TYPES.EMBEDDING]: {
|
||||||
|
displayName: 'Embedding',
|
||||||
|
singularName: 'embedding',
|
||||||
|
defaultPageSize: 100,
|
||||||
|
supportsLetterFilter: true,
|
||||||
|
supportsBulkOperations: true,
|
||||||
|
supportsMove: true,
|
||||||
|
templateName: 'embeddings.html'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate API endpoints for a given model type
|
||||||
|
* @param {string} modelType - The model type (e.g., 'loras', 'checkpoints')
|
||||||
|
* @returns {Object} Object containing all API endpoints for the model type
|
||||||
|
*/
|
||||||
|
export function getApiEndpoints(modelType) {
|
||||||
|
if (!Object.values(MODEL_TYPES).includes(modelType)) {
|
||||||
|
throw new Error(`Invalid model type: ${modelType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Base CRUD operations
|
||||||
|
list: `/api/${modelType}`,
|
||||||
|
delete: `/api/${modelType}/delete`,
|
||||||
|
exclude: `/api/${modelType}/exclude`,
|
||||||
|
rename: `/api/${modelType}/rename`,
|
||||||
|
save: `/api/${modelType}/save-metadata`,
|
||||||
|
|
||||||
|
// Bulk operations
|
||||||
|
bulkDelete: `/api/${modelType}/bulk-delete`,
|
||||||
|
|
||||||
|
// CivitAI integration
|
||||||
|
fetchCivitai: `/api/${modelType}/fetch-civitai`,
|
||||||
|
fetchAllCivitai: `/api/${modelType}/fetch-all-civitai`,
|
||||||
|
relinkCivitai: `/api/${modelType}/relink-civitai`,
|
||||||
|
civitaiVersions: `/api/${modelType}/civitai/versions`,
|
||||||
|
|
||||||
|
// Preview management
|
||||||
|
replacePreview: `/api/${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`,
|
||||||
|
duplicates: `/api/${modelType}/find-duplicates`,
|
||||||
|
conflicts: `/api/${modelType}/find-filename-conflicts`,
|
||||||
|
verify: `/api/${modelType}/verify-duplicates`,
|
||||||
|
|
||||||
|
// Model-specific endpoints (will be merged with specific configs)
|
||||||
|
specific: {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model-specific endpoint configurations
|
||||||
|
*/
|
||||||
|
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`,
|
||||||
|
modelDescription: `/api/${MODEL_TYPES.LORA}/model-description`,
|
||||||
|
moveModel: `/api/${MODEL_TYPES.LORA}/move_model`,
|
||||||
|
moveBulk: `/api/${MODEL_TYPES.LORA}/move_models_bulk`,
|
||||||
|
getTriggerWordsPost: `/api/${MODEL_TYPES.LORA}/get_trigger_words`,
|
||||||
|
civitaiModelByVersion: `/api/${MODEL_TYPES.LORA}/civitai/model/version`,
|
||||||
|
civitaiModelByHash: `/api/${MODEL_TYPES.LORA}/civitai/model/hash`
|
||||||
|
},
|
||||||
|
[MODEL_TYPES.CHECKPOINT]: {
|
||||||
|
info: `/api/${MODEL_TYPES.CHECKPOINT}/info`
|
||||||
|
},
|
||||||
|
[MODEL_TYPES.EMBEDDING]: {
|
||||||
|
// Future embedding-specific endpoints
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get complete API configuration for a model type
|
||||||
|
* @param {string} modelType - The model type
|
||||||
|
* @returns {Object} Complete API configuration
|
||||||
|
*/
|
||||||
|
export function getCompleteApiConfig(modelType) {
|
||||||
|
const baseEndpoints = getApiEndpoints(modelType);
|
||||||
|
const specificEndpoints = MODEL_SPECIFIC_ENDPOINTS[modelType] || {};
|
||||||
|
const config = MODEL_CONFIG[modelType];
|
||||||
|
|
||||||
|
return {
|
||||||
|
modelType,
|
||||||
|
config,
|
||||||
|
endpoints: {
|
||||||
|
...baseEndpoints,
|
||||||
|
specific: specificEndpoints
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate if a model type is supported
|
||||||
|
* @param {string} modelType - The model type to validate
|
||||||
|
* @returns {boolean} True if valid, false otherwise
|
||||||
|
*/
|
||||||
|
export function isValidModelType(modelType) {
|
||||||
|
return Object.values(MODEL_TYPES).includes(modelType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get model type from current page or explicit parameter
|
||||||
|
* @param {string} [explicitType] - Explicitly provided model type
|
||||||
|
* @returns {string} The model type
|
||||||
|
*/
|
||||||
|
export function getCurrentModelType(explicitType = null) {
|
||||||
|
if (explicitType && isValidModelType(explicitType)) {
|
||||||
|
return explicitType;
|
||||||
|
}
|
||||||
|
|
||||||
|
return state.currentPageType || MODEL_TYPES.LORA;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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'
|
||||||
|
};
|
||||||
|
|
||||||
|
// WebSocket endpoints
|
||||||
|
export const WS_ENDPOINTS = {
|
||||||
|
fetchProgress: '/ws/fetch-progress'
|
||||||
|
};
|
||||||
@@ -1,97 +1,64 @@
|
|||||||
import { state, getCurrentPageState } from '../state/index.js';
|
import { state, getCurrentPageState } from '../state/index.js';
|
||||||
import { showToast } from '../utils/uiHelpers.js';
|
import { showToast } from '../utils/uiHelpers.js';
|
||||||
import { getSessionItem, saveMapToStorage } from '../utils/storageHelpers.js';
|
import { getSessionItem, saveMapToStorage } from '../utils/storageHelpers.js';
|
||||||
|
import {
|
||||||
|
getCompleteApiConfig,
|
||||||
|
getCurrentModelType,
|
||||||
|
isValidModelType,
|
||||||
|
DOWNLOAD_ENDPOINTS,
|
||||||
|
WS_ENDPOINTS
|
||||||
|
} from './apiConfig.js';
|
||||||
|
|
||||||
// New method for virtual scrolling fetch
|
/**
|
||||||
export async function fetchModelsPage(options = {}) {
|
* Universal API client for all model types
|
||||||
const {
|
*/
|
||||||
modelType = 'lora',
|
class ModelApiClient {
|
||||||
page = 1,
|
constructor(modelType = null) {
|
||||||
pageSize = 100,
|
this.modelType = modelType || getCurrentModelType();
|
||||||
endpoint = '/api/loras'
|
this.apiConfig = getCompleteApiConfig(this.modelType);
|
||||||
} = options;
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the model type for this client instance
|
||||||
|
* @param {string} modelType - The model type to use
|
||||||
|
*/
|
||||||
|
setModelType(modelType) {
|
||||||
|
if (!isValidModelType(modelType)) {
|
||||||
|
throw new Error(`Invalid model type: ${modelType}`);
|
||||||
|
}
|
||||||
|
this.modelType = modelType;
|
||||||
|
this.apiConfig = getCompleteApiConfig(modelType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current page state for this model type
|
||||||
|
*/
|
||||||
|
getPageState() {
|
||||||
|
const currentType = state.currentPageType;
|
||||||
|
// Temporarily switch to get the right page state
|
||||||
|
state.currentPageType = this.modelType;
|
||||||
const pageState = getCurrentPageState();
|
const pageState = getCurrentPageState();
|
||||||
|
state.currentPageType = currentType; // Restore
|
||||||
|
return pageState;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch models with pagination
|
||||||
|
*/
|
||||||
|
async fetchModelsPage(page = 1, pageSize = null) {
|
||||||
|
const pageState = this.getPageState();
|
||||||
|
const actualPageSize = pageSize || pageState.pageSize || this.apiConfig.config.defaultPageSize;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const params = new URLSearchParams({
|
const params = this._buildQueryParams({
|
||||||
page: page,
|
page,
|
||||||
page_size: pageSize || pageState.pageSize || 20,
|
page_size: actualPageSize,
|
||||||
sort_by: pageState.sortBy
|
sort_by: pageState.sortBy
|
||||||
});
|
}, pageState);
|
||||||
|
|
||||||
if (pageState.activeFolder !== null) {
|
const response = await fetch(`${this.apiConfig.endpoints.list}?${params}`);
|
||||||
params.append('folder', pageState.activeFolder);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add favorites filter parameter if enabled
|
|
||||||
if (pageState.showFavoritesOnly) {
|
|
||||||
params.append('favorites_only', 'true');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add active letter filter if set
|
|
||||||
if (pageState.activeLetterFilter) {
|
|
||||||
params.append('first_letter', pageState.activeLetterFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add search parameters if there's a search term
|
|
||||||
if (pageState.filters?.search) {
|
|
||||||
params.append('search', pageState.filters.search);
|
|
||||||
params.append('fuzzy', 'true');
|
|
||||||
|
|
||||||
// Add search option parameters if available
|
|
||||||
if (pageState.searchOptions) {
|
|
||||||
params.append('search_filename', pageState.searchOptions.filename.toString());
|
|
||||||
params.append('search_modelname', pageState.searchOptions.modelname.toString());
|
|
||||||
if (pageState.searchOptions.tags !== undefined) {
|
|
||||||
params.append('search_tags', pageState.searchOptions.tags.toString());
|
|
||||||
}
|
|
||||||
params.append('recursive', (pageState.searchOptions?.recursive ?? false).toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add filter parameters if active
|
|
||||||
if (pageState.filters) {
|
|
||||||
// Handle tags filters
|
|
||||||
if (pageState.filters.tags && pageState.filters.tags.length > 0) {
|
|
||||||
pageState.filters.tags.forEach(tag => {
|
|
||||||
params.append('tag', tag);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle base model filters
|
|
||||||
if (pageState.filters.baseModel && pageState.filters.baseModel.length > 0) {
|
|
||||||
pageState.filters.baseModel.forEach(model => {
|
|
||||||
params.append('base_model', model);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add model-specific parameters
|
|
||||||
if (modelType === 'lora') {
|
|
||||||
// Check for recipe-based filtering parameters from session storage
|
|
||||||
const filterLoraHash = getSessionItem('recipe_to_lora_filterLoraHash');
|
|
||||||
const filterLoraHashes = getSessionItem('recipe_to_lora_filterLoraHashes');
|
|
||||||
|
|
||||||
// Add hash filter parameter if present
|
|
||||||
if (filterLoraHash) {
|
|
||||||
params.append('lora_hash', filterLoraHash);
|
|
||||||
}
|
|
||||||
// Add multiple hashes filter if present
|
|
||||||
else if (filterLoraHashes) {
|
|
||||||
try {
|
|
||||||
if (Array.isArray(filterLoraHashes) && filterLoraHashes.length > 0) {
|
|
||||||
params.append('lora_hashes', filterLoraHashes.join(','));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error parsing lora hashes from session storage:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(`${endpoint}?${params}`);
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to fetch models: ${response.statusText}`);
|
throw new Error(`Failed to fetch ${this.apiConfig.config.displayName}s: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
@@ -106,262 +73,295 @@ export async function fetchModelsPage(options = {}) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error fetching ${modelType}s:`, error);
|
console.error(`Error fetching ${this.apiConfig.config.displayName}s:`, error);
|
||||||
showToast(`Failed to fetch ${modelType}s: ${error.message}`, 'error');
|
showToast(`Failed to fetch ${this.apiConfig.config.displayName}s: ${error.message}`, 'error');
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset and reload models using virtual scrolling
|
* Delete a model
|
||||||
* @param {Object} options - Operation options
|
|
||||||
* @returns {Promise<Object>} The fetch result
|
|
||||||
*/
|
*/
|
||||||
export async function resetAndReloadWithVirtualScroll(options = {}) {
|
async deleteModel(filePath) {
|
||||||
const {
|
|
||||||
modelType = 'lora',
|
|
||||||
updateFolders = false,
|
|
||||||
fetchPageFunction
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
const pageState = getCurrentPageState();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
pageState.isLoading = true;
|
state.loadingManager.showSimpleLoading(`Deleting ${this.apiConfig.config.singularName}...`);
|
||||||
|
|
||||||
// Reset page counter
|
const response = await fetch(this.apiConfig.endpoints.delete, {
|
||||||
pageState.currentPage = 1;
|
|
||||||
|
|
||||||
// Fetch the first page
|
|
||||||
const result = await fetchPageFunction(1, pageState.pageSize || 50);
|
|
||||||
|
|
||||||
// Update the virtual scroller
|
|
||||||
state.virtualScroller.refreshWithData(
|
|
||||||
result.items,
|
|
||||||
result.totalItems,
|
|
||||||
result.hasMore
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update state
|
|
||||||
pageState.hasMore = result.hasMore;
|
|
||||||
pageState.currentPage = 2; // Next page will be 2
|
|
||||||
|
|
||||||
// Update folders if needed
|
|
||||||
if (updateFolders && result.folders) {
|
|
||||||
updateFolderTags(result.folders);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error reloading ${modelType}s:`, error);
|
|
||||||
showToast(`Failed to reload ${modelType}s: ${error.message}`, 'error');
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
pageState.isLoading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load more models using virtual scrolling
|
|
||||||
* @param {Object} options - Operation options
|
|
||||||
* @returns {Promise<Object>} The fetch result
|
|
||||||
*/
|
|
||||||
export async function loadMoreWithVirtualScroll(options = {}) {
|
|
||||||
const {
|
|
||||||
modelType = 'lora',
|
|
||||||
resetPage = false,
|
|
||||||
updateFolders = false,
|
|
||||||
fetchPageFunction
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
const pageState = getCurrentPageState();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Start loading state
|
|
||||||
pageState.isLoading = true;
|
|
||||||
|
|
||||||
// Reset to first page if requested
|
|
||||||
if (resetPage) {
|
|
||||||
pageState.currentPage = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch the first page of data
|
|
||||||
const result = await fetchPageFunction(pageState.currentPage, pageState.pageSize || 50);
|
|
||||||
|
|
||||||
// Update virtual scroller with the new data
|
|
||||||
state.virtualScroller.refreshWithData(
|
|
||||||
result.items,
|
|
||||||
result.totalItems,
|
|
||||||
result.hasMore
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update state
|
|
||||||
pageState.hasMore = result.hasMore;
|
|
||||||
pageState.currentPage = 2; // Next page to load would be 2
|
|
||||||
|
|
||||||
// Update folders if needed
|
|
||||||
if (updateFolders && result.folders) {
|
|
||||||
updateFolderTags(result.folders);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error loading ${modelType}s:`, error);
|
|
||||||
showToast(`Failed to load ${modelType}s: ${error.message}`, 'error');
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
pageState.isLoading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update folder tags in the UI
|
|
||||||
export function updateFolderTags(folders) {
|
|
||||||
const folderTagsContainer = document.querySelector('.folder-tags');
|
|
||||||
if (!folderTagsContainer) return;
|
|
||||||
|
|
||||||
// Keep track of currently selected folder
|
|
||||||
const pageState = getCurrentPageState();
|
|
||||||
const currentFolder = pageState.activeFolder;
|
|
||||||
|
|
||||||
// Create HTML for folder tags
|
|
||||||
const tagsHTML = folders.map(folder => {
|
|
||||||
const isActive = folder === currentFolder;
|
|
||||||
return `<div class="tag ${isActive ? 'active' : ''}" data-folder="${folder}">${folder}</div>`;
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
// Update the container
|
|
||||||
folderTagsContainer.innerHTML = tagsHTML;
|
|
||||||
|
|
||||||
// Reattach click handlers and ensure the active tag is visible
|
|
||||||
const tags = folderTagsContainer.querySelectorAll('.tag');
|
|
||||||
tags.forEach(tag => {
|
|
||||||
if (typeof toggleFolder === 'function') {
|
|
||||||
tag.addEventListener('click', toggleFolder);
|
|
||||||
}
|
|
||||||
if (tag.dataset.folder === currentFolder) {
|
|
||||||
tag.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generic function to replace a model preview
|
|
||||||
export function replaceModelPreview(filePath, modelType = 'lora') {
|
|
||||||
// Open file picker
|
|
||||||
const input = document.createElement('input');
|
|
||||||
input.type = 'file';
|
|
||||||
input.accept ='image/*,video/mp4';
|
|
||||||
|
|
||||||
input.onchange = async function() {
|
|
||||||
if (!input.files || !input.files[0]) return;
|
|
||||||
|
|
||||||
const file = input.files[0];
|
|
||||||
await uploadPreview(filePath, file, modelType);
|
|
||||||
};
|
|
||||||
|
|
||||||
input.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete a model (generic)
|
|
||||||
export async function deleteModel(filePath, modelType = 'lora') {
|
|
||||||
try {
|
|
||||||
state.loadingManager.showSimpleLoading(`Deleting ${modelType}...`);
|
|
||||||
|
|
||||||
const endpoint = modelType === 'checkpoint'
|
|
||||||
? '/api/checkpoints/delete'
|
|
||||||
: '/api/loras/delete';
|
|
||||||
|
|
||||||
const response = await fetch(endpoint, {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: { 'Content-Type': 'application/json' },
|
||||||
'Content-Type': 'application/json',
|
body: JSON.stringify({ file_path: filePath })
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
file_path: filePath
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to delete ${modelType}: ${response.statusText}`);
|
throw new Error(`Failed to delete ${this.apiConfig.config.singularName}: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
// If virtual scroller exists, update its data
|
|
||||||
if (state.virtualScroller) {
|
if (state.virtualScroller) {
|
||||||
state.virtualScroller.removeItemByFilePath(filePath);
|
state.virtualScroller.removeItemByFilePath(filePath);
|
||||||
} else {
|
|
||||||
// Legacy approach: remove the card from UI directly
|
|
||||||
const card = document.querySelector(`.lora-card[data-filepath="${filePath}"]`);
|
|
||||||
if (card) {
|
|
||||||
card.remove();
|
|
||||||
}
|
}
|
||||||
}
|
showToast(`${this.apiConfig.config.displayName} deleted successfully`, 'success');
|
||||||
|
|
||||||
showToast(`${modelType} deleted successfully`, 'success');
|
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(data.error || `Failed to delete ${modelType}`);
|
throw new Error(data.error || `Failed to delete ${this.apiConfig.config.singularName}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error deleting ${modelType}:`, error);
|
console.error(`Error deleting ${this.apiConfig.config.singularName}:`, error);
|
||||||
showToast(`Failed to delete ${modelType}: ${error.message}`, 'error');
|
showToast(`Failed to delete ${this.apiConfig.config.singularName}: ${error.message}`, 'error');
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
state.loadingManager.hide();
|
state.loadingManager.hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generic function to refresh models
|
/**
|
||||||
export async function refreshModels(options = {}) {
|
* Exclude a model
|
||||||
const {
|
*/
|
||||||
modelType = 'lora',
|
async excludeModel(filePath) {
|
||||||
scanEndpoint = '/api/loras/scan',
|
|
||||||
resetAndReloadFunction,
|
|
||||||
fullRebuild = false // New parameter with default value false
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
state.loadingManager.showSimpleLoading(`${fullRebuild ? 'Full rebuild' : 'Refreshing'} ${modelType}s...`);
|
state.loadingManager.showSimpleLoading(`Excluding ${this.apiConfig.config.singularName}...`);
|
||||||
|
|
||||||
// Add fullRebuild parameter to the request
|
const response = await fetch(this.apiConfig.endpoints.exclude, {
|
||||||
const url = new URL(scanEndpoint, window.location.origin);
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ file_path: filePath })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to exclude ${this.apiConfig.config.singularName}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
if (state.virtualScroller) {
|
||||||
|
state.virtualScroller.removeItemByFilePath(filePath);
|
||||||
|
}
|
||||||
|
showToast(`${this.apiConfig.config.displayName} excluded successfully`, 'success');
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(data.error || `Failed to exclude ${this.apiConfig.config.singularName}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error excluding ${this.apiConfig.config.singularName}:`, error);
|
||||||
|
showToast(`Failed to exclude ${this.apiConfig.config.singularName}: ${error.message}`, 'error');
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
state.loadingManager.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rename a model file
|
||||||
|
*/
|
||||||
|
async renameModelFile(filePath, newFileName) {
|
||||||
|
try {
|
||||||
|
state.loadingManager.showSimpleLoading(`Renaming ${this.apiConfig.config.singularName} file...`);
|
||||||
|
|
||||||
|
const response = await fetch(this.apiConfig.endpoints.rename, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
file_path: filePath,
|
||||||
|
new_file_name: newFileName
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
state.virtualScroller.updateSingleItem(filePath, {
|
||||||
|
file_name: newFileName,
|
||||||
|
file_path: result.new_file_path,
|
||||||
|
preview_url: result.new_preview_path
|
||||||
|
});
|
||||||
|
|
||||||
|
showToast('File name updated successfully', 'success');
|
||||||
|
} else {
|
||||||
|
showToast('Failed to rename file: ' + (result.error || 'Unknown error'), 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error renaming ${this.apiConfig.config.singularName} file:`, error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
state.loadingManager.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace model preview
|
||||||
|
*/
|
||||||
|
replaceModelPreview(filePath) {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.accept = 'image/*,video/mp4';
|
||||||
|
|
||||||
|
input.onchange = async () => {
|
||||||
|
if (!input.files || !input.files[0]) return;
|
||||||
|
|
||||||
|
const file = input.files[0];
|
||||||
|
await this.uploadPreview(filePath, file);
|
||||||
|
};
|
||||||
|
|
||||||
|
input.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload preview image
|
||||||
|
*/
|
||||||
|
async uploadPreview(filePath, file, nsfwLevel = 0) {
|
||||||
|
try {
|
||||||
|
state.loadingManager.showSimpleLoading('Uploading preview...');
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('preview_file', file);
|
||||||
|
formData.append('model_path', filePath);
|
||||||
|
formData.append('nsfw_level', nsfwLevel.toString());
|
||||||
|
|
||||||
|
const response = await fetch(this.apiConfig.endpoints.replacePreview, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Upload failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const pageState = this.getPageState();
|
||||||
|
|
||||||
|
// Update the version timestamp
|
||||||
|
const timestamp = Date.now();
|
||||||
|
if (pageState.previewVersions) {
|
||||||
|
pageState.previewVersions.set(filePath, timestamp);
|
||||||
|
|
||||||
|
const storageKey = `${this.modelType}_preview_versions`;
|
||||||
|
saveMapToStorage(storageKey, pageState.previewVersions);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateData = {
|
||||||
|
preview_url: data.preview_url,
|
||||||
|
preview_nsfw_level: data.preview_nsfw_level
|
||||||
|
};
|
||||||
|
|
||||||
|
state.virtualScroller.updateSingleItem(filePath, updateData);
|
||||||
|
showToast('Preview updated successfully', 'success');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error uploading preview:', error);
|
||||||
|
showToast('Failed to upload preview image', 'error');
|
||||||
|
} finally {
|
||||||
|
state.loadingManager.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save model metadata
|
||||||
|
*/
|
||||||
|
async saveModelMetadata(filePath, data) {
|
||||||
|
try {
|
||||||
|
state.loadingManager.showSimpleLoading('Saving metadata...');
|
||||||
|
|
||||||
|
const response = await fetch(this.apiConfig.endpoints.save, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
file_path: filePath,
|
||||||
|
...data
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to save metadata');
|
||||||
|
}
|
||||||
|
|
||||||
|
state.virtualScroller.updateSingleItem(filePath, data);
|
||||||
|
return response.json();
|
||||||
|
} finally {
|
||||||
|
state.loadingManager.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh models (scan)
|
||||||
|
*/
|
||||||
|
async refreshModels(fullRebuild = false) {
|
||||||
|
try {
|
||||||
|
state.loadingManager.showSimpleLoading(
|
||||||
|
`${fullRebuild ? 'Full rebuild' : 'Refreshing'} ${this.apiConfig.config.displayName}s...`
|
||||||
|
);
|
||||||
|
|
||||||
|
const url = new URL(this.apiConfig.endpoints.scan, window.location.origin);
|
||||||
url.searchParams.append('full_rebuild', fullRebuild);
|
url.searchParams.append('full_rebuild', fullRebuild);
|
||||||
|
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to refresh ${modelType}s: ${response.status} ${response.statusText}`);
|
throw new Error(`Failed to refresh ${this.apiConfig.config.displayName}s: ${response.status} ${response.statusText}`);
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof resetAndReloadFunction === 'function') {
|
|
||||||
await resetAndReloadFunction(true); // update folders
|
|
||||||
}
|
}
|
||||||
|
|
||||||
showToast(`${fullRebuild ? 'Full rebuild' : 'Refresh'} complete`, 'success');
|
showToast(`${fullRebuild ? 'Full rebuild' : 'Refresh'} complete`, 'success');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Refresh failed:`, error);
|
console.error('Refresh failed:', error);
|
||||||
showToast(`Failed to ${fullRebuild ? 'rebuild' : 'refresh'} ${modelType}s`, 'error');
|
showToast(`Failed to ${fullRebuild ? 'rebuild' : 'refresh'} ${this.apiConfig.config.displayName}s`, 'error');
|
||||||
} finally {
|
} finally {
|
||||||
state.loadingManager.hide();
|
state.loadingManager.hide();
|
||||||
state.loadingManager.restoreProgressBar();
|
state.loadingManager.restoreProgressBar();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generic fetch from Civitai
|
/**
|
||||||
export async function fetchCivitaiMetadata(options = {}) {
|
* Fetch CivitAI metadata for single model
|
||||||
const {
|
*/
|
||||||
modelType = 'lora',
|
async refreshSingleModelMetadata(filePath) {
|
||||||
fetchEndpoint = '/api/fetch-all-civitai',
|
try {
|
||||||
resetAndReloadFunction
|
state.loadingManager.showSimpleLoading('Refreshing metadata...');
|
||||||
} = options;
|
|
||||||
|
|
||||||
|
const response = await fetch(this.apiConfig.endpoints.fetchCivitai, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ file_path: filePath })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to refresh metadata');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
if (data.metadata && state.virtualScroller) {
|
||||||
|
state.virtualScroller.updateSingleItem(filePath, data.metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast('Metadata refreshed successfully', 'success');
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(data.error || 'Failed to refresh metadata');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error refreshing metadata:', error);
|
||||||
|
showToast(error.message, 'error');
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
state.loadingManager.hide();
|
||||||
|
state.loadingManager.restoreProgressBar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch CivitAI metadata for all models
|
||||||
|
*/
|
||||||
|
async fetchCivitaiMetadata(resetAndReloadFunction) {
|
||||||
let ws = null;
|
let ws = null;
|
||||||
|
|
||||||
await state.loadingManager.showWithProgress(async (loading) => {
|
await state.loadingManager.showWithProgress(async (loading) => {
|
||||||
try {
|
try {
|
||||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||||
ws = new WebSocket(`${wsProtocol}${window.location.host}/ws/fetch-progress`);
|
ws = new WebSocket(`${wsProtocol}${window.location.host}${WS_ENDPOINTS.fetchProgress}`);
|
||||||
|
|
||||||
const operationComplete = new Promise((resolve, reject) => {
|
const operationComplete = new Promise((resolve, reject) => {
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
@@ -383,7 +383,7 @@ export async function fetchCivitaiMetadata(options = {}) {
|
|||||||
case 'completed':
|
case 'completed':
|
||||||
loading.setProgress(100);
|
loading.setProgress(100);
|
||||||
loading.setStatus(
|
loading.setStatus(
|
||||||
`Completed: Updated ${data.success} of ${data.processed} ${modelType}s`
|
`Completed: Updated ${data.success} of ${data.processed} ${this.apiConfig.config.displayName}s`
|
||||||
);
|
);
|
||||||
resolve();
|
resolve();
|
||||||
break;
|
break;
|
||||||
@@ -404,14 +404,10 @@ export async function fetchCivitaiMetadata(options = {}) {
|
|||||||
ws.onerror = reject;
|
ws.onerror = reject;
|
||||||
});
|
});
|
||||||
|
|
||||||
const requestBody = modelType === 'checkpoint'
|
const response = await fetch(this.apiConfig.endpoints.fetchAllCivitai, {
|
||||||
? JSON.stringify({ model_type: 'checkpoint' })
|
|
||||||
: JSON.stringify({});
|
|
||||||
|
|
||||||
const response = await fetch(fetchEndpoint, {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: requestBody
|
body: JSON.stringify({})
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -438,196 +434,141 @@ export async function fetchCivitaiMetadata(options = {}) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generic function to refresh single model metadata
|
/**
|
||||||
export async function refreshSingleModelMetadata(filePath, modelType = 'lora') {
|
* Build query parameters for API requests
|
||||||
try {
|
*/
|
||||||
state.loadingManager.showSimpleLoading('Refreshing metadata...');
|
_buildQueryParams(baseParams, pageState) {
|
||||||
|
const params = new URLSearchParams(baseParams);
|
||||||
|
|
||||||
const endpoint = modelType === 'checkpoint'
|
// Add common parameters
|
||||||
? '/api/checkpoints/fetch-civitai'
|
if (pageState.activeFolder !== null) {
|
||||||
: '/api/loras/fetch-civitai';
|
params.append('folder', pageState.activeFolder);
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch(endpoint, {
|
if (pageState.showFavoritesOnly) {
|
||||||
method: 'POST',
|
params.append('favorites_only', 'true');
|
||||||
headers: {
|
}
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
// Add letter filter for supported model types
|
||||||
body: JSON.stringify({ file_path: filePath })
|
if (this.apiConfig.config.supportsLetterFilter && pageState.activeLetterFilter) {
|
||||||
|
params.append('first_letter', pageState.activeLetterFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add search parameters
|
||||||
|
if (pageState.filters?.search) {
|
||||||
|
params.append('search', pageState.filters.search);
|
||||||
|
params.append('fuzzy', 'true');
|
||||||
|
|
||||||
|
if (pageState.searchOptions) {
|
||||||
|
params.append('search_filename', pageState.searchOptions.filename.toString());
|
||||||
|
params.append('search_modelname', pageState.searchOptions.modelname.toString());
|
||||||
|
if (pageState.searchOptions.tags !== undefined) {
|
||||||
|
params.append('search_tags', pageState.searchOptions.tags.toString());
|
||||||
|
}
|
||||||
|
params.append('recursive', (pageState.searchOptions?.recursive ?? false).toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add filter parameters
|
||||||
|
if (pageState.filters) {
|
||||||
|
if (pageState.filters.tags && pageState.filters.tags.length > 0) {
|
||||||
|
pageState.filters.tags.forEach(tag => {
|
||||||
|
params.append('tag', tag);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to refresh metadata');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
if (pageState.filters.baseModel && pageState.filters.baseModel.length > 0) {
|
||||||
|
pageState.filters.baseModel.forEach(model => {
|
||||||
if (data.success) {
|
params.append('base_model', model);
|
||||||
// Use the returned metadata to update just this single item
|
});
|
||||||
if (data.metadata && state.virtualScroller) {
|
}
|
||||||
state.virtualScroller.updateSingleItem(filePath, data.metadata);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
showToast('Metadata refreshed successfully', 'success');
|
// Add model-specific parameters
|
||||||
return true;
|
this._addModelSpecificParams(params, pageState);
|
||||||
} else {
|
|
||||||
throw new Error(data.error || 'Failed to refresh metadata');
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add model-specific parameters to query
|
||||||
|
*/
|
||||||
|
_addModelSpecificParams(params, pageState) {
|
||||||
|
// Override in specific implementations or handle via configuration
|
||||||
|
if (this.modelType === 'loras') {
|
||||||
|
const filterLoraHash = getSessionItem('recipe_to_lora_filterLoraHash');
|
||||||
|
const filterLoraHashes = getSessionItem('recipe_to_lora_filterLoraHashes');
|
||||||
|
|
||||||
|
if (filterLoraHash) {
|
||||||
|
params.append('lora_hash', filterLoraHash);
|
||||||
|
} else if (filterLoraHashes) {
|
||||||
|
try {
|
||||||
|
if (Array.isArray(filterLoraHashes) && filterLoraHashes.length > 0) {
|
||||||
|
params.append('lora_hashes', filterLoraHashes.join(','));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error refreshing metadata:', error);
|
console.error('Error parsing lora hashes from session storage:', error);
|
||||||
showToast(error.message, 'error');
|
}
|
||||||
return false;
|
}
|
||||||
} finally {
|
}
|
||||||
state.loadingManager.hide();
|
|
||||||
state.loadingManager.restoreProgressBar();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generic function to exclude a model
|
// Export factory functions and utilities
|
||||||
export async function excludeModel(filePath, modelType = 'lora') {
|
export function createModelApiClient(modelType = null) {
|
||||||
try {
|
return new ModelApiClient(modelType);
|
||||||
state.loadingManager.showSimpleLoading(`Excluding ${modelType}...`);
|
|
||||||
|
|
||||||
const endpoint = modelType === 'checkpoint'
|
|
||||||
? '/api/checkpoints/exclude'
|
|
||||||
: '/api/loras/exclude';
|
|
||||||
|
|
||||||
const response = await fetch(endpoint, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
file_path: filePath
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to exclude ${modelType}: ${response.statusText}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
let _singletonClient = null;
|
||||||
|
|
||||||
if (data.success) {
|
export function getModelApiClient() {
|
||||||
// If virtual scroller exists, update its data
|
if (!_singletonClient) {
|
||||||
if (state.virtualScroller) {
|
_singletonClient = new ModelApiClient();
|
||||||
state.virtualScroller.removeItemByFilePath(filePath);
|
|
||||||
} else {
|
|
||||||
// Legacy approach: remove the card from UI directly
|
|
||||||
const card = document.querySelector(`.lora-card[data-filepath="${filePath}"]`);
|
|
||||||
if (card) {
|
|
||||||
card.remove();
|
|
||||||
}
|
}
|
||||||
|
_singletonClient.setModelType(state.currentPageType);
|
||||||
|
return _singletonClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
showToast(`${modelType} excluded successfully`, 'success');
|
// Legacy compatibility exports
|
||||||
return true;
|
export async function fetchModelsPage(options = {}) {
|
||||||
} else {
|
const { modelType = getCurrentModelType(), ...rest } = options;
|
||||||
throw new Error(data.error || `Failed to exclude ${modelType}`);
|
const client = createModelApiClient(modelType);
|
||||||
}
|
return client.fetchModelsPage(rest.page, rest.pageSize);
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error excluding ${modelType}:`, error);
|
|
||||||
showToast(`Failed to exclude ${modelType}: ${error.message}`, 'error');
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
state.loadingManager.hide();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload a preview image
|
export async function deleteModel(filePath, modelType = null) {
|
||||||
export async function uploadPreview(filePath, file, modelType = 'lora', nsfwLevel = 0) {
|
const client = createModelApiClient(modelType);
|
||||||
try {
|
return client.deleteModel(filePath);
|
||||||
state.loadingManager.showSimpleLoading('Uploading preview...');
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
|
|
||||||
// Prepare common form data
|
|
||||||
formData.append('preview_file', file);
|
|
||||||
formData.append('model_path', filePath);
|
|
||||||
formData.append('nsfw_level', nsfwLevel.toString()); // Add nsfw_level parameter
|
|
||||||
|
|
||||||
// Set endpoint based on model type
|
|
||||||
const endpoint = modelType === 'checkpoint'
|
|
||||||
? '/api/checkpoints/replace-preview'
|
|
||||||
: '/api/loras/replace_preview';
|
|
||||||
|
|
||||||
const response = await fetch(endpoint, {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Upload failed');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
export async function excludeModel(filePath, modelType = null) {
|
||||||
|
const client = createModelApiClient(modelType);
|
||||||
// Get the current page's previewVersions Map based on model type
|
return client.excludeModel(filePath);
|
||||||
const pageType = modelType === 'checkpoint' ? 'checkpoints' : 'loras';
|
|
||||||
const previewVersions = state.pages[pageType].previewVersions;
|
|
||||||
|
|
||||||
// Update the version timestamp
|
|
||||||
const timestamp = Date.now();
|
|
||||||
if (previewVersions) {
|
|
||||||
previewVersions.set(filePath, timestamp);
|
|
||||||
|
|
||||||
// Save the updated Map to localStorage
|
|
||||||
const storageKey = modelType === 'checkpoint' ? 'checkpoint_preview_versions' : 'lora_preview_versions';
|
|
||||||
saveMapToStorage(storageKey, previewVersions);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateData = {
|
export async function renameModelFile(filePath, newFileName, modelType = null) {
|
||||||
preview_url: data.preview_url,
|
const client = createModelApiClient(modelType);
|
||||||
preview_nsfw_level: data.preview_nsfw_level // Include nsfw level in update data
|
return client.renameModelFile(filePath, newFileName);
|
||||||
};
|
|
||||||
|
|
||||||
state.virtualScroller.updateSingleItem(filePath, updateData);
|
|
||||||
|
|
||||||
showToast('Preview updated successfully', 'success');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error uploading preview:', error);
|
|
||||||
showToast('Failed to upload preview image', 'error');
|
|
||||||
} finally {
|
|
||||||
state.loadingManager.hide();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Private methods
|
export async function replaceModelPreview(filePath, modelType = null) {
|
||||||
|
const client = createModelApiClient(modelType);
|
||||||
// Private function to perform the delete operation
|
return client.replaceModelPreview(filePath);
|
||||||
async function performDelete(filePath, modelType = 'lora') {
|
|
||||||
try {
|
|
||||||
showToast(`Deleting ${modelType}...`, 'info');
|
|
||||||
|
|
||||||
const response = await fetch('/api/model/delete', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
file_path: filePath,
|
|
||||||
model_type: modelType
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to delete ${modelType}: ${response.status} ${response.statusText}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
export async function refreshModels(options = {}) {
|
||||||
|
const { modelType = getCurrentModelType(), fullRebuild = false } = options;
|
||||||
if (data.success) {
|
const client = createModelApiClient(modelType);
|
||||||
// Remove the card from UI
|
return client.refreshModels(fullRebuild);
|
||||||
const card = document.querySelector(`.lora-card[data-filepath="${filePath}"]`);
|
|
||||||
if (card) {
|
|
||||||
card.remove();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
showToast(`${modelType} deleted successfully`, 'success');
|
export async function refreshSingleModelMetadata(filePath, modelType = null) {
|
||||||
} else {
|
const client = createModelApiClient(modelType);
|
||||||
throw new Error(data.error || `Failed to delete ${modelType}`);
|
return client.refreshSingleModelMetadata(filePath);
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error deleting ${modelType}:`, error);
|
|
||||||
showToast(`Failed to delete ${modelType}: ${error.message}`, 'error');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchCivitaiMetadata(options = {}) {
|
||||||
|
const { modelType = getCurrentModelType(), resetAndReloadFunction } = options;
|
||||||
|
const client = createModelApiClient(modelType);
|
||||||
|
return client.fetchCivitaiMetadata(resetAndReloadFunction);
|
||||||
}
|
}
|
||||||
@@ -1,165 +1,43 @@
|
|||||||
import {
|
import { createModelApiClient } from './baseModelApi.js';
|
||||||
fetchModelsPage,
|
import { MODEL_TYPES } from './apiConfig.js';
|
||||||
resetAndReloadWithVirtualScroll,
|
|
||||||
loadMoreWithVirtualScroll,
|
|
||||||
refreshModels as baseRefreshModels,
|
|
||||||
deleteModel as baseDeleteModel,
|
|
||||||
replaceModelPreview,
|
|
||||||
fetchCivitaiMetadata,
|
|
||||||
refreshSingleModelMetadata,
|
|
||||||
excludeModel as baseExcludeModel
|
|
||||||
} from './baseModelApi.js';
|
|
||||||
import { state } from '../state/index.js';
|
|
||||||
|
|
||||||
/**
|
// Create Checkpoint-specific API client
|
||||||
* Fetch checkpoints with pagination for virtual scrolling
|
const checkpointApiClient = createModelApiClient(MODEL_TYPES.CHECKPOINT);
|
||||||
* @param {number} page - Page number to fetch
|
|
||||||
* @param {number} pageSize - Number of items per page
|
|
||||||
* @returns {Promise<Object>} Object containing items, total count, and pagination info
|
|
||||||
*/
|
|
||||||
export async function fetchCheckpointsPage(page = 1, pageSize = 100) {
|
|
||||||
return fetchModelsPage({
|
|
||||||
modelType: 'checkpoint',
|
|
||||||
page,
|
|
||||||
pageSize,
|
|
||||||
endpoint: '/api/checkpoints'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// Export all common operations using the unified client
|
||||||
* Load more checkpoints with pagination - updated to work with VirtualScroller
|
export const deleteModel = (filePath) => checkpointApiClient.deleteModel(filePath);
|
||||||
* @param {boolean} resetPage - Whether to reset to the first page
|
export const excludeCheckpoint = (filePath) => checkpointApiClient.excludeModel(filePath);
|
||||||
* @param {boolean} updateFolders - Whether to update folder tags
|
export const renameCheckpointFile = (filePath, newFileName) => checkpointApiClient.renameModelFile(filePath, newFileName);
|
||||||
* @returns {Promise<void>}
|
export const replacePreview = (filePath) => checkpointApiClient.replaceModelPreview(filePath);
|
||||||
*/
|
export const saveModelMetadata = (filePath, data) => checkpointApiClient.saveModelMetadata(filePath, data);
|
||||||
|
export const refreshCheckpoints = (fullRebuild = false) => checkpointApiClient.refreshModels(fullRebuild);
|
||||||
|
export const refreshSingleCheckpointMetadata = (filePath) => checkpointApiClient.refreshSingleModelMetadata(filePath);
|
||||||
|
export const fetchCivitai = (resetAndReloadFunction) => checkpointApiClient.fetchCivitaiMetadata(resetAndReloadFunction);
|
||||||
|
|
||||||
|
// Pagination functions
|
||||||
|
export const fetchCheckpointsPage = (page = 1, pageSize = 50) => checkpointApiClient.fetchModelsPage(page, pageSize);
|
||||||
|
|
||||||
|
// Virtual scrolling operations
|
||||||
export async function loadMoreCheckpoints(resetPage = false, updateFolders = false) {
|
export async function loadMoreCheckpoints(resetPage = false, updateFolders = false) {
|
||||||
return loadMoreWithVirtualScroll({
|
return checkpointApiClient.loadMoreWithVirtualScroll(resetPage, updateFolders);
|
||||||
modelType: 'checkpoint',
|
|
||||||
resetPage,
|
|
||||||
updateFolders,
|
|
||||||
fetchPageFunction: fetchCheckpointsPage
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset and reload checkpoints
|
|
||||||
export async function resetAndReload(updateFolders = false) {
|
export async function resetAndReload(updateFolders = false) {
|
||||||
return resetAndReloadWithVirtualScroll({
|
return checkpointApiClient.resetAndReloadWithVirtualScroll(updateFolders);
|
||||||
modelType: 'checkpoint',
|
|
||||||
updateFolders,
|
|
||||||
fetchPageFunction: fetchCheckpointsPage
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh checkpoints
|
// Checkpoint-specific functions
|
||||||
export async function refreshCheckpoints(fullRebuild = false) {
|
export async function getCheckpointInfo(name) {
|
||||||
return baseRefreshModels({
|
|
||||||
modelType: 'checkpoint',
|
|
||||||
scanEndpoint: '/api/checkpoints/scan',
|
|
||||||
resetAndReloadFunction: resetAndReload,
|
|
||||||
fullRebuild: fullRebuild
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete a checkpoint
|
|
||||||
export function deleteCheckpoint(filePath) {
|
|
||||||
return baseDeleteModel(filePath, 'checkpoint');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace checkpoint preview
|
|
||||||
export function replaceCheckpointPreview(filePath) {
|
|
||||||
return replaceModelPreview(filePath, 'checkpoint');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch metadata from Civitai for checkpoints
|
|
||||||
export async function fetchCivitai() {
|
|
||||||
return fetchCivitaiMetadata({
|
|
||||||
modelType: 'checkpoint',
|
|
||||||
fetchEndpoint: '/api/checkpoints/fetch-all-civitai',
|
|
||||||
resetAndReloadFunction: resetAndReload
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refresh single checkpoint metadata
|
|
||||||
export async function refreshSingleCheckpointMetadata(filePath) {
|
|
||||||
await refreshSingleModelMetadata(filePath, 'checkpoint');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save model metadata to the server
|
|
||||||
* @param {string} filePath - Path to the model file
|
|
||||||
* @param {Object} data - Metadata to save
|
|
||||||
* @returns {Promise} - Promise that resolves with the server response
|
|
||||||
*/
|
|
||||||
export async function saveModelMetadata(filePath, data) {
|
|
||||||
try {
|
try {
|
||||||
// Show loading indicator
|
const response = await fetch(`${checkpointApiClient.apiConfig.endpoints.specific.info}/${encodeURIComponent(name)}`);
|
||||||
state.loadingManager.showSimpleLoading('Saving metadata...');
|
|
||||||
|
|
||||||
const response = await fetch('/api/checkpoints/save-metadata', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
file_path: filePath,
|
|
||||||
...data
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to save metadata');
|
throw new Error(`Failed to fetch checkpoint info: ${response.statusText}`);
|
||||||
}
|
|
||||||
|
|
||||||
// Update the virtual scroller with the new metadata
|
|
||||||
state.virtualScroller.updateSingleItem(filePath, data);
|
|
||||||
|
|
||||||
return response.json();
|
|
||||||
} finally {
|
|
||||||
// Always hide the loading indicator when done
|
|
||||||
state.loadingManager.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exclude a checkpoint model from being shown in the UI
|
|
||||||
* @param {string} filePath - File path of the checkpoint to exclude
|
|
||||||
* @returns {Promise<boolean>} Promise resolving to success status
|
|
||||||
*/
|
|
||||||
export function excludeCheckpoint(filePath) {
|
|
||||||
return baseExcludeModel(filePath, 'checkpoint');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rename a checkpoint file
|
|
||||||
* @param {string} filePath - Current file path
|
|
||||||
* @param {string} newFileName - New file name (without path)
|
|
||||||
* @returns {Promise<Object>} - Promise that resolves with the server response
|
|
||||||
*/
|
|
||||||
export async function renameCheckpointFile(filePath, newFileName) {
|
|
||||||
try {
|
|
||||||
// Show loading indicator
|
|
||||||
state.loadingManager.showSimpleLoading('Renaming checkpoint file...');
|
|
||||||
|
|
||||||
const response = await fetch('/api/checkpoints/rename', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
file_path: filePath,
|
|
||||||
new_file_name: newFileName
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Server returned ${response.status}: ${response.statusText}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return await response.json();
|
return await response.json();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error renaming checkpoint file:', error);
|
console.error('Error fetching checkpoint info:', error);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
|
||||||
state.loadingManager.hide();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,131 +1,35 @@
|
|||||||
import {
|
import { createModelApiClient } from './baseModelApi.js';
|
||||||
fetchModelsPage,
|
import { MODEL_TYPES } from './apiConfig.js';
|
||||||
resetAndReloadWithVirtualScroll,
|
|
||||||
loadMoreWithVirtualScroll,
|
|
||||||
refreshModels as baseRefreshModels,
|
|
||||||
deleteModel as baseDeleteModel,
|
|
||||||
replaceModelPreview,
|
|
||||||
fetchCivitaiMetadata,
|
|
||||||
refreshSingleModelMetadata,
|
|
||||||
excludeModel as baseExcludeModel
|
|
||||||
} from './baseModelApi.js';
|
|
||||||
import { state } from '../state/index.js';
|
|
||||||
|
|
||||||
/**
|
// Create LoRA-specific API client
|
||||||
* Save model metadata to the server
|
const loraApiClient = createModelApiClient(MODEL_TYPES.LORA);
|
||||||
* @param {string} filePath - File path
|
|
||||||
* @param {Object} data - Data to save
|
|
||||||
* @returns {Promise} Promise of the save operation
|
|
||||||
*/
|
|
||||||
export async function saveModelMetadata(filePath, data) {
|
|
||||||
try {
|
|
||||||
// Show loading indicator
|
|
||||||
state.loadingManager.showSimpleLoading('Saving metadata...');
|
|
||||||
|
|
||||||
const response = await fetch('/api/loras/save-metadata', {
|
// Export all common operations using the unified client
|
||||||
method: 'POST',
|
export const deleteModel = (filePath) => loraApiClient.deleteModel(filePath);
|
||||||
headers: {
|
export const excludeLora = (filePath) => loraApiClient.excludeModel(filePath);
|
||||||
'Content-Type': 'application/json',
|
export const renameLoraFile = (filePath, newFileName) => loraApiClient.renameModelFile(filePath, newFileName);
|
||||||
},
|
export const replacePreview = (filePath) => loraApiClient.replaceModelPreview(filePath);
|
||||||
body: JSON.stringify({
|
export const saveModelMetadata = (filePath, data) => loraApiClient.saveModelMetadata(filePath, data);
|
||||||
file_path: filePath,
|
export const refreshLoras = (fullRebuild = false) => loraApiClient.refreshModels(fullRebuild);
|
||||||
...data
|
export const refreshSingleLoraMetadata = (filePath) => loraApiClient.refreshSingleModelMetadata(filePath);
|
||||||
})
|
export const fetchCivitai = (resetAndReloadFunction) => loraApiClient.fetchCivitaiMetadata(resetAndReloadFunction);
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
// Pagination functions
|
||||||
throw new Error('Failed to save metadata');
|
export const fetchLorasPage = (page = 1, pageSize = 100) => loraApiClient.fetchModelsPage(page, pageSize);
|
||||||
}
|
|
||||||
|
|
||||||
// Update the virtual scroller with the new data
|
// Virtual scrolling operations
|
||||||
state.virtualScroller.updateSingleItem(filePath, data);
|
|
||||||
|
|
||||||
return response.json();
|
|
||||||
} finally {
|
|
||||||
// Always hide the loading indicator when done
|
|
||||||
state.loadingManager.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exclude a lora model from being shown in the UI
|
|
||||||
* @param {string} filePath - File path of the model to exclude
|
|
||||||
* @returns {Promise<boolean>} Promise resolving to success status
|
|
||||||
*/
|
|
||||||
export async function excludeLora(filePath) {
|
|
||||||
return baseExcludeModel(filePath, 'lora');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load more loras with pagination - updated to work with VirtualScroller
|
|
||||||
* @param {boolean} resetPage - Whether to reset to the first page
|
|
||||||
* @param {boolean} updateFolders - Whether to update folder tags
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
export async function loadMoreLoras(resetPage = false, updateFolders = false) {
|
export async function loadMoreLoras(resetPage = false, updateFolders = false) {
|
||||||
return loadMoreWithVirtualScroll({
|
return loraApiClient.loadMoreWithVirtualScroll(resetPage, updateFolders);
|
||||||
modelType: 'lora',
|
|
||||||
resetPage,
|
|
||||||
updateFolders,
|
|
||||||
fetchPageFunction: fetchLorasPage
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch loras with pagination for virtual scrolling
|
|
||||||
* @param {number} page - Page number to fetch
|
|
||||||
* @param {number} pageSize - Number of items per page
|
|
||||||
* @returns {Promise<Object>} Object containing items, total count, and pagination info
|
|
||||||
*/
|
|
||||||
export async function fetchLorasPage(page = 1, pageSize = 100) {
|
|
||||||
return fetchModelsPage({
|
|
||||||
modelType: 'lora',
|
|
||||||
page,
|
|
||||||
pageSize,
|
|
||||||
endpoint: '/api/loras'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchCivitai() {
|
|
||||||
return fetchCivitaiMetadata({
|
|
||||||
modelType: 'lora',
|
|
||||||
fetchEndpoint: '/api/loras/fetch-all-civitai',
|
|
||||||
resetAndReloadFunction: resetAndReload
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteModel(filePath) {
|
|
||||||
return baseDeleteModel(filePath, 'lora');
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function replacePreview(filePath) {
|
|
||||||
return replaceModelPreview(filePath, 'lora');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resetAndReload(updateFolders = false) {
|
export async function resetAndReload(updateFolders = false) {
|
||||||
return resetAndReloadWithVirtualScroll({
|
return loraApiClient.resetAndReloadWithVirtualScroll(updateFolders);
|
||||||
modelType: 'lora',
|
|
||||||
updateFolders,
|
|
||||||
fetchPageFunction: fetchLorasPage
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function refreshLoras(fullRebuild = false) {
|
|
||||||
return baseRefreshModels({
|
|
||||||
modelType: 'lora',
|
|
||||||
scanEndpoint: '/api/loras/scan',
|
|
||||||
resetAndReloadFunction: resetAndReload,
|
|
||||||
fullRebuild: fullRebuild
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function refreshSingleLoraMetadata(filePath) {
|
|
||||||
await refreshSingleModelMetadata(filePath, 'lora');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoRA-specific functions that don't have common equivalents
|
||||||
export async function fetchModelDescription(modelId, filePath) {
|
export async function fetchModelDescription(modelId, filePath) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/loras/model-description?model_id=${modelId}&file_path=${encodeURIComponent(filePath)}`);
|
const response = await fetch(`${loraApiClient.apiConfig.endpoints.specific.modelDescription}?model_id=${modelId}&file_path=${encodeURIComponent(filePath)}`);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to fetch model description: ${response.statusText}`);
|
throw new Error(`Failed to fetch model description: ${response.statusText}`);
|
||||||
@@ -138,38 +42,47 @@ export async function fetchModelDescription(modelId, filePath) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Move operations (LoRA-specific)
|
||||||
* Rename a LoRA file
|
export async function moveModel(filePath, targetPath) {
|
||||||
* @param {string} filePath - Current file path
|
|
||||||
* @param {string} newFileName - New file name (without path)
|
|
||||||
* @returns {Promise<Object>} - Promise that resolves with the server response
|
|
||||||
*/
|
|
||||||
export async function renameLoraFile(filePath, newFileName) {
|
|
||||||
try {
|
try {
|
||||||
// Show loading indicator
|
const response = await fetch(loraApiClient.apiConfig.endpoints.specific.moveModel, {
|
||||||
state.loadingManager.showSimpleLoading('Renaming LoRA file...');
|
|
||||||
|
|
||||||
const response = await fetch('/api/loras/rename', {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: { 'Content-Type': 'application/json' },
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
file_path: filePath,
|
file_path: filePath,
|
||||||
new_file_name: newFileName
|
target_path: targetPath
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Server returned ${response.status}: ${response.statusText}`);
|
throw new Error('Failed to move model');
|
||||||
}
|
}
|
||||||
|
|
||||||
return await response.json();
|
return await response.json();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error renaming LoRA file:', error);
|
console.error('Error moving model:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function moveModelsBulk(filePaths, targetPath) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(loraApiClient.apiConfig.endpoints.specific.moveBulk, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
file_paths: filePaths,
|
||||||
|
target_path: targetPath
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to move models');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error moving models in bulk:', error);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
|
||||||
// Hide loading indicator
|
|
||||||
state.loadingManager.hide();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,4 @@
|
|||||||
import { RecipeCard } from '../components/RecipeCard.js';
|
import { RecipeCard } from '../components/RecipeCard.js';
|
||||||
import {
|
|
||||||
resetAndReloadWithVirtualScroll,
|
|
||||||
loadMoreWithVirtualScroll
|
|
||||||
} from './baseModelApi.js';
|
|
||||||
import { state, getCurrentPageState } from '../state/index.js';
|
import { state, getCurrentPageState } from '../state/index.js';
|
||||||
import { showToast } from '../utils/uiHelpers.js';
|
import { showToast } from '../utils/uiHelpers.js';
|
||||||
|
|
||||||
@@ -98,6 +94,98 @@ export async function fetchRecipesPage(page = 1, pageSize = 100) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset and reload models using virtual scrolling
|
||||||
|
* @param {Object} options - Operation options
|
||||||
|
* @returns {Promise<Object>} The fetch result
|
||||||
|
*/
|
||||||
|
export async function resetAndReloadWithVirtualScroll(options = {}) {
|
||||||
|
const {
|
||||||
|
modelType = 'lora',
|
||||||
|
updateFolders = false,
|
||||||
|
fetchPageFunction
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const pageState = getCurrentPageState();
|
||||||
|
|
||||||
|
try {
|
||||||
|
pageState.isLoading = true;
|
||||||
|
|
||||||
|
// Reset page counter
|
||||||
|
pageState.currentPage = 1;
|
||||||
|
|
||||||
|
// Fetch the first page
|
||||||
|
const result = await fetchPageFunction(1, pageState.pageSize || 50);
|
||||||
|
|
||||||
|
// Update the virtual scroller
|
||||||
|
state.virtualScroller.refreshWithData(
|
||||||
|
result.items,
|
||||||
|
result.totalItems,
|
||||||
|
result.hasMore
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update state
|
||||||
|
pageState.hasMore = result.hasMore;
|
||||||
|
pageState.currentPage = 2; // Next page will be 2
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error reloading ${modelType}s:`, error);
|
||||||
|
showToast(`Failed to reload ${modelType}s: ${error.message}`, 'error');
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
pageState.isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load more models using virtual scrolling
|
||||||
|
* @param {Object} options - Operation options
|
||||||
|
* @returns {Promise<Object>} The fetch result
|
||||||
|
*/
|
||||||
|
export async function loadMoreWithVirtualScroll(options = {}) {
|
||||||
|
const {
|
||||||
|
modelType = 'lora',
|
||||||
|
resetPage = false,
|
||||||
|
updateFolders = false,
|
||||||
|
fetchPageFunction
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const pageState = getCurrentPageState();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Start loading state
|
||||||
|
pageState.isLoading = true;
|
||||||
|
|
||||||
|
// Reset to first page if requested
|
||||||
|
if (resetPage) {
|
||||||
|
pageState.currentPage = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the first page of data
|
||||||
|
const result = await fetchPageFunction(pageState.currentPage, pageState.pageSize || 50);
|
||||||
|
|
||||||
|
// Update virtual scroller with the new data
|
||||||
|
state.virtualScroller.refreshWithData(
|
||||||
|
result.items,
|
||||||
|
result.totalItems,
|
||||||
|
result.hasMore
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update state
|
||||||
|
pageState.hasMore = result.hasMore;
|
||||||
|
pageState.currentPage = 2; // Next page to load would be 2
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error loading ${modelType}s:`, error);
|
||||||
|
showToast(`Failed to load ${modelType}s: ${error.message}`, 'error');
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
pageState.isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset and reload recipes using virtual scrolling
|
* Reset and reload recipes using virtual scrolling
|
||||||
* @param {boolean} updateFolders - Whether to update folder tags
|
* @param {boolean} updateFolders - Whether to update folder tags
|
||||||
|
|||||||
@@ -5,18 +5,19 @@ import { loadMoreCheckpoints } from './api/checkpointApi.js';
|
|||||||
import { CheckpointDownloadManager } from './managers/CheckpointDownloadManager.js';
|
import { CheckpointDownloadManager } from './managers/CheckpointDownloadManager.js';
|
||||||
import { CheckpointContextMenu } from './components/ContextMenu/index.js';
|
import { CheckpointContextMenu } from './components/ContextMenu/index.js';
|
||||||
import { ModelDuplicatesManager } from './components/ModelDuplicatesManager.js';
|
import { ModelDuplicatesManager } from './components/ModelDuplicatesManager.js';
|
||||||
|
import { MODEL_TYPES } from './api/apiConfig.js';
|
||||||
|
|
||||||
// Initialize the Checkpoints page
|
// Initialize the Checkpoints page
|
||||||
class CheckpointsPageManager {
|
class CheckpointsPageManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
// Initialize page controls
|
// Initialize page controls
|
||||||
this.pageControls = createPageControls('checkpoints');
|
this.pageControls = createPageControls(MODEL_TYPES.CHECKPOINT);
|
||||||
|
|
||||||
// Initialize checkpoint download manager
|
// Initialize checkpoint download manager
|
||||||
window.checkpointDownloadManager = new CheckpointDownloadManager();
|
window.checkpointDownloadManager = new CheckpointDownloadManager();
|
||||||
|
|
||||||
// Initialize the ModelDuplicatesManager
|
// Initialize the ModelDuplicatesManager
|
||||||
this.duplicatesManager = new ModelDuplicatesManager(this, 'checkpoints');
|
this.duplicatesManager = new ModelDuplicatesManager(this, MODEL_TYPES.CHECKPOINT);
|
||||||
|
|
||||||
// Expose only necessary functions to global scope
|
// Expose only necessary functions to global scope
|
||||||
this._exposeRequiredGlobalFunctions();
|
this._exposeRequiredGlobalFunctions();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { BaseContextMenu } from './BaseContextMenu.js';
|
import { BaseContextMenu } from './BaseContextMenu.js';
|
||||||
import { ModelContextMenuMixin } from './ModelContextMenuMixin.js';
|
import { ModelContextMenuMixin } from './ModelContextMenuMixin.js';
|
||||||
import { refreshSingleCheckpointMetadata, saveModelMetadata, replaceCheckpointPreview, resetAndReload } from '../../api/checkpointApi.js';
|
import { resetAndReload } from '../../api/checkpointApi.js';
|
||||||
|
import { getModelApiClient } from '../../api/baseModelApi.js';
|
||||||
import { showToast } from '../../utils/uiHelpers.js';
|
import { showToast } from '../../utils/uiHelpers.js';
|
||||||
import { showExcludeModal } from '../../utils/modalUtils.js';
|
import { showExcludeModal } from '../../utils/modalUtils.js';
|
||||||
|
|
||||||
@@ -19,7 +20,7 @@ export class CheckpointContextMenu extends BaseContextMenu {
|
|||||||
|
|
||||||
// Implementation needed by the mixin
|
// Implementation needed by the mixin
|
||||||
async saveModelMetadata(filePath, data) {
|
async saveModelMetadata(filePath, data) {
|
||||||
return saveModelMetadata(filePath, data);
|
return getModelApiClient().saveModelMetadata(filePath, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMenuAction(action) {
|
handleMenuAction(action) {
|
||||||
@@ -28,6 +29,8 @@ export class CheckpointContextMenu extends BaseContextMenu {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const apiClient = getModelApiClient();
|
||||||
|
|
||||||
// Otherwise handle checkpoint-specific actions
|
// Otherwise handle checkpoint-specific actions
|
||||||
switch(action) {
|
switch(action) {
|
||||||
case 'details':
|
case 'details':
|
||||||
@@ -36,7 +39,7 @@ export class CheckpointContextMenu extends BaseContextMenu {
|
|||||||
break;
|
break;
|
||||||
case 'replace-preview':
|
case 'replace-preview':
|
||||||
// Add new action for replacing preview images
|
// Add new action for replacing preview images
|
||||||
replaceCheckpointPreview(this.currentCard.dataset.filepath);
|
apiClient.replaceModelPreview(this.currentCard.dataset.filepath);
|
||||||
break;
|
break;
|
||||||
case 'delete':
|
case 'delete':
|
||||||
// Delete checkpoint
|
// Delete checkpoint
|
||||||
@@ -52,14 +55,14 @@ export class CheckpointContextMenu extends BaseContextMenu {
|
|||||||
break;
|
break;
|
||||||
case 'refresh-metadata':
|
case 'refresh-metadata':
|
||||||
// Refresh metadata from CivitAI
|
// Refresh metadata from CivitAI
|
||||||
refreshSingleCheckpointMetadata(this.currentCard.dataset.filepath);
|
apiClient.refreshSingleModelMetadata(this.currentCard.dataset.filepath);
|
||||||
break;
|
break;
|
||||||
case 'move':
|
case 'move':
|
||||||
// Move to folder (placeholder)
|
// Move to folder (placeholder)
|
||||||
showToast('Move to folder feature coming soon', 'info');
|
showToast('Move to folder feature coming soon', 'info');
|
||||||
break;
|
break;
|
||||||
case 'exclude':
|
case 'exclude':
|
||||||
showExcludeModal(this.currentCard.dataset.filepath, 'checkpoint');
|
showExcludeModal(this.currentCard.dataset.filepath);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ import { showModelModal } from './ModelModal.js';
|
|||||||
import { bulkManager } from '../../managers/BulkManager.js';
|
import { bulkManager } from '../../managers/BulkManager.js';
|
||||||
import { modalManager } from '../../managers/ModalManager.js';
|
import { modalManager } from '../../managers/ModalManager.js';
|
||||||
import { NSFW_LEVELS } from '../../utils/constants.js';
|
import { NSFW_LEVELS } from '../../utils/constants.js';
|
||||||
import { replacePreview, saveModelMetadata as saveLoraMetadata } from '../../api/loraApi.js';
|
import { getModelApiClient } from '../../api/baseModelApi.js';
|
||||||
import { replaceCheckpointPreview as apiReplaceCheckpointPreview, saveModelMetadata as saveCheckpointMetadata } from '../../api/checkpointApi.js';
|
|
||||||
import { showDeleteModal } from '../../utils/modalUtils.js';
|
import { showDeleteModal } from '../../utils/modalUtils.js';
|
||||||
|
|
||||||
// Add global event delegation handlers
|
// Add global event delegation handlers
|
||||||
@@ -32,6 +31,8 @@ function handleModelCardEvent_internal(event, modelType) {
|
|||||||
const card = event.target.closest('.lora-card');
|
const card = event.target.closest('.lora-card');
|
||||||
if (!card) return;
|
if (!card) return;
|
||||||
|
|
||||||
|
const apiClient = getModelApiClient();
|
||||||
|
|
||||||
// Handle specific elements within the card
|
// Handle specific elements within the card
|
||||||
if (event.target.closest('.toggle-blur-btn')) {
|
if (event.target.closest('.toggle-blur-btn')) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@@ -73,13 +74,13 @@ function handleModelCardEvent_internal(event, modelType) {
|
|||||||
|
|
||||||
if (event.target.closest('.fa-trash')) {
|
if (event.target.closest('.fa-trash')) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
showDeleteModal(card.dataset.filepath, modelType);
|
showDeleteModal(card.dataset.filepath);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.target.closest('.fa-image')) {
|
if (event.target.closest('.fa-image')) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
handleReplacePreview(card.dataset.filepath, modelType);
|
apiClient.replaceModelPreview(card.dataset.filepath);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,9 +137,7 @@ async function toggleFavorite(card, modelType) {
|
|||||||
const newFavoriteState = !isFavorite;
|
const newFavoriteState = !isFavorite;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use the appropriate save function based on model type
|
await apiClient.saveModelMetadata(card.dataset.filepath, {
|
||||||
const saveFunction = modelType === 'lora' ? saveLoraMetadata : saveCheckpointMetadata;
|
|
||||||
await saveFunction(card.dataset.filepath, {
|
|
||||||
favorite: newFavoriteState
|
favorite: newFavoriteState
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -179,15 +178,7 @@ function handleCopyAction(card, modelType) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleReplacePreview(filePath, modelType) {
|
function handleReplacePreview(filePath, modelType) {
|
||||||
if (modelType === 'lora') {
|
apiClient.replaceModelPreview(filePath);
|
||||||
replacePreview(filePath);
|
|
||||||
} else {
|
|
||||||
if (window.replaceCheckpointPreview) {
|
|
||||||
window.replaceCheckpointPreview(filePath);
|
|
||||||
} else {
|
|
||||||
apiReplaceCheckpointPreview(filePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleExampleImagesAccess(card, modelType) {
|
async function handleExampleImagesAccess(card, modelType) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { BASE_MODELS } from '../../utils/constants.js';
|
|||||||
import { state } from '../../state/index.js';
|
import { state } from '../../state/index.js';
|
||||||
import { saveModelMetadata as saveLoraMetadata, renameLoraFile } from '../../api/loraApi.js';
|
import { saveModelMetadata as saveLoraMetadata, renameLoraFile } from '../../api/loraApi.js';
|
||||||
import { saveModelMetadata as saveCheckpointMetadata, renameCheckpointFile } from '../../api/checkpointApi.js';
|
import { saveModelMetadata as saveCheckpointMetadata, renameCheckpointFile } from '../../api/checkpointApi.js';
|
||||||
|
import { getModelApiClient } from '../../api/baseModelApi.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set up model name editing functionality
|
* Set up model name editing functionality
|
||||||
@@ -114,9 +115,7 @@ export function setupModelNameEditing(filePath) {
|
|||||||
// Get the file path from the dataset
|
// Get the file path from the dataset
|
||||||
const filePath = this.dataset.filePath;
|
const filePath = this.dataset.filePath;
|
||||||
|
|
||||||
const saveFunction = state.currentPageType === 'checkpoints' ? saveCheckpointMetadata : saveLoraMetadata;
|
await getModelApiClient().saveModelMetadata(filePath, { model_name: newModelName });
|
||||||
|
|
||||||
await saveFunction(filePath, { model_name: newModelName });
|
|
||||||
|
|
||||||
showToast('Model name updated successfully', 'success');
|
showToast('Model name updated successfully', 'success');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -295,9 +294,7 @@ async function saveBaseModel(filePath, originalValue) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const saveFunction = state.currentPageType === 'checkpoints' ? saveCheckpointMetadata : saveLoraMetadata;
|
await getModelApiClient().saveModelMetadata(filePath, { base_model: newBaseModel });
|
||||||
|
|
||||||
await saveFunction(filePath, { base_model: newBaseModel });
|
|
||||||
|
|
||||||
showToast('Base model updated successfully', 'success');
|
showToast('Base model updated successfully', 'success');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -417,29 +414,7 @@ export function setupFileNameEditing(filePath) {
|
|||||||
// Get the file path from the dataset
|
// Get the file path from the dataset
|
||||||
const filePath = this.dataset.filePath;
|
const filePath = this.dataset.filePath;
|
||||||
|
|
||||||
let result;
|
await getModelApiClient().renameModelFile(filePath, newFileName);
|
||||||
|
|
||||||
if (state.currentPageType === 'checkpoints') {
|
|
||||||
result = await renameCheckpointFile(filePath, newFileName);
|
|
||||||
} else {
|
|
||||||
// Use LoRA rename function
|
|
||||||
result = await renameLoraFile(filePath, newFileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
showToast('File name updated successfully', 'success');
|
|
||||||
|
|
||||||
// Update virtual scroller if available (mainly for LoRAs)
|
|
||||||
if (state.virtualScroller && typeof state.virtualScroller.updateSingleItem === 'function') {
|
|
||||||
const newFilePath = filePath.replace(originalValue, newFileName);
|
|
||||||
state.virtualScroller.updateSingleItem(filePath, {
|
|
||||||
file_name: newFileName,
|
|
||||||
file_path: newFilePath
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Error(result.error || 'Unknown error');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error renaming file:', error);
|
console.error('Error renaming file:', error);
|
||||||
this.textContent = originalValue; // Restore original file name
|
this.textContent = originalValue; // Restore original file name
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
import { showToast, copyToClipboard } from '../../../utils/uiHelpers.js';
|
import { showToast, copyToClipboard } from '../../../utils/uiHelpers.js';
|
||||||
import { state } from '../../../state/index.js';
|
import { state } from '../../../state/index.js';
|
||||||
import { uploadPreview } from '../../../api/baseModelApi.js';
|
import { getModelApiClient } from '../../../api/baseModelApi.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Try to load local image first, fall back to remote if local fails
|
* Try to load local image first, fall back to remote if local fails
|
||||||
@@ -515,6 +515,7 @@ function initSetPreviewHandlers(container) {
|
|||||||
|
|
||||||
// Get local file path if available
|
// Get local file path if available
|
||||||
const useLocalFile = mediaElement.dataset.localSrc && !mediaElement.dataset.localSrc.includes('undefined');
|
const useLocalFile = mediaElement.dataset.localSrc && !mediaElement.dataset.localSrc.includes('undefined');
|
||||||
|
const apiClient = getModelApiClient();
|
||||||
|
|
||||||
if (useLocalFile) {
|
if (useLocalFile) {
|
||||||
// We have a local file, use it directly
|
// We have a local file, use it directly
|
||||||
@@ -523,7 +524,7 @@ function initSetPreviewHandlers(container) {
|
|||||||
const file = new File([blob], 'preview.jpg', { type: blob.type });
|
const file = new File([blob], 'preview.jpg', { type: blob.type });
|
||||||
|
|
||||||
// Use the existing baseModelApi uploadPreview method with nsfw level
|
// Use the existing baseModelApi uploadPreview method with nsfw level
|
||||||
await uploadPreview(modelFilePath, file, modelType, nsfwLevel);
|
await apiClient.uploadPreview(modelFilePath, file, modelType, nsfwLevel);
|
||||||
} else {
|
} else {
|
||||||
// We need to download the remote file first
|
// We need to download the remote file first
|
||||||
const response = await fetch(mediaElement.src);
|
const response = await fetch(mediaElement.src);
|
||||||
@@ -531,7 +532,7 @@ function initSetPreviewHandlers(container) {
|
|||||||
const file = new File([blob], 'preview.jpg', { type: blob.type });
|
const file = new File([blob], 'preview.jpg', { type: blob.type });
|
||||||
|
|
||||||
// Use the existing baseModelApi uploadPreview method with nsfw level
|
// Use the existing baseModelApi uploadPreview method with nsfw level
|
||||||
await uploadPreview(modelFilePath, file, modelType, nsfwLevel);
|
await apiClient.uploadPreview(modelFilePath, file, modelType, nsfwLevel);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error setting preview:', error);
|
console.error('Error setting preview:', error);
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { showToast } from '../utils/uiHelpers.js';
|
import { showToast, updateFolderTags } from '../utils/uiHelpers.js';
|
||||||
import { state, getCurrentPageState } from '../state/index.js';
|
import { state, getCurrentPageState } from '../state/index.js';
|
||||||
import { modalManager } from './ModalManager.js';
|
import { modalManager } from './ModalManager.js';
|
||||||
import { getStorageItem } from '../utils/storageHelpers.js';
|
import { getStorageItem } from '../utils/storageHelpers.js';
|
||||||
import { updateFolderTags } from '../api/baseModelApi.js';
|
|
||||||
|
|
||||||
class MoveManager {
|
class MoveManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -219,7 +218,7 @@ class MoveManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch('/api/loras/move_model', {
|
const response = await fetch('/api/move_model', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -257,7 +256,7 @@ class MoveManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch('/api/loras/move_models_bulk', {
|
const response = await fetch('/api/move_models_bulk', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
// Create the new hierarchical state structure
|
// Create the new hierarchical state structure
|
||||||
import { getStorageItem, getMapFromStorage } from '../utils/storageHelpers.js';
|
import { getStorageItem, getMapFromStorage } from '../utils/storageHelpers.js';
|
||||||
|
import { MODEL_TYPES } from '../api/apiConfig.js';
|
||||||
|
|
||||||
// Load settings from localStorage or use defaults
|
// Load settings from localStorage or use defaults
|
||||||
const savedSettings = getStorageItem('settings', {
|
const savedSettings = getStorageItem('settings', {
|
||||||
blurMatureContent: true,
|
blurMatureContent: true,
|
||||||
show_only_sfw: false,
|
show_only_sfw: false,
|
||||||
cardInfoDisplay: 'always' // Add default value for card info display
|
cardInfoDisplay: 'always'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load preview versions from localStorage
|
// Load preview versions from localStorage for each model type
|
||||||
const loraPreviewVersions = getMapFromStorage('lora_preview_versions');
|
const loraPreviewVersions = getMapFromStorage('lora_preview_versions');
|
||||||
const checkpointPreviewVersions = getMapFromStorage('checkpoint_preview_versions');
|
const checkpointPreviewVersions = getMapFromStorage('checkpoint_preview_versions');
|
||||||
|
const embeddingPreviewVersions = getMapFromStorage('embedding_preview_versions');
|
||||||
|
|
||||||
export const state = {
|
export const state = {
|
||||||
// Global state
|
// Global state
|
||||||
@@ -22,13 +24,13 @@ export const state = {
|
|||||||
|
|
||||||
// Page-specific states
|
// Page-specific states
|
||||||
pages: {
|
pages: {
|
||||||
loras: {
|
[MODEL_TYPES.LORA]: {
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
hasMore: true,
|
hasMore: true,
|
||||||
sortBy: 'name',
|
sortBy: 'name',
|
||||||
activeFolder: null,
|
activeFolder: null,
|
||||||
activeLetterFilter: null, // New property for letter filtering
|
activeLetterFilter: null,
|
||||||
previewVersions: loraPreviewVersions,
|
previewVersions: loraPreviewVersions,
|
||||||
searchManager: null,
|
searchManager: null,
|
||||||
searchOptions: {
|
searchOptions: {
|
||||||
@@ -67,10 +69,10 @@ export const state = {
|
|||||||
},
|
},
|
||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
showFavoritesOnly: false,
|
showFavoritesOnly: false,
|
||||||
duplicatesMode: false, // Add flag for duplicates mode
|
duplicatesMode: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
checkpoints: {
|
[MODEL_TYPES.CHECKPOINT]: {
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
hasMore: true,
|
hasMore: true,
|
||||||
@@ -89,11 +91,34 @@ export const state = {
|
|||||||
},
|
},
|
||||||
showFavoritesOnly: false,
|
showFavoritesOnly: false,
|
||||||
duplicatesMode: false,
|
duplicatesMode: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
[MODEL_TYPES.EMBEDDING]: {
|
||||||
|
currentPage: 1,
|
||||||
|
isLoading: false,
|
||||||
|
hasMore: true,
|
||||||
|
sortBy: 'name',
|
||||||
|
activeFolder: null,
|
||||||
|
activeLetterFilter: null,
|
||||||
|
previewVersions: embeddingPreviewVersions,
|
||||||
|
searchManager: null,
|
||||||
|
searchOptions: {
|
||||||
|
filename: true,
|
||||||
|
modelname: true,
|
||||||
|
tags: false,
|
||||||
|
recursive: false
|
||||||
|
},
|
||||||
|
filters: {
|
||||||
|
baseModel: [],
|
||||||
|
tags: []
|
||||||
|
},
|
||||||
|
showFavoritesOnly: false,
|
||||||
|
duplicatesMode: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Current active page
|
// Current active page - use MODEL_TYPES constants
|
||||||
currentPageType: 'loras',
|
currentPageType: MODEL_TYPES.LORA,
|
||||||
|
|
||||||
// Backward compatibility - proxy properties
|
// Backward compatibility - proxy properties
|
||||||
get currentPage() { return this.pages[this.currentPageType].currentPage; },
|
get currentPage() { return this.pages[this.currentPageType].currentPage; },
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import { modalManager } from '../managers/ModalManager.js';
|
import { modalManager } from '../managers/ModalManager.js';
|
||||||
import { excludeLora, deleteModel as deleteLora } from '../api/loraApi.js';
|
import { getModelApiClient } from '../api/baseModelApi.js';
|
||||||
import { excludeCheckpoint, deleteCheckpoint } from '../api/checkpointApi.js';
|
|
||||||
|
const apiClient = getModelApiClient();
|
||||||
|
|
||||||
let pendingDeletePath = null;
|
let pendingDeletePath = null;
|
||||||
let pendingModelType = null;
|
|
||||||
let pendingExcludePath = null;
|
let pendingExcludePath = null;
|
||||||
let pendingExcludeModelType = null;
|
|
||||||
|
|
||||||
export function showDeleteModal(filePath, modelType = 'lora') {
|
export function showDeleteModal(filePath) {
|
||||||
pendingDeletePath = filePath;
|
pendingDeletePath = filePath;
|
||||||
pendingModelType = modelType;
|
|
||||||
|
|
||||||
const card = document.querySelector(`.lora-card[data-filepath="${filePath}"]`);
|
const card = document.querySelector(`.lora-card[data-filepath="${filePath}"]`);
|
||||||
const modelName = card ? card.dataset.name : filePath.split('/').pop();
|
const modelName = card ? card.dataset.name : filePath.split('/').pop();
|
||||||
@@ -29,12 +27,7 @@ export async function confirmDelete() {
|
|||||||
if (!pendingDeletePath) return;
|
if (!pendingDeletePath) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use appropriate delete function based on model type
|
await apiClient.deleteModel(pendingDeletePath);
|
||||||
if (pendingModelType === 'checkpoint') {
|
|
||||||
await deleteCheckpoint(pendingDeletePath);
|
|
||||||
} else {
|
|
||||||
await deleteLora(pendingDeletePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
closeDeleteModal();
|
closeDeleteModal();
|
||||||
|
|
||||||
@@ -54,9 +47,8 @@ export function closeDeleteModal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Functions for the exclude modal
|
// Functions for the exclude modal
|
||||||
export function showExcludeModal(filePath, modelType = 'lora') {
|
export function showExcludeModal(filePath) {
|
||||||
pendingExcludePath = filePath;
|
pendingExcludePath = filePath;
|
||||||
pendingExcludeModelType = modelType;
|
|
||||||
|
|
||||||
const card = document.querySelector(`.lora-card[data-filepath="${filePath}"]`);
|
const card = document.querySelector(`.lora-card[data-filepath="${filePath}"]`);
|
||||||
const modelName = card ? card.dataset.name : filePath.split('/').pop();
|
const modelName = card ? card.dataset.name : filePath.split('/').pop();
|
||||||
@@ -82,12 +74,7 @@ export async function confirmExclude() {
|
|||||||
if (!pendingExcludePath) return;
|
if (!pendingExcludePath) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use appropriate exclude function based on model type
|
await apiClient.excludeModel(pendingExcludePath);
|
||||||
if (pendingExcludeModelType === 'checkpoint') {
|
|
||||||
await excludeCheckpoint(pendingExcludePath);
|
|
||||||
} else {
|
|
||||||
await excludeLora(pendingExcludePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
closeExcludeModal();
|
closeExcludeModal();
|
||||||
|
|
||||||
|
|||||||
@@ -616,3 +616,30 @@ export async function openExampleImagesFolder(modelHash) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the folder tags display with new folder list
|
||||||
|
* @param {Array} folders - List of folder names
|
||||||
|
*/
|
||||||
|
export function updateFolderTags(folders) {
|
||||||
|
const folderTagsContainer = document.querySelector('.folder-tags');
|
||||||
|
if (!folderTagsContainer) return;
|
||||||
|
|
||||||
|
// Keep track of currently selected folder
|
||||||
|
const currentFolder = this.pageState.activeFolder;
|
||||||
|
|
||||||
|
// Create HTML for folder tags
|
||||||
|
const tagsHTML = folders.map(folder => {
|
||||||
|
const isActive = folder === currentFolder;
|
||||||
|
return `<div class="tag ${isActive ? 'active' : ''}" data-folder="${folder}">${folder}</div>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
// Update the container
|
||||||
|
folderTagsContainer.innerHTML = tagsHTML;
|
||||||
|
|
||||||
|
// Scroll active folder into view (no need to reattach click handlers)
|
||||||
|
const activeTag = folderTagsContainer.querySelector(`.tag[data-folder="${currentFolder}"]`);
|
||||||
|
if (activeTag) {
|
||||||
|
activeTag.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user