mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
Add bulk operation
This commit is contained in:
@@ -43,6 +43,7 @@ class ApiRoutes:
|
|||||||
app.router.add_post('/api/move_model', routes.move_model)
|
app.router.add_post('/api/move_model', routes.move_model)
|
||||||
app.router.add_post('/loras/api/save-metadata', routes.save_metadata)
|
app.router.add_post('/loras/api/save-metadata', routes.save_metadata)
|
||||||
app.router.add_get('/api/lora-preview-url', routes.get_lora_preview_url) # Add new route
|
app.router.add_get('/api/lora-preview-url', routes.get_lora_preview_url) # Add new route
|
||||||
|
app.router.add_post('/api/move_models_bulk', routes.move_models_bulk)
|
||||||
|
|
||||||
# Add update check routes
|
# Add update check routes
|
||||||
UpdateRoutes.setup_routes(app)
|
UpdateRoutes.setup_routes(app)
|
||||||
@@ -654,3 +655,39 @@ class ApiRoutes:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error getting lora preview URL: {e}", exc_info=True)
|
logger.error(f"Error getting lora preview URL: {e}", exc_info=True)
|
||||||
return web.Response(text=str(e), status=500)
|
return web.Response(text=str(e), status=500)
|
||||||
|
|
||||||
|
async def move_models_bulk(self, request: web.Request) -> web.Response:
|
||||||
|
"""Handle bulk model move request"""
|
||||||
|
try:
|
||||||
|
data = await request.json()
|
||||||
|
file_paths = data.get('file_paths', [])
|
||||||
|
target_path = data.get('target_path')
|
||||||
|
|
||||||
|
if not file_paths or not target_path:
|
||||||
|
return web.Response(text='File paths and target path are required', status=400)
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for file_path in file_paths:
|
||||||
|
success = await self.scanner.move_model(file_path, target_path)
|
||||||
|
results.append({"path": file_path, "success": success})
|
||||||
|
|
||||||
|
# Count successes
|
||||||
|
success_count = sum(1 for r in results if r["success"])
|
||||||
|
|
||||||
|
if success_count == len(file_paths):
|
||||||
|
return web.json_response({
|
||||||
|
'success': True,
|
||||||
|
'message': f'Successfully moved {success_count} models'
|
||||||
|
})
|
||||||
|
elif success_count > 0:
|
||||||
|
return web.json_response({
|
||||||
|
'success': True,
|
||||||
|
'message': f'Moved {success_count} of {len(file_paths)} models',
|
||||||
|
'results': results
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
return web.Response(text='Failed to move any models', status=500)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error moving models in bulk: {e}", exc_info=True)
|
||||||
|
return web.Response(text=str(e), status=500)
|
||||||
|
|||||||
@@ -385,4 +385,138 @@
|
|||||||
.back-to-top {
|
.back-to-top {
|
||||||
bottom: 60px; /* Give some extra space from bottom on mobile */
|
bottom: 60px; /* Give some extra space from bottom on mobile */
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bulk Operations Styles */
|
||||||
|
.bulk-operations-panel {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateY(100px) translateX(-50%);
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius-base);
|
||||||
|
padding: 12px 16px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
z-index: var(--z-overlay);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 300px;
|
||||||
|
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk-operations-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
gap: 20px; /* Increase space between count and buttons */
|
||||||
|
}
|
||||||
|
|
||||||
|
#selectedCount {
|
||||||
|
font-weight: 500;
|
||||||
|
background: var(--bg-color);
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
min-width: 80px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk-operations-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk-operations-actions button {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
|
background: var(--bg-color);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk-operations-actions button:hover {
|
||||||
|
background: var(--lora-accent);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--lora-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style for selected cards */
|
||||||
|
.lora-card.selected {
|
||||||
|
box-shadow: 0 0 0 2px var(--lora-accent);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lora-card.selected::after {
|
||||||
|
content: "✓";
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
background: var(--lora-accent);
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 14px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Standardize button widths in controls */
|
||||||
|
.control-group button {
|
||||||
|
min-width: 100px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Update bulk operations button to match others when active */
|
||||||
|
#bulkOperationsBtn.active {
|
||||||
|
background: var(--lora-accent);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--lora-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.bulk-operations-panel {
|
||||||
|
width: calc(100% - 40px);
|
||||||
|
left: 20px;
|
||||||
|
transform: none;
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk-operations-actions {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk-operations-panel.visible {
|
||||||
|
transform: translateY(0) translateX(-50%);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove the page overlay */
|
||||||
|
.bulk-mode-overlay {
|
||||||
|
display: none; /* Hide the overlay completely */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove card scaling in bulk mode but keep the transition for other properties */
|
||||||
|
.lora-card {
|
||||||
|
transition: box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove the transform scale from bulk mode cards */
|
||||||
|
.bulk-mode .lora-card {
|
||||||
|
transform: none;
|
||||||
}
|
}
|
||||||
@@ -60,23 +60,30 @@ export function createLoraCard(lora) {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Main card click event
|
// Main card click event - modified to handle bulk mode
|
||||||
card.addEventListener('click', () => {
|
card.addEventListener('click', () => {
|
||||||
const loraMeta = {
|
// Check if we're in bulk mode
|
||||||
sha256: card.dataset.sha256,
|
if (state.bulkMode) {
|
||||||
file_path: card.dataset.filepath,
|
// Toggle selection
|
||||||
model_name: card.dataset.name,
|
toggleCardSelection(card);
|
||||||
file_name: card.dataset.file_name,
|
} else {
|
||||||
folder: card.dataset.folder,
|
// Normal behavior - show modal
|
||||||
modified: card.dataset.modified,
|
const loraMeta = {
|
||||||
file_size: card.dataset.file_size,
|
sha256: card.dataset.sha256,
|
||||||
from_civitai: card.dataset.from_civitai === 'true',
|
file_path: card.dataset.filepath,
|
||||||
base_model: card.dataset.base_model,
|
model_name: card.dataset.name,
|
||||||
usage_tips: card.dataset.usage_tips,
|
file_name: card.dataset.file_name,
|
||||||
notes: card.dataset.notes,
|
folder: card.dataset.folder,
|
||||||
civitai: JSON.parse(card.dataset.meta || '{}')
|
modified: card.dataset.modified,
|
||||||
};
|
file_size: card.dataset.file_size,
|
||||||
showLoraModal(loraMeta);
|
from_civitai: card.dataset.from_civitai === 'true',
|
||||||
|
base_model: card.dataset.base_model,
|
||||||
|
usage_tips: card.dataset.usage_tips,
|
||||||
|
notes: card.dataset.notes,
|
||||||
|
civitai: JSON.parse(card.dataset.meta || '{}')
|
||||||
|
};
|
||||||
|
showLoraModal(loraMeta);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Copy button click event
|
// Copy button click event
|
||||||
@@ -127,6 +134,35 @@ export function createLoraCard(lora) {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
replacePreview(lora.file_path);
|
replacePreview(lora.file_path);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Apply bulk mode styling if currently in bulk mode
|
||||||
|
if (state.bulkMode) {
|
||||||
|
const actions = card.querySelectorAll('.card-actions');
|
||||||
|
actions.forEach(actionGroup => {
|
||||||
|
actionGroup.style.display = 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return card;
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to toggle selection of a card
|
||||||
|
function toggleCardSelection(card) {
|
||||||
|
card.classList.toggle('selected');
|
||||||
|
updateSelectedCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a method to update card appearance based on bulk mode
|
||||||
|
export function updateCardsForBulkMode(isBulkMode) {
|
||||||
|
// Update the state
|
||||||
|
state.bulkMode = isBulkMode;
|
||||||
|
|
||||||
|
document.body.classList.toggle('bulk-mode', isBulkMode);
|
||||||
|
|
||||||
|
document.querySelectorAll('.lora-card').forEach(card => {
|
||||||
|
const actions = card.querySelectorAll('.card-actions');
|
||||||
|
actions.forEach(actionGroup => {
|
||||||
|
actionGroup.style.display = isBulkMode ? 'none' : 'flex';
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
@@ -27,8 +27,14 @@ import { SettingsManager, toggleApiKeyVisibility } from './managers/SettingsMana
|
|||||||
import { LoraContextMenu } from './components/ContextMenu.js';
|
import { LoraContextMenu } from './components/ContextMenu.js';
|
||||||
import { moveManager } from './managers/MoveManager.js';
|
import { moveManager } from './managers/MoveManager.js';
|
||||||
import { FilterManager } from './managers/FilterManager.js';
|
import { FilterManager } from './managers/FilterManager.js';
|
||||||
|
import { createLoraCard, updateCardsForBulkMode } from './components/LoraCard.js';
|
||||||
|
import { bulkManager } from './managers/BulkManager.js';
|
||||||
|
|
||||||
// Export all functions that need global access
|
// Add bulk mode to state
|
||||||
|
state.bulkMode = false;
|
||||||
|
state.selectedLoras = new Set();
|
||||||
|
|
||||||
|
// Export functions to global window object
|
||||||
window.loadMoreLoras = loadMoreLoras;
|
window.loadMoreLoras = loadMoreLoras;
|
||||||
window.fetchCivitai = fetchCivitai;
|
window.fetchCivitai = fetchCivitai;
|
||||||
window.deleteModel = deleteModel;
|
window.deleteModel = deleteModel;
|
||||||
@@ -51,6 +57,14 @@ window.moveManager = moveManager;
|
|||||||
window.toggleShowcase = toggleShowcase;
|
window.toggleShowcase = toggleShowcase;
|
||||||
window.scrollToTop = scrollToTop;
|
window.scrollToTop = scrollToTop;
|
||||||
|
|
||||||
|
// Export bulk manager methods to window
|
||||||
|
window.toggleBulkMode = () => bulkManager.toggleBulkMode();
|
||||||
|
window.clearSelection = () => bulkManager.clearSelection();
|
||||||
|
window.toggleCardSelection = (card) => bulkManager.toggleCardSelection(card);
|
||||||
|
window.copyAllLorasSyntax = () => bulkManager.copyAllLorasSyntax();
|
||||||
|
window.updateSelectedCount = () => bulkManager.updateSelectedCount();
|
||||||
|
window.bulkManager = bulkManager;
|
||||||
|
|
||||||
// Initialize everything when DOM is ready
|
// Initialize everything when DOM is ready
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
state.loadingManager = new LoadingManager();
|
state.loadingManager = new LoadingManager();
|
||||||
@@ -73,6 +87,12 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
initBackToTop();
|
initBackToTop();
|
||||||
window.searchManager = new SearchManager();
|
window.searchManager = new SearchManager();
|
||||||
new LoraContextMenu();
|
new LoraContextMenu();
|
||||||
|
|
||||||
|
// Initialize cards for current bulk mode state (should be false initially)
|
||||||
|
updateCardsForBulkMode(state.bulkMode);
|
||||||
|
|
||||||
|
// Initialize the bulk manager
|
||||||
|
bulkManager.initialize();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize event listeners
|
// Initialize event listeners
|
||||||
|
|||||||
106
static/js/managers/BulkManager.js
Normal file
106
static/js/managers/BulkManager.js
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import { state } from '../state/index.js';
|
||||||
|
import { showToast } from '../utils/uiHelpers.js';
|
||||||
|
import { updateCardsForBulkMode } from '../components/LoraCard.js';
|
||||||
|
|
||||||
|
export class BulkManager {
|
||||||
|
constructor() {
|
||||||
|
this.bulkBtn = document.getElementById('bulkOperationsBtn');
|
||||||
|
this.bulkPanel = document.getElementById('bulkOperationsPanel');
|
||||||
|
|
||||||
|
// Initialize selected loras set in state if not already there
|
||||||
|
if (!state.selectedLoras) {
|
||||||
|
state.selectedLoras = new Set();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
// Add event listeners if needed
|
||||||
|
// (Already handled via onclick attributes in HTML, but could be moved here)
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleBulkMode() {
|
||||||
|
// Toggle the state
|
||||||
|
state.bulkMode = !state.bulkMode;
|
||||||
|
|
||||||
|
// Update UI
|
||||||
|
this.bulkBtn.classList.toggle('active', state.bulkMode);
|
||||||
|
|
||||||
|
// Important: Remove the hidden class when entering bulk mode
|
||||||
|
if (state.bulkMode) {
|
||||||
|
this.bulkPanel.classList.remove('hidden');
|
||||||
|
// Use setTimeout to ensure the DOM updates before adding visible class
|
||||||
|
// This helps with the transition animation
|
||||||
|
setTimeout(() => {
|
||||||
|
this.bulkPanel.classList.add('visible');
|
||||||
|
}, 10);
|
||||||
|
} else {
|
||||||
|
this.bulkPanel.classList.remove('visible');
|
||||||
|
// Add hidden class back after transition completes
|
||||||
|
setTimeout(() => {
|
||||||
|
this.bulkPanel.classList.add('hidden');
|
||||||
|
}, 400); // Match this with the transition duration in CSS
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update all cards
|
||||||
|
updateCardsForBulkMode(state.bulkMode);
|
||||||
|
|
||||||
|
// Clear selection if exiting bulk mode
|
||||||
|
if (!state.bulkMode) {
|
||||||
|
this.clearSelection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearSelection() {
|
||||||
|
document.querySelectorAll('.lora-card.selected').forEach(card => {
|
||||||
|
card.classList.remove('selected');
|
||||||
|
});
|
||||||
|
state.selectedLoras.clear();
|
||||||
|
this.updateSelectedCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSelectedCount() {
|
||||||
|
const selectedCards = document.querySelectorAll('.lora-card.selected');
|
||||||
|
const countElement = document.getElementById('selectedCount');
|
||||||
|
|
||||||
|
if (countElement) {
|
||||||
|
countElement.textContent = `${selectedCards.length} selected`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update state with selected loras
|
||||||
|
state.selectedLoras.clear();
|
||||||
|
selectedCards.forEach(card => {
|
||||||
|
state.selectedLoras.add(card.dataset.filepath);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleCardSelection(card) {
|
||||||
|
card.classList.toggle('selected');
|
||||||
|
this.updateSelectedCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
async copyAllLorasSyntax() {
|
||||||
|
const selectedCards = document.querySelectorAll('.lora-card.selected');
|
||||||
|
if (selectedCards.length === 0) {
|
||||||
|
showToast('No LoRAs selected', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loraSyntaxes = [];
|
||||||
|
selectedCards.forEach(card => {
|
||||||
|
const usageTips = JSON.parse(card.dataset.usage_tips || '{}');
|
||||||
|
const strength = usageTips.strength || 1;
|
||||||
|
loraSyntaxes.push(`<lora:${card.dataset.file_name}:${strength}>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(loraSyntaxes.join(', '));
|
||||||
|
showToast(`Copied ${selectedCards.length} LoRA syntaxes to clipboard`, 'success');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Copy failed:', err);
|
||||||
|
showToast('Copy failed', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a singleton instance
|
||||||
|
export const bulkManager = new BulkManager();
|
||||||
@@ -5,11 +5,13 @@ import { modalManager } from './ModalManager.js';
|
|||||||
class MoveManager {
|
class MoveManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.currentFilePath = null;
|
this.currentFilePath = null;
|
||||||
|
this.bulkFilePaths = null;
|
||||||
this.modal = document.getElementById('moveModal');
|
this.modal = document.getElementById('moveModal');
|
||||||
this.loraRootSelect = document.getElementById('moveLoraRoot');
|
this.loraRootSelect = document.getElementById('moveLoraRoot');
|
||||||
this.folderBrowser = document.getElementById('moveFolderBrowser');
|
this.folderBrowser = document.getElementById('moveFolderBrowser');
|
||||||
this.newFolderInput = document.getElementById('moveNewFolder');
|
this.newFolderInput = document.getElementById('moveNewFolder');
|
||||||
this.pathDisplay = document.getElementById('moveTargetPathDisplay');
|
this.pathDisplay = document.getElementById('moveTargetPathDisplay');
|
||||||
|
this.modalTitle = document.getElementById('moveModalTitle');
|
||||||
|
|
||||||
this.initializeEventListeners();
|
this.initializeEventListeners();
|
||||||
}
|
}
|
||||||
@@ -43,7 +45,24 @@ class MoveManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async showMoveModal(filePath) {
|
async showMoveModal(filePath) {
|
||||||
this.currentFilePath = filePath;
|
// Reset state
|
||||||
|
this.currentFilePath = null;
|
||||||
|
this.bulkFilePaths = null;
|
||||||
|
|
||||||
|
// Handle bulk mode
|
||||||
|
if (filePath === 'bulk') {
|
||||||
|
const selectedPaths = Array.from(state.selectedLoras);
|
||||||
|
if (selectedPaths.length === 0) {
|
||||||
|
showToast('No LoRAs selected', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.bulkFilePaths = selectedPaths;
|
||||||
|
this.modalTitle.textContent = `Move ${selectedPaths.length} LoRAs`;
|
||||||
|
} else {
|
||||||
|
// Single file mode
|
||||||
|
this.currentFilePath = filePath;
|
||||||
|
this.modalTitle.textContent = "Move Model";
|
||||||
|
}
|
||||||
|
|
||||||
// 清除之前的选择
|
// 清除之前的选择
|
||||||
this.folderBrowser.querySelectorAll('.folder-item').forEach(item => {
|
this.folderBrowser.querySelectorAll('.folder-item').forEach(item => {
|
||||||
@@ -105,36 +124,81 @@ class MoveManager {
|
|||||||
targetPath = `${targetPath}/${newFolder}`;
|
targetPath = `${targetPath}/${newFolder}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (this.bulkFilePaths) {
|
||||||
|
// Bulk move mode
|
||||||
|
await this.moveBulkModels(this.bulkFilePaths, targetPath);
|
||||||
|
} else {
|
||||||
|
// Single move mode
|
||||||
|
await this.moveSingleModel(this.currentFilePath, targetPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
modalManager.closeModal('moveModal');
|
||||||
|
await resetAndReload(true);
|
||||||
|
|
||||||
|
// If we were in bulk mode, exit it after successful move
|
||||||
|
if (this.bulkFilePaths && state.bulkMode) {
|
||||||
|
toggleBulkMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error moving model(s):', error);
|
||||||
|
showToast('Failed to move model(s): ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async moveSingleModel(filePath, targetPath) {
|
||||||
// show toast if current path is same as target path
|
// show toast if current path is same as target path
|
||||||
if (this.currentFilePath.substring(0, this.currentFilePath.lastIndexOf('/')) === targetPath) {
|
if (filePath.substring(0, filePath.lastIndexOf('/')) === targetPath) {
|
||||||
showToast('Model is already in the selected folder', 'info');
|
showToast('Model is already in the selected folder', 'info');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const response = await fetch('/api/move_model', {
|
||||||
const response = await fetch('/api/move_model', {
|
method: 'POST',
|
||||||
method: 'POST',
|
headers: {
|
||||||
headers: {
|
'Content-Type': 'application/json',
|
||||||
'Content-Type': 'application/json',
|
},
|
||||||
},
|
body: JSON.stringify({
|
||||||
body: JSON.stringify({
|
file_path: filePath,
|
||||||
file_path: this.currentFilePath,
|
target_path: targetPath
|
||||||
target_path: targetPath
|
})
|
||||||
})
|
});
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to move model');
|
throw new Error('Failed to move model');
|
||||||
}
|
|
||||||
|
|
||||||
showToast('Model moved successfully', 'success');
|
|
||||||
modalManager.closeModal('moveModal');
|
|
||||||
await resetAndReload(true);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error moving model:', error);
|
|
||||||
showToast('Failed to move model: ' + error.message, 'error');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to move models');
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast(`Successfully moved ${movedPaths.length} models`, 'success');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,5 +10,6 @@ export const state = {
|
|||||||
searchManager: null,
|
searchManager: null,
|
||||||
filters: {
|
filters: {
|
||||||
baseModel: []
|
baseModel: []
|
||||||
}
|
},
|
||||||
|
bulkMode: false
|
||||||
};
|
};
|
||||||
@@ -28,6 +28,11 @@
|
|||||||
<i class="fas fa-cloud-download-alt"></i> Download
|
<i class="fas fa-cloud-download-alt"></i> Download
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="control-group">
|
||||||
|
<button id="bulkOperationsBtn" onclick="bulkManager.toggleBulkMode()" title="Bulk Operations">
|
||||||
|
<i class="fas fa-th-large"></i> Bulk
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div class="search-container">
|
<div class="search-container">
|
||||||
<input type="text" id="searchInput" placeholder="Search models..." />
|
<input type="text" id="searchInput" placeholder="Search models..." />
|
||||||
<!-- 清空按钮将由JavaScript动态添加到这里 -->
|
<!-- 清空按钮将由JavaScript动态添加到这里 -->
|
||||||
@@ -62,4 +67,22 @@
|
|||||||
Clear All Filters
|
Clear All Filters
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add bulk operations panel (initially hidden) -->
|
||||||
|
<div id="bulkOperationsPanel" class="bulk-operations-panel hidden">
|
||||||
|
<div class="bulk-operations-header">
|
||||||
|
<span id="selectedCount">0 selected</span>
|
||||||
|
<div class="bulk-operations-actions">
|
||||||
|
<button onclick="bulkManager.copyAllLorasSyntax()" title="Copy all selected LoRAs syntax">
|
||||||
|
<i class="fas fa-copy"></i> Copy All
|
||||||
|
</button>
|
||||||
|
<button onclick="moveManager.showMoveModal('bulk')" title="Move selected LoRAs to folder">
|
||||||
|
<i class="fas fa-folder-open"></i> Move All
|
||||||
|
</button>
|
||||||
|
<button onclick="bulkManager.clearSelection()" title="Clear selection">
|
||||||
|
<i class="fas fa-times"></i> Clear
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -86,8 +86,10 @@
|
|||||||
<!-- Move Model Modal -->
|
<!-- Move Model Modal -->
|
||||||
<div id="moveModal" class="modal">
|
<div id="moveModal" class="modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<button class="close" onclick="modalManager.closeModal('moveModal')">×</button>
|
<div class="modal-header">
|
||||||
<h2>Move Model</h2>
|
<h2 id="moveModalTitle">Move Model</h2>
|
||||||
|
<span class="close" onclick="modalManager.closeModal('moveModal')">×</span>
|
||||||
|
</div>
|
||||||
<div class="location-selection">
|
<div class="location-selection">
|
||||||
<div class="path-preview">
|
<div class="path-preview">
|
||||||
<label>Target Location Preview:</label>
|
<label>Target Location Preview:</label>
|
||||||
|
|||||||
@@ -88,9 +88,13 @@
|
|||||||
<div class="card-grid" id="loraGrid" style="height: calc(100vh - [header-height]px); overflow-y: auto;">
|
<div class="card-grid" id="loraGrid" style="height: calc(100vh - [header-height]px); overflow-y: auto;">
|
||||||
<!-- Cards will be dynamically inserted here -->
|
<!-- Cards will be dynamically inserted here -->
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Bulk operations panel will be inserted here by JavaScript -->
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Add after the container div -->
|
||||||
|
<div class="bulk-mode-overlay"></div>
|
||||||
|
|
||||||
<script type="module" src="/loras_static/js/main.js"></script>
|
<script type="module" src="/loras_static/js/main.js"></script>
|
||||||
{% if is_initializing %}
|
{% if is_initializing %}
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
Reference in New Issue
Block a user