mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 23:25:43 -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'
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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