mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 15:15:44 -03:00
Add delete confirmation modal and update related styles and logic
This commit is contained in:
168
nodes.py
168
nodes.py
@@ -10,6 +10,7 @@ from .utils.file_utils import get_file_info, save_metadata, load_metadata, updat
|
|||||||
from .utils.lora_metadata import extract_lora_metadata
|
from .utils.lora_metadata import extract_lora_metadata
|
||||||
from typing import Dict, Optional
|
from typing import Dict, Optional
|
||||||
from .services.civitai_client import CivitaiClient
|
from .services.civitai_client import CivitaiClient
|
||||||
|
import folder_paths
|
||||||
|
|
||||||
class LorasEndpoint:
|
class LorasEndpoint:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -19,9 +20,12 @@ class LorasEndpoint:
|
|||||||
),
|
),
|
||||||
autoescape=True
|
autoescape=True
|
||||||
)
|
)
|
||||||
# 配置Loras根目录(根据实际安装位置调整)
|
# Configure Loras root directories (from ComfyUI folder paths settings)
|
||||||
self.loras_root = os.path.join(Path(__file__).parents[2], "models", "loras")
|
self.loras_roots = [path.replace(os.sep, "/") for path in folder_paths.get_folder_paths("loras") if os.path.exists(path)]
|
||||||
# 添加 server 属性
|
if not self.loras_roots:
|
||||||
|
raise ValueError("No valid loras folders found")
|
||||||
|
print(f"Loras roots: {self.loras_roots}") # debug log
|
||||||
|
|
||||||
self.server = PromptServer.instance
|
self.server = PromptServer.instance
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -29,9 +33,16 @@ class LorasEndpoint:
|
|||||||
instance = cls()
|
instance = cls()
|
||||||
app = PromptServer.instance.app
|
app = PromptServer.instance.app
|
||||||
static_path = os.path.join(os.path.dirname(__file__), 'static')
|
static_path = os.path.join(os.path.dirname(__file__), 'static')
|
||||||
|
|
||||||
|
# Generate multiple static paths based on the number of folders in instance.loras_roots
|
||||||
|
for idx, root in enumerate(instance.loras_roots, start=1):
|
||||||
|
# Create different static paths for each folder, like /loras_static/root1/preview
|
||||||
|
preview_path = f'/loras_static/root{idx}/preview'
|
||||||
|
app.add_routes([web.static(preview_path, root)])
|
||||||
|
|
||||||
app.add_routes([
|
app.add_routes([
|
||||||
web.get('/loras', instance.handle_loras_request),
|
web.get('/loras', instance.handle_loras_request),
|
||||||
web.static('/loras_static/previews', instance.loras_root),
|
# web.static('/loras_static/previews', instance.loras_root),
|
||||||
web.static('/loras_static', static_path),
|
web.static('/loras_static', static_path),
|
||||||
web.post('/api/delete_model', instance.delete_model),
|
web.post('/api/delete_model', instance.delete_model),
|
||||||
web.post('/api/fetch-civitai', instance.fetch_civitai),
|
web.post('/api/fetch-civitai', instance.fetch_civitai),
|
||||||
@@ -52,34 +63,35 @@ class LorasEndpoint:
|
|||||||
|
|
||||||
async def scan_loras(self):
|
async def scan_loras(self):
|
||||||
loras = []
|
loras = []
|
||||||
for root, _, files in os.walk(self.loras_root):
|
for loras_root in self.loras_roots:
|
||||||
safetensors_files = [f for f in files if f.endswith('.safetensors')]
|
for root, _, files in os.walk(loras_root):
|
||||||
total_files = len(safetensors_files)
|
safetensors_files = [f for f in files if f.endswith('.safetensors')]
|
||||||
|
total_files = len(safetensors_files)
|
||||||
for idx, filename in enumerate(safetensors_files, 1):
|
|
||||||
self.send_progress(idx, total_files, f"Scanning: {filename}")
|
|
||||||
|
|
||||||
file_path = os.path.join(root, filename)
|
for idx, filename in enumerate(safetensors_files, 1):
|
||||||
|
self.send_progress(idx, total_files, f"Scanning: {filename}")
|
||||||
# Try to load existing metadata first
|
|
||||||
metadata = await load_metadata(file_path, self.loras_root)
|
file_path = os.path.join(root, filename).replace(os.sep, "/")
|
||||||
|
|
||||||
if metadata is None:
|
# Try to load existing metadata first
|
||||||
# Only get file info and extract metadata if no existing metadata
|
metadata = await load_metadata(file_path)
|
||||||
metadata = await get_file_info(file_path, self.loras_root)
|
|
||||||
base_model_info = await extract_lora_metadata(file_path)
|
if metadata is None:
|
||||||
metadata.base_model = base_model_info['base_model']
|
# Only get file info and extract metadata if no existing metadata
|
||||||
await save_metadata(file_path, metadata)
|
metadata = await get_file_info(file_path)
|
||||||
|
base_model_info = await extract_lora_metadata(file_path)
|
||||||
# Convert to dict for API response
|
metadata.base_model = base_model_info['base_model']
|
||||||
lora_data = metadata.to_dict()
|
await save_metadata(file_path, metadata)
|
||||||
# Get relative path and remove filename to get just the folder structure
|
|
||||||
rel_path = os.path.relpath(file_path, self.loras_root)
|
# Convert to dict for API response
|
||||||
folder = os.path.dirname(rel_path)
|
lora_data = metadata.to_dict()
|
||||||
# Ensure forward slashes for consistency across platforms
|
# Get relative path and remove filename to get just the folder structure
|
||||||
lora_data['folder'] = folder.replace(os.path.sep, '/')
|
rel_path = os.path.relpath(file_path, loras_root)
|
||||||
|
folder = os.path.dirname(rel_path)
|
||||||
loras.append(lora_data)
|
# Ensure forward slashes for consistency across platforms
|
||||||
|
lora_data['folder'] = folder.replace(os.path.sep, '/')
|
||||||
|
|
||||||
|
loras.append(lora_data)
|
||||||
|
|
||||||
self.send_progress(total_files, total_files, "Scan completed")
|
self.send_progress(total_files, total_files, "Scan completed")
|
||||||
return loras
|
return loras
|
||||||
@@ -89,13 +101,6 @@ class LorasEndpoint:
|
|||||||
"""清理HTML格式的描述"""
|
"""清理HTML格式的描述"""
|
||||||
return desc.replace("<p>", "").replace("</p>", "\n").strip()
|
return desc.replace("<p>", "").replace("</p>", "\n").strip()
|
||||||
|
|
||||||
async def get_preview_url(self, preview_path, root_dir):
|
|
||||||
"""生成预览图URL"""
|
|
||||||
if os.path.exists(preview_path):
|
|
||||||
rel_path = os.path.relpath(preview_path, self.loras_root)
|
|
||||||
return f"/loras_static/previews/{rel_path.replace(os.sep, '/')}"
|
|
||||||
return "/loras_static/images/no-preview.png"
|
|
||||||
|
|
||||||
async def handle_loras_request(self, request):
|
async def handle_loras_request(self, request):
|
||||||
"""处理Loras请求并渲染模板"""
|
"""处理Loras请求并渲染模板"""
|
||||||
try:
|
try:
|
||||||
@@ -145,29 +150,43 @@ class LorasEndpoint:
|
|||||||
return {
|
return {
|
||||||
"model_name": lora["model_name"],
|
"model_name": lora["model_name"],
|
||||||
"file_name": lora["file_name"],
|
"file_name": lora["file_name"],
|
||||||
"preview_url": lora["preview_url"],
|
"preview_url": self.get_static_url_for_preview(lora["preview_url"]),
|
||||||
"base_model": lora["base_model"],
|
"base_model": lora["base_model"],
|
||||||
"folder": lora["folder"],
|
"folder": lora["folder"],
|
||||||
"sha256": lora["sha256"],
|
"sha256": lora["sha256"],
|
||||||
"file_path": lora["file_path"],
|
"file_path": lora["file_path"].replace(os.sep, "/"),
|
||||||
"modified": lora["modified"],
|
"modified": lora["modified"],
|
||||||
"from_civitai": lora.get("from_civitai", True),
|
"from_civitai": lora.get("from_civitai", True),
|
||||||
"civitai": self.filter_civitai_data(lora.get("civitai", {}))
|
"civitai": self.filter_civitai_data(lora.get("civitai", {}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def get_static_url_for_preview(self, preview_url):
|
||||||
|
"""
|
||||||
|
Determines which loras_root the preview_url belongs to and
|
||||||
|
returns the corresponding static URL.
|
||||||
|
"""
|
||||||
|
for idx, root in enumerate(self.loras_roots, start=1):
|
||||||
|
# Check if preview_url belongs to current root
|
||||||
|
if preview_url.startswith(root):
|
||||||
|
# Get relative path and generate static URL
|
||||||
|
relative_path = os.path.relpath(preview_url, root)
|
||||||
|
static_url = f'/loras_static/root{idx}/preview/{relative_path.replace(os.sep, "/")}'
|
||||||
|
return static_url
|
||||||
|
|
||||||
|
# If no matching root found, return empty string
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
async def delete_model(self, request):
|
async def delete_model(self, request):
|
||||||
try:
|
try:
|
||||||
data = await request.json()
|
data = await request.json()
|
||||||
file_name = data.get('file_name')
|
file_path = data.get('file_path') # 从请求中获取file_path信息
|
||||||
folder = data.get('folder') # 从请求中获取folder信息
|
if not file_path:
|
||||||
if not file_name:
|
return web.Response(text='Model full path is required', status=400)
|
||||||
return web.Response(text='Model name is required', status=400)
|
|
||||||
|
|
||||||
# 构建完整的目录路径
|
# 构建完整的目录路径
|
||||||
target_dir = self.loras_root
|
target_dir = os.path.dirname(file_path)
|
||||||
if folder and folder != "root":
|
file_name = os.path.splitext(os.path.basename(file_path))[0]
|
||||||
target_dir = os.path.join(self.loras_root, folder)
|
|
||||||
|
|
||||||
# List of file patterns to delete
|
# List of file patterns to delete
|
||||||
required_file = f"{file_name}.safetensors" # 主文件必须存在
|
required_file = f"{file_name}.safetensors" # 主文件必须存在
|
||||||
@@ -176,7 +195,13 @@ class LorasEndpoint:
|
|||||||
f"{file_name}.preview.png",
|
f"{file_name}.preview.png",
|
||||||
f"{file_name}.preview.jpg",
|
f"{file_name}.preview.jpg",
|
||||||
f"{file_name}.preview.jpeg",
|
f"{file_name}.preview.jpeg",
|
||||||
f"{file_name}.preview.webp"
|
f"{file_name}.preview.webp",
|
||||||
|
f"{file_name}.preview.mp4",
|
||||||
|
f"{file_name}.png",
|
||||||
|
f"{file_name}.jpg",
|
||||||
|
f"{file_name}.jpeg",
|
||||||
|
f"{file_name}.webp",
|
||||||
|
f"{file_name}.mp4"
|
||||||
]
|
]
|
||||||
|
|
||||||
deleted_files = []
|
deleted_files = []
|
||||||
@@ -201,7 +226,7 @@ class LorasEndpoint:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error deleting optional file {file_path}: {str(e)}")
|
print(f"Error deleting optional file {file_path}: {str(e)}")
|
||||||
else:
|
else:
|
||||||
return web.Response(text=f"Model file {required_file} not found in {folder}", status=404)
|
return web.Response(text=f"Model file {required_file} not found in {target_dir}", status=404)
|
||||||
|
|
||||||
return web.json_response({
|
return web.json_response({
|
||||||
'success': True,
|
'success': True,
|
||||||
@@ -268,7 +293,7 @@ class LorasEndpoint:
|
|||||||
|
|
||||||
# 4. 下载预览图
|
# 4. 下载预览图
|
||||||
# Check if existing preview is valid and the file exists
|
# Check if existing preview is valid and the file exists
|
||||||
if not local_metadata.get('preview_url') or not os.path.exists(os.path.join(self.loras_root, local_metadata['preview_url'].replace('/', os.sep))):
|
if not local_metadata.get('preview_url') or not os.path.exists(local_metadata['preview_url']):
|
||||||
first_preview = next((img for img in civitai_metadata.get('images', [])), None)
|
first_preview = next((img for img in civitai_metadata.get('images', [])), None)
|
||||||
if first_preview:
|
if first_preview:
|
||||||
|
|
||||||
@@ -277,7 +302,7 @@ class LorasEndpoint:
|
|||||||
preview_path = os.path.join(os.path.dirname(data['file_path']), preview_filename)
|
preview_path = os.path.join(os.path.dirname(data['file_path']), preview_filename)
|
||||||
await client.download_preview_image(first_preview['url'], preview_path)
|
await client.download_preview_image(first_preview['url'], preview_path)
|
||||||
# 存储相对路径,使用正斜杠格式
|
# 存储相对路径,使用正斜杠格式
|
||||||
local_metadata['preview_url'] = os.path.relpath(preview_path, self.loras_root).replace(os.sep, '/')
|
local_metadata['preview_url'] = preview_path.replace(os.sep, '/')
|
||||||
|
|
||||||
# 5. 保存更新后的元数据
|
# 5. 保存更新后的元数据
|
||||||
with open(metadata_path, 'w', encoding='utf-8') as f:
|
with open(metadata_path, 'w', encoding='utf-8') as f:
|
||||||
@@ -307,28 +332,22 @@ class LorasEndpoint:
|
|||||||
try:
|
try:
|
||||||
reader = await request.multipart()
|
reader = await request.multipart()
|
||||||
|
|
||||||
# Get the file field first
|
# Get the preview_file field first
|
||||||
file_field = await reader.next()
|
file_field = await reader.next()
|
||||||
if file_field.name != 'file':
|
if file_field.name != 'preview_file':
|
||||||
raise ValueError("Expected 'file' field first")
|
raise ValueError("Expected 'preview_file' field first")
|
||||||
file_data = await file_field.read()
|
preview_data = await file_field.read()
|
||||||
|
|
||||||
# Get the file name field
|
# Get the file model_path field
|
||||||
name_field = await reader.next()
|
name_field = await reader.next()
|
||||||
if name_field.name != 'file_name':
|
if name_field.name != 'model_path':
|
||||||
raise ValueError("Expected 'file_name' field second")
|
raise ValueError("Expected 'model_path' field second")
|
||||||
file_name = (await name_field.read()).decode()
|
model_path = (await name_field.read()).decode()
|
||||||
|
|
||||||
# Get the folder field
|
|
||||||
folder_field = await reader.next()
|
|
||||||
if folder_field.name != 'folder':
|
|
||||||
raise ValueError("Expected 'folder' field third")
|
|
||||||
folder = (await folder_field.read()).decode()
|
|
||||||
|
|
||||||
# Get the content type from the file field headers
|
# Get the content type from the file field headers
|
||||||
content_type = file_field.headers.get('Content-Type', '')
|
content_type = file_field.headers.get('Content-Type', '')
|
||||||
|
|
||||||
print(f"Received preview file: {file_name} ({content_type})") # Debug log
|
print(f"Received preview file: {model_path} ({content_type})") # Debug log
|
||||||
|
|
||||||
# Determine file extension based on content type
|
# Determine file extension based on content type
|
||||||
if content_type.startswith('video/'):
|
if content_type.startswith('video/'):
|
||||||
@@ -337,29 +356,34 @@ class LorasEndpoint:
|
|||||||
extension = '.preview.png'
|
extension = '.preview.png'
|
||||||
|
|
||||||
# Construct the preview file path
|
# Construct the preview file path
|
||||||
base_name = os.path.splitext(file_name)[0] # Remove original extension
|
base_name = os.path.splitext(os.path.basename(model_path))[0] # Remove original extension
|
||||||
preview_name = base_name + extension
|
preview_name = base_name + extension
|
||||||
preview_path = os.path.join(self.loras_root, folder, preview_name)
|
# Get the folder path from the model_path
|
||||||
|
folder = os.path.dirname(model_path)
|
||||||
|
preview_path = os.path.join(folder, preview_name).replace(os.sep, '/')
|
||||||
|
|
||||||
# Save the preview file
|
# Save the preview file
|
||||||
with open(preview_path, 'wb') as f:
|
with open(preview_path, 'wb') as f:
|
||||||
f.write(file_data)
|
f.write(preview_data)
|
||||||
|
|
||||||
# Update metadata if it exists
|
# Update metadata if it exists
|
||||||
metadata_path = os.path.join(self.loras_root, folder, base_name + '.metadata.json')
|
metadata_path = os.path.join(folder, base_name + '.metadata.json')
|
||||||
if os.path.exists(metadata_path):
|
if os.path.exists(metadata_path):
|
||||||
try:
|
try:
|
||||||
with open(metadata_path, 'r', encoding='utf-8') as f:
|
with open(metadata_path, 'r', encoding='utf-8') as f:
|
||||||
metadata = json.load(f)
|
metadata = json.load(f)
|
||||||
# Update the preview_url to match the new file name
|
# Update the preview_url to match the new file name
|
||||||
metadata['preview_url'] = os.path.join(folder, preview_name).replace(os.sep, '/')
|
metadata['preview_url'] = preview_path
|
||||||
with open(metadata_path, 'w', encoding='utf-8') as f:
|
with open(metadata_path, 'w', encoding='utf-8') as f:
|
||||||
json.dump(metadata, f, indent=2, ensure_ascii=False)
|
json.dump(metadata, f, indent=2, ensure_ascii=False)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error updating metadata: {str(e)}")
|
print(f"Error updating metadata: {str(e)}")
|
||||||
# Continue even if metadata update fails
|
# Continue even if metadata update fails
|
||||||
|
|
||||||
return web.Response(status=200)
|
return web.json_response({
|
||||||
|
"success": True,
|
||||||
|
"preview_url": self.get_static_url_for_preview(preview_path)
|
||||||
|
})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error replacing preview: {str(e)}")
|
print(f"Error replacing preview: {str(e)}")
|
||||||
|
|||||||
@@ -302,6 +302,114 @@ body.modal-open {
|
|||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Delete Modal specific styles */
|
||||||
|
.delete-modal-content {
|
||||||
|
max-width: 500px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-message {
|
||||||
|
color: var(--text-color);
|
||||||
|
margin: var(--space-2) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-model-info {
|
||||||
|
background: var(--lora-surface);
|
||||||
|
border: 1px solid var(--lora-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: var(--space-2);
|
||||||
|
margin: var(--space-2) 0;
|
||||||
|
color: var(--text-color);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Update delete modal styles */
|
||||||
|
.delete-modal {
|
||||||
|
display: none; /* Set initial display to none */
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
z-index: var(--z-overlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add new style for when modal is shown */
|
||||||
|
.delete-modal.show {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-modal-content {
|
||||||
|
max-width: 500px;
|
||||||
|
width: 90%;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0 auto;
|
||||||
|
position: relative;
|
||||||
|
animation: modalFadeIn 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-model-info {
|
||||||
|
/* Update info display styling */
|
||||||
|
background: var(--lora-surface);
|
||||||
|
border: 1px solid var(--lora-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: var(--space-2);
|
||||||
|
margin: var(--space-2) 0;
|
||||||
|
color: var(--text-color);
|
||||||
|
word-break: break-all;
|
||||||
|
text-align: left;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modalFadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-2);
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn, .delete-btn {
|
||||||
|
padding: 8px var(--space-2);
|
||||||
|
border-radius: 6px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn {
|
||||||
|
background: var(--lora-surface);
|
||||||
|
border: 1px solid var(--lora-border);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn {
|
||||||
|
background: var(--lora-error);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn:hover {
|
||||||
|
background: var(--lora-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
.carousel {
|
.carousel {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-auto-flow: column;
|
grid-auto-flow: column;
|
||||||
|
|||||||
@@ -96,45 +96,78 @@ function openCivitai(modelName) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteModel(fileName) {
|
let pendingDeletePath = null;
|
||||||
// Prevent event bubbling
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
// Get the folder from the card's data attributes
|
|
||||||
const card = document.querySelector(`.lora-card[data-file_name="${fileName}"]`);
|
|
||||||
const folder = card ? card.dataset.folder : null;
|
|
||||||
|
|
||||||
// Show confirmation dialog
|
|
||||||
const confirmed = confirm(`Are you sure you want to delete "${fileName}" and all associated files?`);
|
|
||||||
|
|
||||||
if (confirmed) {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/delete_model', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
file_name: fileName,
|
|
||||||
folder: folder
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
function showDeleteModal(filePath) {
|
||||||
// Remove the card from UI
|
event.stopPropagation();
|
||||||
if (card) {
|
pendingDeletePath = filePath;
|
||||||
card.remove();
|
|
||||||
}
|
const card = document.querySelector(`.lora-card[data-filepath="${filePath}"]`);
|
||||||
} else {
|
const modelName = card.dataset.name;
|
||||||
const error = await response.text();
|
const modal = document.getElementById('deleteModal');
|
||||||
alert(`Failed to delete model: ${error}`);
|
const modelInfo = modal.querySelector('.delete-model-info');
|
||||||
}
|
|
||||||
} catch (error) {
|
// Format the info with better structure
|
||||||
alert(`Error deleting model: ${error}`);
|
modelInfo.innerHTML = `
|
||||||
|
<strong>Model:</strong> ${modelName}
|
||||||
|
<br>
|
||||||
|
<strong>File:</strong> ${filePath}
|
||||||
|
`;
|
||||||
|
|
||||||
|
modal.classList.add('show'); // Use class instead of style.display
|
||||||
|
document.body.classList.add('modal-open');
|
||||||
|
|
||||||
|
// Add click outside to close
|
||||||
|
modal.onclick = function(event) {
|
||||||
|
if (event.target === modal) {
|
||||||
|
closeDeleteModal();
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDeleteModal() {
|
||||||
|
const modal = document.getElementById('deleteModal');
|
||||||
|
modal.classList.remove('show'); // Use class instead of style.display
|
||||||
|
document.body.classList.remove('modal-open');
|
||||||
|
pendingDeletePath = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmDelete() {
|
||||||
|
if (!pendingDeletePath) return;
|
||||||
|
|
||||||
|
const modal = document.getElementById('deleteModal');
|
||||||
|
const card = document.querySelector(`.lora-card[data-filepath="${pendingDeletePath}"]`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/delete_model', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
file_path: pendingDeletePath
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
if (card) {
|
||||||
|
card.remove();
|
||||||
|
}
|
||||||
|
closeDeleteModal();
|
||||||
|
} else {
|
||||||
|
const error = await response.text();
|
||||||
|
alert(`Failed to delete model: ${error}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert(`Error deleting model: ${error}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Replace the existing deleteModel function with this one
|
||||||
|
async function deleteModel(filePath) {
|
||||||
|
showDeleteModal(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
// 初始化排序
|
// 初始化排序
|
||||||
document.getElementById('sortSelect')?.addEventListener('change', (e) => {
|
document.getElementById('sortSelect')?.addEventListener('change', (e) => {
|
||||||
sortCards(e.target.value);
|
sortCards(e.target.value);
|
||||||
@@ -381,7 +414,7 @@ async function fetchCivitai() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function replacePreview(fileName, folder) {
|
async function replacePreview(filePath) {
|
||||||
// Get loading elements first
|
// Get loading elements first
|
||||||
const loadingOverlay = document.getElementById('loading-overlay');
|
const loadingOverlay = document.getElementById('loading-overlay');
|
||||||
const loadingStatus = document.querySelector('.loading-status');
|
const loadingStatus = document.querySelector('.loading-status');
|
||||||
@@ -397,9 +430,8 @@ async function replacePreview(fileName, folder) {
|
|||||||
|
|
||||||
const file = input.files[0];
|
const file = input.files[0];
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('preview_file', file);
|
||||||
formData.append('file_name', fileName);
|
formData.append('model_path', filePath);
|
||||||
formData.append('folder', folder);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Show loading overlay
|
// Show loading overlay
|
||||||
@@ -414,18 +446,15 @@ async function replacePreview(fileName, folder) {
|
|||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Upload failed');
|
throw new Error('Upload failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const newPreviewPath = `${data.preview_url}?t=${new Date().getTime()}`;
|
||||||
|
|
||||||
// Update the preview image in the card
|
// Update the preview image in the card
|
||||||
const card = document.querySelector(`.lora-card[data-file_name="${fileName}"]`);
|
const card = document.querySelector(`.lora-card[data-filepath="${filePath}"]`);
|
||||||
const previewContainer = card.querySelector('.card-preview');
|
const previewContainer = card.querySelector('.card-preview');
|
||||||
const oldPreview = previewContainer.querySelector('img, video');
|
const oldPreview = previewContainer.querySelector('img, video');
|
||||||
|
|
||||||
// Force reload the preview by adding a timestamp
|
|
||||||
const timestamp = new Date().getTime();
|
|
||||||
const baseName = fileName.split('.')[0];
|
|
||||||
const extension = file.type.startsWith('video/') ? '.preview.mp4' : '.preview.png';
|
|
||||||
const newPreviewPath = `/loras_static/previews/${folder}/${baseName}${extension}?t=${timestamp}`;
|
|
||||||
|
|
||||||
// Create new preview element based on file type
|
// Create new preview element based on file type
|
||||||
if (file.type.startsWith('video/')) {
|
if (file.type.startsWith('video/')) {
|
||||||
const video = document.createElement('video');
|
const video = document.createElement('video');
|
||||||
|
|||||||
@@ -27,6 +27,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Delete Confirmation Modal -->
|
||||||
|
<div id="deleteModal" class="modal delete-modal">
|
||||||
|
<div class="modal-content delete-modal-content">
|
||||||
|
<h2>Delete Model</h2>
|
||||||
|
<p class="delete-message">Are you sure you want to delete this model and all associated files?</p>
|
||||||
|
<div class="delete-model-info"></div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="cancel-btn" onclick="closeDeleteModal()">Cancel</button>
|
||||||
|
<button class="delete-btn" onclick="confirmDelete()">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<!-- 控制栏 -->
|
<!-- 控制栏 -->
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
@@ -61,11 +74,11 @@
|
|||||||
<div class="card-preview">
|
<div class="card-preview">
|
||||||
{% if lora.preview_url.endswith('.mp4') or lora.preview_url.endswith('.webm') %}
|
{% if lora.preview_url.endswith('.mp4') or lora.preview_url.endswith('.webm') %}
|
||||||
<video controls autoplay muted loop>
|
<video controls autoplay muted loop>
|
||||||
<source src="{{ '/loras_static/previews/' + lora.preview_url }}" type="video/mp4">
|
<source src="{{ lora.preview_url }}" type="video/mp4">
|
||||||
Your browser does not support the video tag.
|
Your browser does not support the video tag.
|
||||||
</video>
|
</video>
|
||||||
{% else %}
|
{% else %}
|
||||||
<img src="{{ ('/loras_static/previews/' + lora.preview_url) if lora.preview_url else '/loras_static/images/no-preview.png' }}" alt="{{ lora.name }}">
|
<img src="{{ lora.preview_url if lora.preview_url else '/loras_static/images/no-preview.png' }}" alt="{{ lora.name }}">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span class="base-model-label" title="{{ lora.base_model }}">
|
<span class="base-model-label" title="{{ lora.base_model }}">
|
||||||
@@ -81,7 +94,7 @@
|
|||||||
onclick="event.stopPropagation(); navigator.clipboard.writeText(this.closest('.lora-card').dataset.file_name)"></i>
|
onclick="event.stopPropagation(); navigator.clipboard.writeText(this.closest('.lora-card').dataset.file_name)"></i>
|
||||||
<i class="fas fa-trash"
|
<i class="fas fa-trash"
|
||||||
title="Delete Model"
|
title="Delete Model"
|
||||||
onclick="event.stopPropagation(); deleteModel('{{ lora.file_name }}')"></i>
|
onclick="event.stopPropagation(); deleteModel('{{ lora.file_path }}')"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer">
|
<div class="card-footer">
|
||||||
@@ -91,7 +104,7 @@
|
|||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<i class="fas fa-image"
|
<i class="fas fa-image"
|
||||||
title="Replace Preview Image"
|
title="Replace Preview Image"
|
||||||
onclick="event.stopPropagation(); replacePreview('{{ lora.file_name }}', '{{ lora.folder }}')"></i>
|
onclick="event.stopPropagation(); replacePreview('{{ lora.file_path }}')"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ async def calculate_sha256(file_path: str) -> str:
|
|||||||
sha256_hash.update(byte_block)
|
sha256_hash.update(byte_block)
|
||||||
return sha256_hash.hexdigest()
|
return sha256_hash.hexdigest()
|
||||||
|
|
||||||
def _find_preview_file(base_name: str, dir_path: str, lora_root: str) -> str:
|
def _find_preview_file(base_name: str, dir_path: str) -> str:
|
||||||
"""Find preview file for given base name in directory"""
|
"""Find preview file for given base name in directory"""
|
||||||
preview_patterns = [
|
preview_patterns = [
|
||||||
f"{base_name}.preview.png",
|
f"{base_name}.preview.png",
|
||||||
@@ -28,15 +28,15 @@ def _find_preview_file(base_name: str, dir_path: str, lora_root: str) -> str:
|
|||||||
for pattern in preview_patterns:
|
for pattern in preview_patterns:
|
||||||
full_pattern = os.path.join(dir_path, pattern)
|
full_pattern = os.path.join(dir_path, pattern)
|
||||||
if os.path.exists(full_pattern):
|
if os.path.exists(full_pattern):
|
||||||
return os.path.relpath(full_pattern, lora_root).replace("\\", "/")
|
return full_pattern.replace("\\", "/")
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
async def get_file_info(file_path: str, loras_root: str) -> LoraMetadata:
|
async def get_file_info(file_path: str) -> LoraMetadata:
|
||||||
"""Get basic file information as LoraMetadata object"""
|
"""Get basic file information as LoraMetadata object"""
|
||||||
base_name = os.path.splitext(os.path.basename(file_path))[0]
|
base_name = os.path.splitext(os.path.basename(file_path))[0]
|
||||||
dir_path = os.path.dirname(file_path)
|
dir_path = os.path.dirname(file_path)
|
||||||
|
|
||||||
preview_path = _find_preview_file(base_name, dir_path, loras_root)
|
preview_url = _find_preview_file(base_name, dir_path)
|
||||||
|
|
||||||
return LoraMetadata(
|
return LoraMetadata(
|
||||||
file_name=base_name,
|
file_name=base_name,
|
||||||
@@ -47,7 +47,7 @@ async def get_file_info(file_path: str, loras_root: str) -> LoraMetadata:
|
|||||||
sha256=await calculate_sha256(file_path),
|
sha256=await calculate_sha256(file_path),
|
||||||
base_model="Unknown", # Will be updated later
|
base_model="Unknown", # Will be updated later
|
||||||
from_civitai=True,
|
from_civitai=True,
|
||||||
preview_url=preview_path,
|
preview_url=preview_url,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def save_metadata(file_path: str, metadata: LoraMetadata) -> None:
|
async def save_metadata(file_path: str, metadata: LoraMetadata) -> None:
|
||||||
@@ -59,7 +59,7 @@ async def save_metadata(file_path: str, metadata: LoraMetadata) -> None:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error saving metadata to {metadata_path}: {str(e)}")
|
print(f"Error saving metadata to {metadata_path}: {str(e)}")
|
||||||
|
|
||||||
async def load_metadata(file_path: str, loras_root: str) -> Optional[LoraMetadata]:
|
async def load_metadata(file_path: str) -> Optional[LoraMetadata]:
|
||||||
"""Load metadata from .metadata.json file"""
|
"""Load metadata from .metadata.json file"""
|
||||||
metadata_path = f"{os.path.splitext(file_path)[0]}.metadata.json"
|
metadata_path = f"{os.path.splitext(file_path)[0]}.metadata.json"
|
||||||
try:
|
try:
|
||||||
@@ -67,11 +67,11 @@ async def load_metadata(file_path: str, loras_root: str) -> Optional[LoraMetadat
|
|||||||
with open(metadata_path, 'r', encoding='utf-8') as f:
|
with open(metadata_path, 'r', encoding='utf-8') as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
preview_url = data.get('preview_url', '')
|
preview_url = data.get('preview_url', '')
|
||||||
preview_path = os.path.join(loras_root, preview_url) if preview_url else ''
|
# preview_path = os.path.join(loras_root, preview_url) if preview_url else ''
|
||||||
if not preview_url or not os.path.exists(preview_path):
|
if not preview_url or not os.path.exists(preview_url):
|
||||||
base_name = os.path.splitext(os.path.basename(file_path))[0]
|
base_name = os.path.splitext(os.path.basename(file_path))[0]
|
||||||
dir_path = os.path.dirname(file_path)
|
dir_path = os.path.dirname(file_path)
|
||||||
data['preview_url'] = _find_preview_file(base_name, dir_path, loras_root)
|
data['preview_url'] = _find_preview_file(base_name, dir_path)
|
||||||
if data['preview_url']:
|
if data['preview_url']:
|
||||||
with open(metadata_path, 'w', encoding='utf-8') as f:
|
with open(metadata_path, 'w', encoding='utf-8') as f:
|
||||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||||
|
|||||||
Reference in New Issue
Block a user