diff --git a/py/utils/routes_common.py b/py/utils/routes_common.py index 4e5c9807..8f00106b 100644 --- a/py/utils/routes_common.py +++ b/py/utils/routes_common.py @@ -329,8 +329,6 @@ class ModelRouteUtils: # Update hash index if available if hasattr(scanner, '_hash_index') and scanner._hash_index: scanner._hash_index.remove_by_path(file_path) - - await scanner._save_cache_to_disk() return web.json_response({ 'success': True, diff --git a/static/js/api/baseModelApi.js b/static/js/api/baseModelApi.js index 116521ed..a3d93da4 100644 --- a/static/js/api/baseModelApi.js +++ b/static/js/api/baseModelApi.js @@ -1,5 +1,5 @@ import { state, getCurrentPageState } from '../state/index.js'; -import { showToast } from '../utils/uiHelpers.js'; +import { showToast, updateFolderTags } from '../utils/uiHelpers.js'; import { getSessionItem, saveMapToStorage } from '../utils/storageHelpers.js'; import { getCompleteApiConfig, @@ -79,6 +79,53 @@ class ModelApiClient { } } + /** + * Reset and reload models with virtual scrolling + */ + async loadMoreWithVirtualScroll(resetPage = false, updateFolders = false) { + const pageState = this.getPageState(); + + try { + state.loadingManager.showSimpleLoading(`Loading more ${this.apiConfig.config.displayName}s...`); + + pageState.isLoading = true; + if (resetPage) { + pageState.currentPage = 1; // Reset to first page + } + + // Fetch the current page + const startTime = performance.now(); + const result = await this.fetchModelsPage(pageState.currentPage, pageState.pageSize); + const endTime = performance.now(); + console.log(`fetchModelsPage耗时: ${(endTime - startTime).toFixed(2)} ms`); + + // Update the virtual scroller + state.virtualScroller.refreshWithData( + result.items, + result.totalItems, + result.hasMore + ); + + // Update state + pageState.hasMore = result.hasMore; + pageState.currentPage = pageState.currentPage + 1; + + // Update folders if needed + if (updateFolders && result.folders) { + updateFolderTags(result.folders); + } + + return result; + } catch (error) { + console.error(`Error reloading ${this.apiConfig.config.displayName}s:`, error); + showToast(`Failed to reload ${this.apiConfig.config.displayName}s: ${error.message}`, 'error'); + throw error; + } finally { + pageState.isLoading = false; + state.loadingManager.hide(); + } + } + /** * Delete a model */ @@ -355,7 +402,7 @@ class ModelApiClient { /** * Fetch CivitAI metadata for all models */ - async fetchCivitaiMetadata(resetAndReloadFunction) { + async fetchCivitaiMetadata() { let ws = null; await state.loadingManager.showWithProgress(async (loading) => { @@ -416,10 +463,6 @@ class ModelApiClient { await operationComplete; - if (typeof resetAndReloadFunction === 'function') { - await resetAndReloadFunction(); - } - } catch (error) { console.error('Error fetching metadata:', error); showToast('Failed to fetch metadata: ' + error.message, 'error'); @@ -434,6 +477,110 @@ class ModelApiClient { }); } + /** + * Move a single model to target path + * @returns {string|null} - The new file path if moved, null if not moved + */ + async moveSingleModel(filePath, targetPath) { + if (filePath.substring(0, filePath.lastIndexOf('/')) === targetPath) { + showToast('Model is already in the selected folder', 'info'); + return null; + } + + const response = await fetch(this.apiConfig.endpoints.specific.moveModel, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + file_path: filePath, + target_path: targetPath + }) + }); + + const result = await response.json(); + + if (!response.ok) { + if (result && result.error) { + throw new Error(result.error); + } + throw new Error('Failed to move model'); + } + + if (result && result.message) { + showToast(result.message, 'info'); + } else { + showToast('Model moved successfully', 'success'); + } + + // Return new file path if move succeeded + if (result.success) { + return targetPath; + } + return null; + } + + /** + * Move multiple models to target path + * @returns {Array} - Array of new file paths that were moved successfully + */ + async moveBulkModels(filePaths, targetPath) { + const movedPaths = filePaths.filter(path => { + return path.substring(0, path.lastIndexOf('/')) !== targetPath; + }); + + if (movedPaths.length === 0) { + showToast('All selected models are already in the target folder', 'info'); + return []; + } + + const response = await fetch(this.apiConfig.endpoints.specific.moveBulk, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + file_paths: movedPaths, + target_path: targetPath + }) + }); + + const result = await response.json(); + + if (!response.ok) { + throw new Error('Failed to move models'); + } + + let successFilePaths = []; + if (result.success) { + if (result.failure_count > 0) { + showToast(`Moved ${result.success_count} models, ${result.failure_count} failed`, 'warning'); + console.log('Move operation results:', result.results); + const failedFiles = result.results + .filter(r => !r.success) + .map(r => { + const fileName = r.path.substring(r.path.lastIndexOf('/') + 1); + return `${fileName}: ${r.message}`; + }); + if (failedFiles.length > 0) { + const failureMessage = failedFiles.length <= 3 + ? failedFiles.join('\n') + : failedFiles.slice(0, 3).join('\n') + `\n(and ${failedFiles.length - 3} more)`; + showToast(`Failed moves:\n${failureMessage}`, 'warning', 6000); + } + } else { + showToast(`Successfully moved ${result.success_count} models`, 'success'); + } + // Collect new file paths for successful moves + successFilePaths = result.results + .filter(r => r.success && r.new_file_path) + .map(r => r.new_file_path); + } else { + throw new Error(result.message || 'Failed to move models'); + } + return successFilePaths; + } + /** * Build query parameters for API requests */ @@ -527,48 +674,4 @@ export function getModelApiClient() { } _singletonClient.setModelType(state.currentPageType); return _singletonClient; -} - -// Legacy compatibility exports -export async function fetchModelsPage(options = {}) { - const { modelType = getCurrentModelType(), ...rest } = options; - const client = createModelApiClient(modelType); - return client.fetchModelsPage(rest.page, rest.pageSize); -} - -export async function deleteModel(filePath, modelType = null) { - const client = createModelApiClient(modelType); - return client.deleteModel(filePath); -} - -export async function excludeModel(filePath, modelType = null) { - const client = createModelApiClient(modelType); - return client.excludeModel(filePath); -} - -export async function renameModelFile(filePath, newFileName, modelType = null) { - const client = createModelApiClient(modelType); - return client.renameModelFile(filePath, newFileName); -} - -export async function replaceModelPreview(filePath, modelType = null) { - const client = createModelApiClient(modelType); - return client.replaceModelPreview(filePath); -} - -export async function refreshModels(options = {}) { - const { modelType = getCurrentModelType(), fullRebuild = false } = options; - const client = createModelApiClient(modelType); - return client.refreshModels(fullRebuild); -} - -export async function refreshSingleModelMetadata(filePath, modelType = null) { - const client = createModelApiClient(modelType); - return client.refreshSingleModelMetadata(filePath); -} - -export async function fetchCivitaiMetadata(options = {}) { - const { modelType = getCurrentModelType(), resetAndReloadFunction } = options; - const client = createModelApiClient(modelType); - return client.fetchCivitaiMetadata(resetAndReloadFunction); } \ No newline at end of file diff --git a/static/js/api/checkpointApi.js b/static/js/api/checkpointApi.js index 574572a1..048de2ae 100644 --- a/static/js/api/checkpointApi.js +++ b/static/js/api/checkpointApi.js @@ -12,7 +12,7 @@ export const replacePreview = (filePath) => checkpointApiClient.replaceModelPrev 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); +export const fetchCivitai = () => checkpointApiClient.fetchCivitaiMetadata(); // Pagination functions export const fetchCheckpointsPage = (page = 1, pageSize = 50) => checkpointApiClient.fetchModelsPage(page, pageSize); @@ -23,7 +23,7 @@ export async function loadMoreCheckpoints(resetPage = false, updateFolders = fal } export async function resetAndReload(updateFolders = false) { - return checkpointApiClient.resetAndReloadWithVirtualScroll(updateFolders); + return checkpointApiClient.loadMoreWithVirtualScroll(true, updateFolders); } // Checkpoint-specific functions diff --git a/static/js/api/loraApi.js b/static/js/api/loraApi.js index 738571fb..49a06bbc 100644 --- a/static/js/api/loraApi.js +++ b/static/js/api/loraApi.js @@ -12,7 +12,7 @@ export const replacePreview = (filePath) => loraApiClient.replaceModelPreview(fi export const saveModelMetadata = (filePath, data) => loraApiClient.saveModelMetadata(filePath, data); export const refreshLoras = (fullRebuild = false) => loraApiClient.refreshModels(fullRebuild); export const refreshSingleLoraMetadata = (filePath) => loraApiClient.refreshSingleModelMetadata(filePath); -export const fetchCivitai = (resetAndReloadFunction) => loraApiClient.fetchCivitaiMetadata(resetAndReloadFunction); +export const fetchCivitai = () => loraApiClient.fetchCivitaiMetadata(); // Pagination functions export const fetchLorasPage = (page = 1, pageSize = 100) => loraApiClient.fetchModelsPage(page, pageSize); @@ -23,7 +23,7 @@ export async function loadMoreLoras(resetPage = false, updateFolders = false) { } export async function resetAndReload(updateFolders = false) { - return loraApiClient.resetAndReloadWithVirtualScroll(updateFolders); + return loraApiClient.loadMoreWithVirtualScroll(true, updateFolders); } // LoRA-specific functions that don't have common equivalents diff --git a/static/js/managers/DownloadManager.js b/static/js/managers/DownloadManager.js index b3d0433e..f4f5b9ba 100644 --- a/static/js/managers/DownloadManager.js +++ b/static/js/managers/DownloadManager.js @@ -155,8 +155,6 @@ export class DownloadManager { `; } - - console.log(earlyAccessBadge); // Status badge for local models const localStatus = existsLocally ? diff --git a/static/js/managers/MoveManager.js b/static/js/managers/MoveManager.js index 3798edab..5f14ea1e 100644 --- a/static/js/managers/MoveManager.js +++ b/static/js/managers/MoveManager.js @@ -2,6 +2,7 @@ import { showToast, updateFolderTags } from '../utils/uiHelpers.js'; import { state, getCurrentPageState } from '../state/index.js'; import { modalManager } from './ModalManager.js'; import { getStorageItem } from '../utils/storageHelpers.js'; +import { getModelApiClient } from '../api/baseModelApi.js'; class MoveManager { constructor() { @@ -145,45 +146,46 @@ class MoveManager { targetPath = `${targetPath}/${newFolder}`; } + const apiClient = getModelApiClient(); + try { if (this.bulkFilePaths) { // Bulk move mode - await this.moveBulkModels(this.bulkFilePaths, targetPath); + const movedFilePaths = await apiClient.moveBulkModels(this.bulkFilePaths, targetPath); // Update virtual scroller if in active folder view const pageState = getCurrentPageState(); if (pageState.activeFolder !== null && state.virtualScroller) { - // Remove moved items from virtual scroller instead of reloading - this.bulkFilePaths.forEach(filePath => { - state.virtualScroller.removeItemByFilePath(filePath); + // Remove only successfully moved items + movedFilePaths.forEach(newFilePath => { + // Find original filePath by matching filename + const filename = newFilePath.substring(newFilePath.lastIndexOf('/') + 1); + const originalFilePath = this.bulkFilePaths.find(fp => fp.endsWith('/' + filename)); + if (originalFilePath) { + state.virtualScroller.removeItemByFilePath(originalFilePath); + } }); } else { // Update the model cards' filepath in the DOM - this.bulkFilePaths.forEach(filePath => { - // Extract filename from original path - const filename = filePath.substring(filePath.lastIndexOf('/') + 1); - // Construct new filepath - const newFilePath = `${targetPath}/${filename}`; - - state.virtualScroller.updateSingleItem(filePath, {file_path: newFilePath}); + movedFilePaths.forEach(newFilePath => { + const filename = newFilePath.substring(newFilePath.lastIndexOf('/') + 1); + const originalFilePath = this.bulkFilePaths.find(fp => fp.endsWith('/' + filename)); + if (originalFilePath) { + state.virtualScroller.updateSingleItem(originalFilePath, {file_path: newFilePath}); + } }); } } else { // Single move mode - await this.moveSingleModel(this.currentFilePath, targetPath); - - // Update virtual scroller if in active folder view - const pageState = getCurrentPageState(); - if (pageState.activeFolder !== null && state.virtualScroller) { - // Remove moved item from virtual scroller instead of reloading - state.virtualScroller.removeItemByFilePath(this.currentFilePath); - } else { - // Extract filename from original path - const filename = this.currentFilePath.substring(this.currentFilePath.lastIndexOf('/') + 1); - // Construct new filepath - const newFilePath = `${targetPath}/${filename}`; + const newFilePath = await apiClient.moveSingleModel(this.currentFilePath, targetPath); - state.virtualScroller.updateSingleItem(this.currentFilePath, {file_path: newFilePath}); + const pageState = getCurrentPageState(); + if (newFilePath) { + if (pageState.activeFolder !== null && state.virtualScroller) { + state.virtualScroller.removeItemByFilePath(this.currentFilePath); + } else { + state.virtualScroller.updateSingleItem(this.currentFilePath, {file_path: newFilePath}); + } } } @@ -210,102 +212,6 @@ class MoveManager { showToast('Failed to move model(s): ' + error.message, 'error'); } } - - async moveSingleModel(filePath, targetPath) { - // show toast if current path is same as target path - if (filePath.substring(0, filePath.lastIndexOf('/')) === targetPath) { - showToast('Model is already in the selected folder', 'info'); - return; - } - - const response = await fetch('/api/move_model', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - file_path: filePath, - target_path: targetPath - }) - }); - - const result = await response.json(); - - if (!response.ok) { - if (result && result.error) { - throw new Error(result.error); - } - throw new Error('Failed to move model'); - } - - if (result && result.message) { - showToast(result.message, 'info'); - } else { - showToast('Model moved successfully', 'success'); - } - } - - async moveBulkModels(filePaths, targetPath) { - // Filter out models already in the target path - const movedPaths = filePaths.filter(path => { - return path.substring(0, path.lastIndexOf('/')) !== targetPath; - }); - - if (movedPaths.length === 0) { - showToast('All selected models are already in the target folder', 'info'); - return; - } - - const response = await fetch('/api/move_models_bulk', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - file_paths: movedPaths, - target_path: targetPath - }) - }); - - const result = await response.json(); - - if (!response.ok) { - throw new Error('Failed to move models'); - } - - // Display results with more details - if (result.success) { - if (result.failure_count > 0) { - // Some files failed to move - showToast(`Moved ${result.success_count} models, ${result.failure_count} failed`, 'warning'); - - // Log details about failures - console.log('Move operation results:', result.results); - - // Get list of failed files with reasons - const failedFiles = result.results - .filter(r => !r.success) - .map(r => { - const fileName = r.path.substring(r.path.lastIndexOf('/') + 1); - return `${fileName}: ${r.message}`; - }); - - // Show first few failures in a toast - if (failedFiles.length > 0) { - const failureMessage = failedFiles.length <= 3 - ? failedFiles.join('\n') - : failedFiles.slice(0, 3).join('\n') + `\n(and ${failedFiles.length - 3} more)`; - - showToast(`Failed moves:\n${failureMessage}`, 'warning', 6000); - } - } else { - // All files moved successfully - showToast(`Successfully moved ${result.success_count} models`, 'success'); - } - } else { - throw new Error(result.message || 'Failed to move models'); - } - } } export const moveManager = new MoveManager(); diff --git a/static/js/utils/VirtualScroller.js b/static/js/utils/VirtualScroller.js index bbdc73b6..0d614211 100644 --- a/static/js/utils/VirtualScroller.js +++ b/static/js/utils/VirtualScroller.js @@ -164,23 +164,6 @@ export class VirtualScroller { // Calculate the left offset to center the grid within the content area this.leftOffset = Math.max(0, (availableContentWidth - actualGridWidth) / 2); - - // Log layout info - console.log('Virtual Scroll Layout:', { - containerWidth, - availableContentWidth, - actualGridWidth, - columnsCount: this.columnsCount, - itemWidth: this.itemWidth, - itemHeight: this.itemHeight, - leftOffset: this.leftOffset, - paddingLeft, - paddingRight, - displayDensity, - maxColumns, - baseCardWidth, - rowGap: this.rowGap - }); // Update grid element max-width to match available width this.gridElement.style.maxWidth = `${actualGridWidth}px`; diff --git a/static/js/utils/modalUtils.js b/static/js/utils/modalUtils.js index 512b0c31..19a0edf5 100644 --- a/static/js/utils/modalUtils.js +++ b/static/js/utils/modalUtils.js @@ -43,7 +43,6 @@ export async function confirmDelete() { export function closeDeleteModal() { modalManager.closeModal('deleteModal'); pendingDeletePath = null; - pendingModelType = null; } // Functions for the exclude modal @@ -67,7 +66,6 @@ export function showExcludeModal(filePath) { export function closeExcludeModal() { modalManager.closeModal('excludeModal'); pendingExcludePath = null; - pendingExcludeModelType = null; } export async function confirmExclude() { diff --git a/static/js/utils/uiHelpers.js b/static/js/utils/uiHelpers.js index 1d03a01f..0b46245b 100644 --- a/static/js/utils/uiHelpers.js +++ b/static/js/utils/uiHelpers.js @@ -1,4 +1,4 @@ -import { state } from '../state/index.js'; +import { state, getCurrentPageState } from '../state/index.js'; import { resetAndReload } from '../api/loraApi.js'; import { getStorageItem, setStorageItem } from './storageHelpers.js'; import { NODE_TYPES, NODE_TYPE_ICONS, DEFAULT_NODE_COLOR } from './constants.js'; @@ -626,7 +626,8 @@ export function updateFolderTags(folders) { if (!folderTagsContainer) return; // Keep track of currently selected folder - const currentFolder = this.pageState.activeFolder; + const pageState = getCurrentPageState(); + const currentFolder = pageState.activeFolder; // Create HTML for folder tags const tagsHTML = folders.map(folder => {