From 3df96034a1edda66f0582912cd13e0fbcf0446b9 Mon Sep 17 00:00:00 2001
From: Will Miao <13051207myq@gmail.com>
Date: Fri, 11 Apr 2025 14:35:56 +0800
Subject: [PATCH] refactor: Consolidate model handling functions into
baseModelApi for better code reuse and organization
---
static/js/api/baseModelApi.js | 512 +++++++++++++++++++++++++
static/js/api/checkpointApi.js | 335 ++--------------
static/js/api/loraApi.js | 348 ++---------------
static/js/components/CheckpointCard.js | 3 +-
4 files changed, 580 insertions(+), 618 deletions(-)
create mode 100644 static/js/api/baseModelApi.js
diff --git a/static/js/api/baseModelApi.js b/static/js/api/baseModelApi.js
new file mode 100644
index 00000000..20a669a3
--- /dev/null
+++ b/static/js/api/baseModelApi.js
@@ -0,0 +1,512 @@
+// filepath: d:\Workspace\ComfyUI\custom_nodes\ComfyUI-Lora-Manager\static\js\api\baseModelApi.js
+import { state, getCurrentPageState } from '../state/index.js';
+import { showToast } from '../utils/uiHelpers.js';
+import { showDeleteModal, confirmDelete } from '../utils/modalUtils.js';
+
+/**
+ * Shared functionality for handling models (loras and checkpoints)
+ */
+
+// Generic function to load more models with pagination
+export async function loadMoreModels(options = {}) {
+ const {
+ resetPage = false,
+ updateFolders = false,
+ modelType = 'lora', // 'lora' or 'checkpoint'
+ createCardFunction,
+ endpoint = '/api/loras'
+ } = options;
+
+ const pageState = getCurrentPageState();
+
+ if (pageState.isLoading || (!pageState.hasMore && !resetPage)) return;
+
+ pageState.isLoading = true;
+ document.body.classList.add('loading');
+
+ try {
+ // Reset to first page if requested
+ if (resetPage) {
+ pageState.currentPage = 1;
+ // Clear grid if resetting
+ const gridId = modelType === 'checkpoint' ? 'checkpointGrid' : 'loraGrid';
+ const grid = document.getElementById(gridId);
+ if (grid) grid.innerHTML = '';
+ }
+
+ const params = new URLSearchParams({
+ page: pageState.currentPage,
+ page_size: pageState.pageSize || 20,
+ sort_by: pageState.sortBy
+ });
+
+ if (pageState.activeFolder !== null) {
+ params.append('folder', pageState.activeFolder);
+ }
+
+ // 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) {
+ // Checkpoints API expects individual 'tag' parameters, Loras API expects comma-separated 'tags'
+ if (modelType === 'checkpoint') {
+ pageState.filters.tags.forEach(tag => {
+ params.append('tag', tag);
+ });
+ } else {
+ params.append('tags', pageState.filters.tags.join(','));
+ }
+ }
+
+ // Handle base model filters
+ if (pageState.filters.baseModel && pageState.filters.baseModel.length > 0) {
+ if (modelType === 'checkpoint') {
+ pageState.filters.baseModel.forEach(model => {
+ params.append('base_model', model);
+ });
+ } else {
+ params.append('base_models', pageState.filters.baseModel.join(','));
+ }
+ }
+ }
+
+ // Add model-specific parameters
+ if (modelType === 'lora') {
+ // Check for recipe-based filtering parameters from session storage
+ const filterLoraHash = getSessionItem ? getSessionItem('recipe_to_lora_filterLoraHash') : null;
+ const filterLoraHashes = getSessionItem ? getSessionItem('recipe_to_lora_filterLoraHashes') : null;
+
+ // 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) {
+ throw new Error(`Failed to fetch models: ${response.statusText}`);
+ }
+
+ const data = await response.json();
+
+ const gridId = modelType === 'checkpoint' ? 'checkpointGrid' : 'loraGrid';
+ const grid = document.getElementById(gridId);
+
+ if (data.items.length === 0 && pageState.currentPage === 1) {
+ grid.innerHTML = `
No ${modelType}s found in this folder
`;
+ pageState.hasMore = false;
+ } else if (data.items.length > 0) {
+ pageState.hasMore = pageState.currentPage < data.total_pages;
+
+ // Append model cards using the provided card creation function
+ data.items.forEach(model => {
+ const card = createCardFunction(model);
+ grid.appendChild(card);
+ });
+
+ // Increment the page number AFTER successful loading
+ pageState.currentPage++;
+ } else {
+ pageState.hasMore = false;
+ }
+
+ if (updateFolders && data.folders) {
+ updateFolderTags(data.folders);
+ }
+
+ } catch (error) {
+ console.error(`Error loading ${modelType}s:`, error);
+ showToast(`Failed to load ${modelType}s: ${error.message}`, 'error');
+ } finally {
+ pageState.isLoading = false;
+ document.body.classList.remove('loading');
+ }
+}
+
+// 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 `${folder}
`;
+ }).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 function deleteModel(filePath, modelType = 'lora') {
+ if (modelType === 'checkpoint') {
+ confirmDelete('Are you sure you want to delete this checkpoint?', () => {
+ performDelete(filePath, modelType);
+ });
+ } else {
+ showDeleteModal(filePath);
+ }
+}
+
+// Reset and reload models
+export async function resetAndReload(options = {}) {
+ const {
+ updateFolders = false,
+ modelType = 'lora',
+ loadMoreFunction
+ } = options;
+
+ const pageState = getCurrentPageState();
+ console.log('Resetting with state:', { ...pageState });
+
+ // Reset pagination and load more models
+ if (typeof loadMoreFunction === 'function') {
+ await loadMoreFunction(true, updateFolders);
+ }
+}
+
+// Generic function to refresh models
+export async function refreshModels(options = {}) {
+ const {
+ modelType = 'lora',
+ scanEndpoint = '/api/loras/scan',
+ resetAndReloadFunction
+ } = options;
+
+ try {
+ state.loadingManager.showSimpleLoading(`Refreshing ${modelType}s...`);
+
+ const response = await fetch(scanEndpoint);
+
+ if (!response.ok) {
+ throw new Error(`Failed to refresh ${modelType}s: ${response.status} ${response.statusText}`);
+ }
+
+ if (typeof resetAndReloadFunction === 'function') {
+ await resetAndReloadFunction();
+ }
+
+ showToast(`Refresh complete`, 'success');
+ } catch (error) {
+ console.error(`Refresh failed:`, error);
+ showToast(`Failed to refresh ${modelType}s`, 'error');
+ } finally {
+ state.loadingManager.hide();
+ state.loadingManager.restoreProgressBar();
+ }
+}
+
+// Generic fetch from Civitai
+export async function fetchCivitaiMetadata(options = {}) {
+ const {
+ modelType = 'lora',
+ fetchEndpoint = '/api/fetch-all-civitai',
+ resetAndReloadFunction
+ } = options;
+
+ let ws = null;
+
+ await state.loadingManager.showWithProgress(async (loading) => {
+ try {
+ const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
+ ws = new WebSocket(`${wsProtocol}${window.location.host}/ws/fetch-progress`);
+
+ const operationComplete = new Promise((resolve, reject) => {
+ ws.onmessage = (event) => {
+ const data = JSON.parse(event.data);
+
+ switch(data.status) {
+ case 'started':
+ loading.setStatus('Starting metadata fetch...');
+ break;
+
+ case 'processing':
+ const percent = ((data.processed / data.total) * 100).toFixed(1);
+ loading.setProgress(percent);
+ loading.setStatus(
+ `Processing (${data.processed}/${data.total}) ${data.current_name}`
+ );
+ break;
+
+ case 'completed':
+ loading.setProgress(100);
+ loading.setStatus(
+ `Completed: Updated ${data.success} of ${data.processed} ${modelType}s`
+ );
+ resolve();
+ break;
+
+ case 'error':
+ reject(new Error(data.error));
+ break;
+ }
+ };
+
+ ws.onerror = (error) => {
+ reject(new Error('WebSocket error: ' + error.message));
+ };
+ });
+
+ await new Promise((resolve, reject) => {
+ ws.onopen = resolve;
+ ws.onerror = reject;
+ });
+
+ const requestBody = modelType === 'checkpoint'
+ ? JSON.stringify({ model_type: 'checkpoint' })
+ : JSON.stringify({});
+
+ const response = await fetch(fetchEndpoint, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: requestBody
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch metadata');
+ }
+
+ await operationComplete;
+
+ if (typeof resetAndReloadFunction === 'function') {
+ await resetAndReloadFunction();
+ }
+
+ } catch (error) {
+ console.error('Error fetching metadata:', error);
+ showToast('Failed to fetch metadata: ' + error.message, 'error');
+ } finally {
+ if (ws) {
+ ws.close();
+ }
+ }
+ }, {
+ initialMessage: 'Connecting...',
+ completionMessage: 'Metadata update complete'
+ });
+}
+
+// Generic function to refresh single model metadata
+export async function refreshSingleModelMetadata(filePath, modelType = 'lora') {
+ try {
+ state.loadingManager.showSimpleLoading('Refreshing metadata...');
+
+ const endpoint = modelType === 'checkpoint'
+ ? '/api/checkpoints/fetch-civitai'
+ : '/api/fetch-civitai';
+
+ 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 refresh metadata');
+ }
+
+ const data = await response.json();
+
+ if (data.success) {
+ 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();
+ }
+}
+
+// Private methods
+
+// Upload a preview image
+async function uploadPreview(filePath, file, modelType = 'lora') {
+ const loadingOverlay = document.getElementById('loading-overlay');
+ const loadingStatus = document.querySelector('.loading-status');
+
+ try {
+ if (loadingOverlay) loadingOverlay.style.display = 'flex';
+ if (loadingStatus) loadingStatus.textContent = 'Uploading preview...';
+
+ const formData = new FormData();
+
+ // Use appropriate parameter names and endpoint based on model type
+ // Prepare common form data
+ formData.append('preview_file', file);
+ formData.append('model_path', filePath);
+
+ // Set endpoint based on model type
+ const endpoint = modelType === 'checkpoint'
+ ? '/api/checkpoints/replace-preview'
+ : '/api/replace_preview';
+
+ const response = await fetch(endpoint, {
+ method: 'POST',
+ body: formData
+ });
+
+ if (!response.ok) {
+ throw new Error('Upload failed');
+ }
+
+ const data = await response.json();
+
+ // Update the card preview in UI
+ const card = document.querySelector(`.lora-card[data-filepath="${filePath}"]`);
+ if (card) {
+ const previewContainer = card.querySelector('.card-preview');
+ const oldPreview = previewContainer.querySelector('img, video');
+
+ // For LoRA models, use timestamp to prevent caching
+ if (modelType === 'lora') {
+ state.previewVersions?.set(filePath, Date.now());
+ }
+
+ const timestamp = Date.now();
+ const previewUrl = data.preview_url ?
+ `${data.preview_url}?t=${timestamp}` :
+ `/api/model/preview_image?path=${encodeURIComponent(filePath)}&t=${timestamp}`;
+
+ // Create appropriate element based on file type
+ if (file.type.startsWith('video/')) {
+ const video = document.createElement('video');
+ video.controls = true;
+ video.autoplay = true;
+ video.muted = true;
+ video.loop = true;
+ video.src = previewUrl;
+ oldPreview.replaceWith(video);
+ } else {
+ const img = document.createElement('img');
+ img.src = previewUrl;
+ oldPreview.replaceWith(img);
+ }
+
+ showToast('Preview updated successfully', 'success');
+ }
+ } catch (error) {
+ console.error('Error uploading preview:', error);
+ showToast('Failed to upload preview image', 'error');
+ } finally {
+ if (loadingOverlay) loadingOverlay.style.display = 'none';
+ }
+}
+
+// Private function to perform the delete operation
+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();
+
+ if (data.success) {
+ // Remove the card from UI
+ const card = document.querySelector(`.lora-card[data-filepath="${filePath}"]`);
+ if (card) {
+ card.remove();
+ }
+
+ showToast(`${modelType} deleted successfully`, 'success');
+ } else {
+ throw new Error(data.error || `Failed to delete ${modelType}`);
+ }
+ } catch (error) {
+ console.error(`Error deleting ${modelType}:`, error);
+ showToast(`Failed to delete ${modelType}: ${error.message}`, 'error');
+ }
+}
+
+// Helper function to get session item - import if available, otherwise provide fallback
+function getSessionItem(key) {
+ if (typeof window !== 'undefined' && window.sessionStorage) {
+ const item = window.sessionStorage.getItem(key);
+ try {
+ return item ? JSON.parse(item) : null;
+ } catch (e) {
+ return item;
+ }
+ }
+ return null;
+}
\ No newline at end of file
diff --git a/static/js/api/checkpointApi.js b/static/js/api/checkpointApi.js
index e0ed5d4f..8b243be9 100644
--- a/static/js/api/checkpointApi.js
+++ b/static/js/api/checkpointApi.js
@@ -1,330 +1,57 @@
-import { state, getCurrentPageState } from '../state/index.js';
-import { showToast } from '../utils/uiHelpers.js';
-import { confirmDelete } from '../utils/modalUtils.js';
import { createCheckpointCard } from '../components/CheckpointCard.js';
+import {
+ loadMoreModels,
+ resetAndReload as baseResetAndReload,
+ refreshModels as baseRefreshModels,
+ deleteModel as baseDeleteModel,
+ replaceModelPreview,
+ fetchCivitaiMetadata
+} from './baseModelApi.js';
// Load more checkpoints with pagination
export async function loadMoreCheckpoints(resetPagination = true) {
- try {
- const pageState = getCurrentPageState();
-
- // Don't load if we're already loading or there are no more items
- if (pageState.isLoading || (!resetPagination && !pageState.hasMore)) {
- return;
- }
-
- // Set loading state
- pageState.isLoading = true;
- document.body.classList.add('loading');
-
- // Reset pagination if requested
- if (resetPagination) {
- pageState.currentPage = 1;
- const grid = document.getElementById('checkpointGrid');
- if (grid) grid.innerHTML = '';
- }
-
- // Build API URL with parameters
- const params = new URLSearchParams({
- page: pageState.currentPage,
- page_size: pageState.pageSize || 20,
- sort: pageState.sortBy || 'name'
- });
-
- // Add folder filter if active
- if (pageState.activeFolder) {
- params.append('folder', pageState.activeFolder);
- }
-
- // Add search if available
- if (pageState.filters && pageState.filters.search) {
- params.append('search', pageState.filters.search);
-
- // Add search options
- if (pageState.searchOptions) {
- params.append('search_filename', pageState.searchOptions.filename.toString());
- params.append('search_modelname', pageState.searchOptions.modelname.toString());
- params.append('recursive', pageState.searchOptions.recursive.toString());
- }
- }
-
- // Add base model filters
- if (pageState.filters && pageState.filters.baseModel && pageState.filters.baseModel.length > 0) {
- pageState.filters.baseModel.forEach(model => {
- params.append('base_model', model);
- });
- }
-
- // Add tags filters
- if (pageState.filters && pageState.filters.tags && pageState.filters.tags.length > 0) {
- pageState.filters.tags.forEach(tag => {
- params.append('tag', tag);
- });
- }
-
- // Execute fetch
- const response = await fetch(`/api/checkpoints?${params.toString()}`);
-
- if (!response.ok) {
- throw new Error(`Failed to load checkpoints: ${response.status} ${response.statusText}`);
- }
-
- const data = await response.json();
-
- // Update state with response data
- pageState.hasMore = data.page < data.total_pages;
-
- // Update UI with checkpoints
- const grid = document.getElementById('checkpointGrid');
- if (!grid) {
- return;
- }
-
- // Clear grid if this is the first page
- if (resetPagination) {
- grid.innerHTML = '';
- }
-
- // Check for empty result
- if (data.items.length === 0 && resetPagination) {
- grid.innerHTML = `
-
-
No checkpoints found
-
Add checkpoints to your models folders to see them here.
-
- `;
- return;
- }
-
- // Render checkpoint cards
- data.items.forEach(checkpoint => {
- const card = createCheckpointCard(checkpoint);
- grid.appendChild(card);
- });
-
- // Increment the page number AFTER successful loading
- if (data.items.length > 0) {
- pageState.currentPage++;
- }
- } catch (error) {
- console.error('Error loading checkpoints:', error);
- showToast('Failed to load checkpoints', 'error');
- } finally {
- // Clear loading state
- const pageState = getCurrentPageState();
- pageState.isLoading = false;
- document.body.classList.remove('loading');
- }
+ return loadMoreModels({
+ resetPage: resetPagination,
+ updateFolders: true,
+ modelType: 'checkpoint',
+ createCardFunction: createCheckpointCard,
+ endpoint: '/api/checkpoints'
+ });
}
// Reset and reload checkpoints
export async function resetAndReload() {
- const pageState = getCurrentPageState();
- pageState.currentPage = 1;
- pageState.hasMore = true;
- await loadMoreCheckpoints(true);
+ return baseResetAndReload({
+ updateFolders: true,
+ modelType: 'checkpoint',
+ loadMoreFunction: loadMoreCheckpoints
+ });
}
// Refresh checkpoints
export async function refreshCheckpoints() {
- try {
- showToast('Scanning for checkpoints...', 'info');
- const response = await fetch('/api/checkpoints/scan');
-
- if (!response.ok) {
- throw new Error(`Failed to scan checkpoints: ${response.status} ${response.statusText}`);
- }
-
- await resetAndReload();
- showToast('Checkpoints refreshed successfully', 'success');
- } catch (error) {
- console.error('Error refreshing checkpoints:', error);
- showToast('Failed to refresh checkpoints', 'error');
- }
+ return baseRefreshModels({
+ modelType: 'checkpoint',
+ scanEndpoint: '/api/checkpoints/scan',
+ resetAndReloadFunction: resetAndReload
+ });
}
// Delete a checkpoint
export function deleteCheckpoint(filePath) {
- confirmDelete('Are you sure you want to delete this checkpoint?', () => {
- _performDelete(filePath);
- });
-}
-
-// Private function to perform the delete operation
-async function _performDelete(filePath) {
- try {
- showToast('Deleting checkpoint...', 'info');
-
- const response = await fetch('/api/model/delete', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- file_path: filePath,
- model_type: 'checkpoint'
- })
- });
-
- if (!response.ok) {
- throw new Error(`Failed to delete checkpoint: ${response.status} ${response.statusText}`);
- }
-
- const data = await response.json();
-
- if (data.success) {
- // Remove the card from UI
- const card = document.querySelector(`.lora-card[data-filepath="${filePath}"]`);
- if (card) {
- card.remove();
- }
-
- showToast('Checkpoint deleted successfully', 'success');
- } else {
- throw new Error(data.error || 'Failed to delete checkpoint');
- }
- } catch (error) {
- console.error('Error deleting checkpoint:', error);
- showToast(`Failed to delete checkpoint: ${error.message}`, 'error');
- }
+ return baseDeleteModel(filePath, 'checkpoint');
}
// Replace checkpoint preview
export function replaceCheckpointPreview(filePath) {
- // Open file picker
- const input = document.createElement('input');
- input.type = 'file';
- input.accept = 'image/*';
- input.onchange = async (e) => {
- if (!e.target.files.length) return;
-
- const file = e.target.files[0];
- await _uploadPreview(filePath, file);
- };
- input.click();
-}
-
-// Upload a preview image
-async function _uploadPreview(filePath, file) {
- try {
- showToast('Uploading preview...', 'info');
-
- const formData = new FormData();
- formData.append('file', file);
- formData.append('file_path', filePath);
- formData.append('model_type', 'checkpoint');
-
- const response = await fetch('/api/model/preview', {
- method: 'POST',
- body: formData
- });
-
- if (!response.ok) {
- throw new Error(`Failed to upload preview: ${response.status} ${response.statusText}`);
- }
-
- const data = await response.json();
-
- if (data.success) {
- // Update the preview in UI
- const card = document.querySelector(`.lora-card[data-filepath="${filePath}"]`);
- if (card) {
- const img = card.querySelector('.card-preview img');
- if (img) {
- // Add timestamp to prevent caching
- const timestamp = new Date().getTime();
- if (data.preview_url) {
- img.src = `${data.preview_url}?t=${timestamp}`;
- } else {
- img.src = `/api/model/preview_image?path=${encodeURIComponent(filePath)}&t=${timestamp}`;
- }
- }
- }
-
- showToast('Preview updated successfully', 'success');
- } else {
- throw new Error(data.error || 'Failed to update preview');
- }
- } catch (error) {
- console.error('Error updating preview:', error);
- showToast(`Failed to update preview: ${error.message}`, 'error');
- }
+ return replaceModelPreview(filePath, 'checkpoint');
}
// Fetch metadata from Civitai for checkpoints
export async function fetchCivitai() {
- let ws = null;
-
- await state.loadingManager.showWithProgress(async (loading) => {
- try {
- const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
- const ws = new WebSocket(`${wsProtocol}${window.location.host}/ws/fetch-progress`);
-
- const operationComplete = new Promise((resolve, reject) => {
- ws.onmessage = (event) => {
- const data = JSON.parse(event.data);
-
- switch(data.status) {
- case 'started':
- loading.setStatus('Starting metadata fetch...');
- break;
-
- case 'processing':
- const percent = ((data.processed / data.total) * 100).toFixed(1);
- loading.setProgress(percent);
- loading.setStatus(
- `Processing (${data.processed}/${data.total}) ${data.current_name}`
- );
- break;
-
- case 'completed':
- loading.setProgress(100);
- loading.setStatus(
- `Completed: Updated ${data.success} of ${data.processed} checkpoints`
- );
- resolve();
- break;
-
- case 'error':
- reject(new Error(data.error));
- break;
- }
- };
-
- ws.onerror = (error) => {
- reject(new Error('WebSocket error: ' + error.message));
- };
- });
-
- await new Promise((resolve, reject) => {
- ws.onopen = resolve;
- ws.onerror = reject;
- });
-
- const response = await fetch('/api/checkpoints/fetch-all-civitai', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ model_type: 'checkpoint' }) // Specify we're fetching checkpoint metadata
- });
-
- if (!response.ok) {
- throw new Error('Failed to fetch metadata');
- }
-
- await operationComplete;
-
- await resetAndReload();
-
- } catch (error) {
- console.error('Error fetching metadata:', error);
- showToast('Failed to fetch metadata: ' + error.message, 'error');
- } finally {
- if (ws) {
- ws.close();
- }
- }
- }, {
- initialMessage: 'Connecting...',
- completionMessage: 'Metadata update complete'
+ return fetchCivitaiMetadata({
+ modelType: 'checkpoint',
+ fetchEndpoint: '/api/checkpoints/fetch-all-civitai',
+ resetAndReloadFunction: resetAndReload
});
}
\ No newline at end of file
diff --git a/static/js/api/loraApi.js b/static/js/api/loraApi.js
index c344e930..5e433799 100644
--- a/static/js/api/loraApi.js
+++ b/static/js/api/loraApi.js
@@ -1,285 +1,38 @@
-import { state, getCurrentPageState } from '../state/index.js';
-import { showToast } from '../utils/uiHelpers.js';
import { createLoraCard } from '../components/LoraCard.js';
-import { initializeInfiniteScroll } from '../utils/infiniteScroll.js';
-import { showDeleteModal } from '../utils/modalUtils.js';
-import { toggleFolder } from '../utils/uiHelpers.js';
-import { getSessionItem } from '../utils/storageHelpers.js';
+import {
+ loadMoreModels,
+ resetAndReload as baseResetAndReload,
+ refreshModels as baseRefreshModels,
+ deleteModel as baseDeleteModel,
+ replaceModelPreview,
+ fetchCivitaiMetadata,
+ refreshSingleModelMetadata
+} from './baseModelApi.js';
export async function loadMoreLoras(resetPage = false, updateFolders = false) {
- const pageState = getCurrentPageState();
-
- if (pageState.isLoading || (!pageState.hasMore && !resetPage)) return;
-
- pageState.isLoading = true;
- try {
- // Reset to first page if requested
- if (resetPage) {
- pageState.currentPage = 1;
- // Clear grid if resetting
- const grid = document.getElementById('loraGrid');
- if (grid) grid.innerHTML = '';
- }
-
- const params = new URLSearchParams({
- page: pageState.currentPage,
- page_size: 20,
- sort_by: pageState.sortBy
- });
-
- if (pageState.activeFolder !== null) {
- params.append('folder', pageState.activeFolder);
- }
-
- // 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());
- params.append('search_tags', (pageState.searchOptions.tags || false).toString());
- params.append('recursive', (pageState.searchOptions?.recursive ?? false).toString());
- }
- }
-
- // Add filter parameters if active
- if (pageState.filters) {
- if (pageState.filters.tags && pageState.filters.tags.length > 0) {
- // Convert the array of tags to a comma-separated string
- params.append('tags', pageState.filters.tags.join(','));
- }
- if (pageState.filters.baseModel && pageState.filters.baseModel.length > 0) {
- // Convert the array of base models to a comma-separated string
- params.append('base_models', pageState.filters.baseModel.join(','));
- }
- }
-
- // 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(`/api/loras?${params}`);
- if (!response.ok) {
- throw new Error(`Failed to fetch loras: ${response.statusText}`);
- }
-
- const data = await response.json();
-
- if (data.items.length === 0 && pageState.currentPage === 1) {
- const grid = document.getElementById('loraGrid');
- grid.innerHTML = 'No loras found in this folder
';
- pageState.hasMore = false;
- } else if (data.items.length > 0) {
- pageState.hasMore = pageState.currentPage < data.total_pages;
- appendLoraCards(data.items);
-
- // Increment the page number AFTER successful loading
- pageState.currentPage++;
- } else {
- pageState.hasMore = false;
- }
-
- if (updateFolders && data.folders) {
- updateFolderTags(data.folders);
- }
-
- } catch (error) {
- console.error('Error loading loras:', error);
- showToast('Failed to load loras: ' + error.message, 'error');
- } finally {
- pageState.isLoading = false;
- }
-}
-
-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 `${folder}
`;
- }).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 => {
- tag.addEventListener('click', toggleFolder);
- if (tag.dataset.folder === currentFolder) {
- tag.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
- }
+ return loadMoreModels({
+ resetPage,
+ updateFolders,
+ modelType: 'lora',
+ createCardFunction: createLoraCard,
+ endpoint: '/api/loras'
});
}
export async function fetchCivitai() {
- let ws = null;
-
- await state.loadingManager.showWithProgress(async (loading) => {
- try {
- const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
- const ws = new WebSocket(`${wsProtocol}${window.location.host}/ws/fetch-progress`);
-
- const operationComplete = new Promise((resolve, reject) => {
- ws.onmessage = (event) => {
- const data = JSON.parse(event.data);
-
- switch(data.status) {
- case 'started':
- loading.setStatus('Starting metadata fetch...');
- break;
-
- case 'processing':
- const percent = ((data.processed / data.total) * 100).toFixed(1);
- loading.setProgress(percent);
- loading.setStatus(
- `Processing (${data.processed}/${data.total}) ${data.current_name}`
- );
- break;
-
- case 'completed':
- loading.setProgress(100);
- loading.setStatus(
- `Completed: Updated ${data.success} of ${data.processed} loras`
- );
- resolve();
- break;
-
- case 'error':
- reject(new Error(data.error));
- break;
- }
- };
-
- ws.onerror = (error) => {
- reject(new Error('WebSocket error: ' + error.message));
- };
- });
-
- await new Promise((resolve, reject) => {
- ws.onopen = resolve;
- ws.onerror = reject;
- });
-
- const response = await fetch('/api/fetch-all-civitai', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' }
- });
-
- if (!response.ok) {
- throw new Error('Failed to fetch metadata');
- }
-
- await operationComplete;
-
- await resetAndReload();
-
- } catch (error) {
- console.error('Error fetching metadata:', error);
- showToast('Failed to fetch metadata: ' + error.message, 'error');
- } finally {
- if (ws) {
- ws.close();
- }
- }
- }, {
- initialMessage: 'Connecting...',
- completionMessage: 'Metadata update complete'
+ return fetchCivitaiMetadata({
+ modelType: 'lora',
+ fetchEndpoint: '/api/fetch-all-civitai',
+ resetAndReloadFunction: resetAndReload
});
}
export async function deleteModel(filePath) {
- showDeleteModal(filePath);
+ return baseDeleteModel(filePath, 'lora');
}
export async function replacePreview(filePath) {
- const loadingOverlay = document.getElementById('loading-overlay');
- const loadingStatus = document.querySelector('.loading-status');
-
- 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];
- const formData = new FormData();
- formData.append('preview_file', file);
- formData.append('model_path', filePath);
-
- try {
- loadingOverlay.style.display = 'flex';
- loadingStatus.textContent = 'Uploading preview...';
-
- const response = await fetch('/api/replace_preview', {
- method: 'POST',
- body: formData
- });
-
- if (!response.ok) {
- throw new Error('Upload failed');
- }
-
- const data = await response.json();
-
- // 更新预览版本
- state.previewVersions.set(filePath, Date.now());
-
- // 更新卡片显示
- const card = document.querySelector(`.lora-card[data-filepath="${filePath}"]`);
- const previewContainer = card.querySelector('.card-preview');
- const oldPreview = previewContainer.querySelector('img, video');
-
- const previewUrl = `${data.preview_url}?t=${state.previewVersions.get(filePath)}`;
-
- if (file.type.startsWith('video/')) {
- const video = document.createElement('video');
- video.controls = true;
- video.autoplay = true;
- video.muted = true;
- video.loop = true;
- video.src = previewUrl;
- oldPreview.replaceWith(video);
- } else {
- const img = document.createElement('img');
- img.src = previewUrl;
- oldPreview.replaceWith(img);
- }
-
- } catch (error) {
- console.error('Error uploading preview:', error);
- alert('Failed to upload preview image');
- } finally {
- loadingOverlay.style.display = 'none';
- }
- };
-
- input.click();
+ return replaceModelPreview(filePath, 'lora');
}
export function appendLoraCards(loras) {
@@ -293,57 +46,26 @@ export function appendLoraCards(loras) {
}
export async function resetAndReload(updateFolders = false) {
- const pageState = getCurrentPageState();
- console.log('Resetting with state:', { ...pageState });
-
- // Reset pagination and load more loras
- await loadMoreLoras(true, updateFolders);
+ return baseResetAndReload({
+ updateFolders,
+ modelType: 'lora',
+ loadMoreFunction: loadMoreLoras
+ });
}
export async function refreshLoras() {
- try {
- state.loadingManager.showSimpleLoading('Refreshing loras...');
- await resetAndReload();
- showToast('Refresh complete', 'success');
- } catch (error) {
- console.error('Refresh failed:', error);
- showToast('Failed to refresh loras', 'error');
- } finally {
- state.loadingManager.hide();
- state.loadingManager.restoreProgressBar();
- }
+ return baseRefreshModels({
+ modelType: 'lora',
+ scanEndpoint: '/api/loras/scan',
+ resetAndReloadFunction: resetAndReload
+ });
}
export async function refreshSingleLoraMetadata(filePath) {
- try {
- state.loadingManager.showSimpleLoading('Refreshing metadata...');
- const response = await fetch('/api/fetch-civitai', {
- 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) {
- showToast('Metadata refreshed successfully', 'success');
- // Reload the current view to show updated data
- await resetAndReload();
- } else {
- throw new Error(data.error || 'Failed to refresh metadata');
- }
- } catch (error) {
- console.error('Error refreshing metadata:', error);
- showToast(error.message, 'error');
- } finally {
- state.loadingManager.hide();
- state.loadingManager.restoreProgressBar();
+ const success = await refreshSingleModelMetadata(filePath, 'lora');
+ if (success) {
+ // Reload the current view to show updated data
+ await resetAndReload();
}
}
diff --git a/static/js/components/CheckpointCard.js b/static/js/components/CheckpointCard.js
index abb95afe..c000ecfc 100644
--- a/static/js/components/CheckpointCard.js
+++ b/static/js/components/CheckpointCard.js
@@ -2,6 +2,7 @@ import { showToast } from '../utils/uiHelpers.js';
import { state } from '../state/index.js';
import { showCheckpointModal } from './checkpointModal/index.js';
import { NSFW_LEVELS } from '../utils/constants.js';
+import { replaceCheckpointPreview as apiReplaceCheckpointPreview } from '../api/checkpointApi.js';
export function createCheckpointCard(checkpoint) {
const card = document.createElement('div');
@@ -305,6 +306,6 @@ function replaceCheckpointPreview(filePath) {
if (window.replaceCheckpointPreview) {
window.replaceCheckpointPreview(filePath);
} else {
- console.log('Replace checkpoint preview:', filePath);
+ apiReplaceCheckpointPreview(filePath);
}
}
\ No newline at end of file