mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-24 14:42:11 -03:00
Add rename functionality for LoRA files and enhance UI for editing file names
- Introduced a new API endpoint to rename LoRA files, including validation and error handling for file paths and names. - Updated the RecipeScanner to reflect changes in LoRA filenames across recipe files and cache. - Enhanced the LoraModal UI to allow inline editing of file names with improved user interaction and validation. - Added CSS styles for the editing interface to improve visual feedback during file name editing.
This commit is contained in:
@@ -52,6 +52,7 @@ class ApiRoutes:
|
|||||||
app.router.add_get('/api/loras/top-tags', routes.get_top_tags) # Add new route for top tags
|
app.router.add_get('/api/loras/top-tags', routes.get_top_tags) # Add new route for top tags
|
||||||
app.router.add_get('/api/loras/base-models', routes.get_base_models) # Add new route for base models
|
app.router.add_get('/api/loras/base-models', routes.get_base_models) # Add new route for base models
|
||||||
app.router.add_get('/api/lora-civitai-url', routes.get_lora_civitai_url) # Add new route for Civitai URL
|
app.router.add_get('/api/lora-civitai-url', routes.get_lora_civitai_url) # Add new route for Civitai URL
|
||||||
|
app.router.add_post('/api/rename_lora', routes.rename_lora) # Add new route for renaming LoRA files
|
||||||
|
|
||||||
# Add update check routes
|
# Add update check routes
|
||||||
UpdateRoutes.setup_routes(app)
|
UpdateRoutes.setup_routes(app)
|
||||||
@@ -929,6 +930,149 @@ class ApiRoutes:
|
|||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error retrieving base models: {e}", exc_info=True)
|
logger.error(f"Error retrieving base models: {e}", exc_info=True)
|
||||||
|
return web.json_response({
|
||||||
|
'success': False,
|
||||||
|
'error': str(e)
|
||||||
|
}, status=500)
|
||||||
|
|
||||||
|
def get_multipart_ext(self, filename):
|
||||||
|
parts = filename.split(".")
|
||||||
|
if len(parts) > 2: # 如果包含多级扩展名
|
||||||
|
return "." + ".".join(parts[-2:]) # 取最后两部分,如 ".metadata.json"
|
||||||
|
return os.path.splitext(filename)[1] # 否则取普通扩展名,如 ".safetensors"
|
||||||
|
|
||||||
|
async def rename_lora(self, request: web.Request) -> web.Response:
|
||||||
|
"""Handle renaming a LoRA file and its associated files"""
|
||||||
|
try:
|
||||||
|
data = await request.json()
|
||||||
|
file_path = data.get('file_path')
|
||||||
|
new_file_name = data.get('new_file_name')
|
||||||
|
|
||||||
|
if not file_path or not new_file_name:
|
||||||
|
return web.json_response({
|
||||||
|
'success': False,
|
||||||
|
'error': 'File path and new file name are required'
|
||||||
|
}, status=400)
|
||||||
|
|
||||||
|
# Validate the new file name (no path separators or invalid characters)
|
||||||
|
invalid_chars = ['/', '\\', ':', '*', '?', '"', '<', '>', '|']
|
||||||
|
if any(char in new_file_name for char in invalid_chars):
|
||||||
|
return web.json_response({
|
||||||
|
'success': False,
|
||||||
|
'error': 'Invalid characters in file name'
|
||||||
|
}, status=400)
|
||||||
|
|
||||||
|
# Get the directory and current file name
|
||||||
|
target_dir = os.path.dirname(file_path)
|
||||||
|
old_file_name = os.path.splitext(os.path.basename(file_path))[0]
|
||||||
|
|
||||||
|
# Check if the target file already exists
|
||||||
|
new_file_path = os.path.join(target_dir, f"{new_file_name}.safetensors").replace(os.sep, '/')
|
||||||
|
if os.path.exists(new_file_path):
|
||||||
|
return web.json_response({
|
||||||
|
'success': False,
|
||||||
|
'error': 'A file with this name already exists'
|
||||||
|
}, status=400)
|
||||||
|
|
||||||
|
# Define the patterns for associated files
|
||||||
|
patterns = [
|
||||||
|
f"{old_file_name}.safetensors", # Required
|
||||||
|
f"{old_file_name}.metadata.json",
|
||||||
|
f"{old_file_name}.preview.png",
|
||||||
|
f"{old_file_name}.preview.jpg",
|
||||||
|
f"{old_file_name}.preview.jpeg",
|
||||||
|
f"{old_file_name}.preview.webp",
|
||||||
|
f"{old_file_name}.preview.mp4",
|
||||||
|
f"{old_file_name}.png",
|
||||||
|
f"{old_file_name}.jpg",
|
||||||
|
f"{old_file_name}.jpeg",
|
||||||
|
f"{old_file_name}.webp",
|
||||||
|
f"{old_file_name}.mp4"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Find all matching files
|
||||||
|
existing_files = []
|
||||||
|
for pattern in patterns:
|
||||||
|
path = os.path.join(target_dir, pattern)
|
||||||
|
if os.path.exists(path):
|
||||||
|
existing_files.append((path, pattern))
|
||||||
|
|
||||||
|
# Get the hash from the main file to update hash index
|
||||||
|
hash_value = None
|
||||||
|
metadata = None
|
||||||
|
metadata_path = os.path.join(target_dir, f"{old_file_name}.metadata.json")
|
||||||
|
|
||||||
|
if os.path.exists(metadata_path):
|
||||||
|
try:
|
||||||
|
with open(metadata_path, 'r', encoding='utf-8') as f:
|
||||||
|
metadata = json.load(f)
|
||||||
|
hash_value = metadata.get('sha256')
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error loading metadata for rename: {e}")
|
||||||
|
|
||||||
|
# Rename all files
|
||||||
|
renamed_files = []
|
||||||
|
new_metadata_path = None
|
||||||
|
|
||||||
|
# Notify file monitor to ignore these events
|
||||||
|
main_file_path = os.path.join(target_dir, f"{old_file_name}.safetensors")
|
||||||
|
if os.path.exists(main_file_path) and self.download_manager.file_monitor:
|
||||||
|
# Add old and new paths to ignore list
|
||||||
|
file_size = os.path.getsize(main_file_path)
|
||||||
|
self.download_manager.file_monitor.handler.add_ignore_path(main_file_path, file_size)
|
||||||
|
self.download_manager.file_monitor.handler.add_ignore_path(new_file_path, file_size)
|
||||||
|
|
||||||
|
for old_path, pattern in existing_files:
|
||||||
|
# Get the file extension like .safetensors or .metadata.json
|
||||||
|
ext = self.get_multipart_ext(pattern)
|
||||||
|
|
||||||
|
# Create the new path
|
||||||
|
new_path = os.path.join(target_dir, f"{new_file_name}{ext}").replace(os.sep, '/')
|
||||||
|
|
||||||
|
# Rename the file
|
||||||
|
os.rename(old_path, new_path)
|
||||||
|
renamed_files.append(new_path)
|
||||||
|
|
||||||
|
# Keep track of metadata path for later update
|
||||||
|
if ext == '.metadata.json':
|
||||||
|
new_metadata_path = new_path
|
||||||
|
|
||||||
|
# Update the metadata file with new file name and paths
|
||||||
|
if new_metadata_path and metadata:
|
||||||
|
# Update file_name, file_path and preview_url in metadata
|
||||||
|
metadata['file_name'] = new_file_name
|
||||||
|
metadata['file_path'] = new_file_path
|
||||||
|
|
||||||
|
# Update preview_url if it exists
|
||||||
|
if 'preview_url' in metadata and metadata['preview_url']:
|
||||||
|
old_preview = metadata['preview_url']
|
||||||
|
ext = self.get_multipart_ext(old_preview)
|
||||||
|
new_preview = os.path.join(target_dir, f"{new_file_name}{ext}").replace(os.sep, '/')
|
||||||
|
metadata['preview_url'] = new_preview
|
||||||
|
|
||||||
|
# Save updated metadata
|
||||||
|
with open(new_metadata_path, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(metadata, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
# Update the scanner cache
|
||||||
|
if metadata:
|
||||||
|
await self.scanner.update_single_lora_cache(file_path, new_file_path, metadata)
|
||||||
|
|
||||||
|
# Update recipe files and cache if hash is available
|
||||||
|
if hash_value:
|
||||||
|
recipe_scanner = RecipeScanner(self.scanner)
|
||||||
|
recipes_updated, cache_updated = await recipe_scanner.update_lora_filename_by_hash(hash_value, new_file_name)
|
||||||
|
logger.info(f"Updated {recipes_updated} recipe files and {cache_updated} cache entries for renamed LoRA")
|
||||||
|
|
||||||
|
return web.json_response({
|
||||||
|
'success': True,
|
||||||
|
'new_file_path': new_file_path,
|
||||||
|
'renamed_files': renamed_files,
|
||||||
|
'reload_required': False
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error renaming LoRA: {e}", exc_info=True)
|
||||||
return web.json_response({
|
return web.json_response({
|
||||||
'success': False,
|
'success': False,
|
||||||
'error': str(e)
|
'error': str(e)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import os
|
|||||||
import logging
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
from typing import List, Dict, Optional, Any
|
from typing import List, Dict, Optional, Any, Tuple
|
||||||
from ..config import config
|
from ..config import config
|
||||||
from .recipe_cache import RecipeCache
|
from .recipe_cache import RecipeCache
|
||||||
from .lora_scanner import LoraScanner
|
from .lora_scanner import LoraScanner
|
||||||
@@ -430,3 +430,87 @@ class RecipeScanner:
|
|||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
async def update_lora_filename_by_hash(self, hash_value: str, new_file_name: str) -> Tuple[int, int]:
|
||||||
|
"""Update file_name in all recipes that contain a LoRA with the specified hash.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
hash_value: The SHA256 hash value of the LoRA
|
||||||
|
new_file_name: The new file_name to set
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[int, int]: (number of recipes updated in files, number of recipes updated in cache)
|
||||||
|
"""
|
||||||
|
if not hash_value or not new_file_name:
|
||||||
|
return 0, 0
|
||||||
|
|
||||||
|
# Always use lowercase hash for consistency
|
||||||
|
hash_value = hash_value.lower()
|
||||||
|
|
||||||
|
# Get recipes directory
|
||||||
|
recipes_dir = self.recipes_dir
|
||||||
|
if not recipes_dir or not os.path.exists(recipes_dir):
|
||||||
|
logger.warning(f"Recipes directory not found: {recipes_dir}")
|
||||||
|
return 0, 0
|
||||||
|
|
||||||
|
# Check if cache is initialized
|
||||||
|
cache_initialized = self._cache is not None
|
||||||
|
cache_updated_count = 0
|
||||||
|
file_updated_count = 0
|
||||||
|
|
||||||
|
# Get all recipe JSON files in the recipes directory
|
||||||
|
recipe_files = []
|
||||||
|
for root, _, files in os.walk(recipes_dir):
|
||||||
|
for file in files:
|
||||||
|
if file.lower().endswith('.recipe.json'):
|
||||||
|
recipe_files.append(os.path.join(root, file))
|
||||||
|
|
||||||
|
# Process each recipe file
|
||||||
|
for recipe_path in recipe_files:
|
||||||
|
try:
|
||||||
|
# Load the recipe data
|
||||||
|
with open(recipe_path, 'r', encoding='utf-8') as f:
|
||||||
|
recipe_data = json.load(f)
|
||||||
|
|
||||||
|
# Skip if no loras or invalid structure
|
||||||
|
if not recipe_data or not isinstance(recipe_data, dict) or 'loras' not in recipe_data:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check if any lora has matching hash
|
||||||
|
file_updated = False
|
||||||
|
for lora in recipe_data.get('loras', []):
|
||||||
|
if 'hash' in lora and lora['hash'].lower() == hash_value:
|
||||||
|
# Update file_name
|
||||||
|
old_file_name = lora.get('file_name', '')
|
||||||
|
lora['file_name'] = new_file_name
|
||||||
|
file_updated = True
|
||||||
|
logger.info(f"Updated file_name in recipe {recipe_path}: {old_file_name} -> {new_file_name}")
|
||||||
|
|
||||||
|
# If updated, save the file
|
||||||
|
if file_updated:
|
||||||
|
with open(recipe_path, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(recipe_data, f, indent=4, ensure_ascii=False)
|
||||||
|
file_updated_count += 1
|
||||||
|
|
||||||
|
# Also update in cache if it exists
|
||||||
|
if cache_initialized:
|
||||||
|
recipe_id = recipe_data.get('id')
|
||||||
|
if recipe_id:
|
||||||
|
for cache_item in self._cache.raw_data:
|
||||||
|
if cache_item.get('id') == recipe_id:
|
||||||
|
# Replace loras array with updated version
|
||||||
|
cache_item['loras'] = recipe_data['loras']
|
||||||
|
cache_updated_count += 1
|
||||||
|
break
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error updating recipe file {recipe_path}: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc(file=sys.stderr)
|
||||||
|
|
||||||
|
# Resort cache if updates were made
|
||||||
|
if cache_initialized and cache_updated_count > 0:
|
||||||
|
await self._cache.resort()
|
||||||
|
logger.info(f"Resorted recipe cache after updating {cache_updated_count} items")
|
||||||
|
|
||||||
|
return file_updated_count, cache_updated_count
|
||||||
|
|||||||
@@ -543,25 +543,53 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
cursor: pointer;
|
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
transition: background-color 0.2s;
|
transition: background-color 0.2s;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-name-wrapper:hover {
|
.file-name-wrapper:hover {
|
||||||
background: oklch(var(--lora-accent) / 0.1);
|
background: oklch(var(--lora-accent) / 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-name-wrapper i {
|
.file-name-content {
|
||||||
color: var(--text-color);
|
padding: 2px 4px;
|
||||||
opacity: 0.5;
|
border-radius: var(--border-radius-xs);
|
||||||
transition: opacity 0.2s;
|
border: 1px solid transparent;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-name-wrapper:hover i {
|
.file-name-wrapper.editing .file-name-content {
|
||||||
opacity: 1;
|
border: 1px solid var(--lora-accent);
|
||||||
color: var(--lora-accent);
|
background: var(--bg-color);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-file-name-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-color);
|
||||||
|
opacity: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 5px;
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
margin-left: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-file-name-btn.visible,
|
||||||
|
.file-name-wrapper:hover .edit-file-name-btn {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-file-name-btn:hover {
|
||||||
|
opacity: 0.8 !important;
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .edit-file-name-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Base Model and Size combined styles */
|
/* Base Model and Size combined styles */
|
||||||
|
|||||||
@@ -29,9 +29,11 @@ export function showLoraModal(lora) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<label>File Name</label>
|
<label>File Name</label>
|
||||||
<div class="file-name-wrapper" onclick="copyFileName('${lora.file_name}')">
|
<div class="file-name-wrapper">
|
||||||
<span id="file-name">${lora.file_name || 'N/A'}</span>
|
<span id="file-name" class="file-name-content">${lora.file_name || 'N/A'}</span>
|
||||||
<i class="fas fa-copy" title="Copy file name"></i>
|
<button class="edit-file-name-btn" title="Edit file name">
|
||||||
|
<i class="fas fa-pencil-alt"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item location-size">
|
<div class="info-item location-size">
|
||||||
@@ -130,6 +132,7 @@ export function showLoraModal(lora) {
|
|||||||
setupTriggerWordsEditMode();
|
setupTriggerWordsEditMode();
|
||||||
setupModelNameEditing();
|
setupModelNameEditing();
|
||||||
setupBaseModelEditing();
|
setupBaseModelEditing();
|
||||||
|
setupFileNameEditing();
|
||||||
|
|
||||||
// If we have a model ID but no description, fetch it
|
// If we have a model ID but no description, fetch it
|
||||||
if (lora.civitai?.modelId && !lora.modelDescription) {
|
if (lora.civitai?.modelId && !lora.modelDescription) {
|
||||||
@@ -1562,3 +1565,169 @@ function setupBaseModelEditing() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New function to handle file name editing
|
||||||
|
function setupFileNameEditing() {
|
||||||
|
const fileNameContent = document.querySelector('.file-name-content');
|
||||||
|
const editBtn = document.querySelector('.edit-file-name-btn');
|
||||||
|
|
||||||
|
if (!fileNameContent || !editBtn) return;
|
||||||
|
|
||||||
|
// Show edit button on hover
|
||||||
|
const fileNameWrapper = document.querySelector('.file-name-wrapper');
|
||||||
|
fileNameWrapper.addEventListener('mouseenter', () => {
|
||||||
|
editBtn.classList.add('visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
fileNameWrapper.addEventListener('mouseleave', () => {
|
||||||
|
if (!fileNameWrapper.classList.contains('editing')) {
|
||||||
|
editBtn.classList.remove('visible');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle edit button click
|
||||||
|
editBtn.addEventListener('click', () => {
|
||||||
|
fileNameWrapper.classList.add('editing');
|
||||||
|
fileNameContent.setAttribute('contenteditable', 'true');
|
||||||
|
fileNameContent.focus();
|
||||||
|
|
||||||
|
// Store original value for comparison later
|
||||||
|
fileNameContent.dataset.originalValue = fileNameContent.textContent.trim();
|
||||||
|
|
||||||
|
// Place cursor at the end
|
||||||
|
const range = document.createRange();
|
||||||
|
const sel = window.getSelection();
|
||||||
|
range.selectNodeContents(fileNameContent);
|
||||||
|
range.collapse(false);
|
||||||
|
sel.removeAllRanges();
|
||||||
|
sel.addRange(range);
|
||||||
|
|
||||||
|
editBtn.classList.add('visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle keyboard events in edit mode
|
||||||
|
fileNameContent.addEventListener('keydown', function(e) {
|
||||||
|
if (!this.getAttribute('contenteditable')) return;
|
||||||
|
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
this.blur(); // Trigger save on Enter
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
// Restore original value
|
||||||
|
this.textContent = this.dataset.originalValue;
|
||||||
|
exitEditMode();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle input validation
|
||||||
|
fileNameContent.addEventListener('input', function() {
|
||||||
|
if (!this.getAttribute('contenteditable')) return;
|
||||||
|
|
||||||
|
// Replace invalid characters for filenames
|
||||||
|
const invalidChars = /[\\/:*?"<>|]/g;
|
||||||
|
if (invalidChars.test(this.textContent)) {
|
||||||
|
const cursorPos = window.getSelection().getRangeAt(0).startOffset;
|
||||||
|
this.textContent = this.textContent.replace(invalidChars, '');
|
||||||
|
|
||||||
|
// Restore cursor position
|
||||||
|
const range = document.createRange();
|
||||||
|
const sel = window.getSelection();
|
||||||
|
const newPos = Math.min(cursorPos, this.textContent.length);
|
||||||
|
|
||||||
|
if (this.firstChild) {
|
||||||
|
range.setStart(this.firstChild, newPos);
|
||||||
|
range.collapse(true);
|
||||||
|
sel.removeAllRanges();
|
||||||
|
sel.addRange(range);
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast('Invalid characters removed from filename', 'warning');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle focus out - save changes
|
||||||
|
fileNameContent.addEventListener('blur', async function() {
|
||||||
|
if (!this.getAttribute('contenteditable')) return;
|
||||||
|
|
||||||
|
const newFileName = this.textContent.trim();
|
||||||
|
const originalValue = this.dataset.originalValue;
|
||||||
|
|
||||||
|
// Basic validation
|
||||||
|
if (!newFileName) {
|
||||||
|
// Restore original value if empty
|
||||||
|
this.textContent = originalValue;
|
||||||
|
showToast('File name cannot be empty', 'error');
|
||||||
|
exitEditMode();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newFileName === originalValue) {
|
||||||
|
// No changes, just exit edit mode
|
||||||
|
exitEditMode();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get the full file path
|
||||||
|
const filePath = document.querySelector('#loraModal .modal-content')
|
||||||
|
.querySelector('.file-path').textContent + originalValue + '.safetensors';
|
||||||
|
|
||||||
|
// Call API to rename the file
|
||||||
|
const response = await fetch('/api/rename_lora', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
file_path: filePath,
|
||||||
|
new_file_name: newFileName
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
showToast('File name updated successfully', 'success');
|
||||||
|
|
||||||
|
// Update card in the gallery
|
||||||
|
const loraCard = document.querySelector(`.lora-card[data-filepath="${filePath}"]`);
|
||||||
|
if (loraCard) {
|
||||||
|
// Update the card's filepath attribute to the new path
|
||||||
|
loraCard.dataset.filepath = result.new_file_path;
|
||||||
|
loraCard.dataset.file_name = newFileName;
|
||||||
|
|
||||||
|
// Update the filename display in the card
|
||||||
|
const cardFileName = loraCard.querySelector('.card-filename');
|
||||||
|
if (cardFileName) {
|
||||||
|
cardFileName.textContent = newFileName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle the case where we need to reload the page
|
||||||
|
if (result.reload_required) {
|
||||||
|
showToast('Reloading page to apply changes...', 'info');
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Show error and restore original filename
|
||||||
|
showToast(result.error || 'Failed to update file name', 'error');
|
||||||
|
this.textContent = originalValue;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving filename:', error);
|
||||||
|
showToast('Failed to update file name', 'error');
|
||||||
|
this.textContent = originalValue;
|
||||||
|
} finally {
|
||||||
|
exitEditMode();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function exitEditMode() {
|
||||||
|
fileNameContent.removeAttribute('contenteditable');
|
||||||
|
fileNameWrapper.classList.remove('editing');
|
||||||
|
editBtn.classList.remove('visible');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user