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