mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 07:05:43 -03:00
11
README.md
11
README.md
@@ -283,7 +283,16 @@ If you find this project helpful, consider supporting its development:
|
|||||||
|
|
||||||
[](https://ko-fi.com/pixelpawsai)
|
[](https://ko-fi.com/pixelpawsai)
|
||||||
|
|
||||||
WeChat & Alipay: [Click to view QR codes](https://raw.githubusercontent.com/willmiao/ComfyUI-Lora-Manager/main/static/images/combined-qr.webp)
|
<div align="left">
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<p>WeChat:</p>
|
||||||
|
<img src="https://github.com/willmiao/ComfyUI-Lora-Manager/blob/main/static/images/wechat-qr.webp" alt="WeChat QR" width="200" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
## 💬 Community
|
## 💬 Community
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ class ApiRoutes:
|
|||||||
app.on_startup.append(lambda _: routes.initialize_services())
|
app.on_startup.append(lambda _: routes.initialize_services())
|
||||||
|
|
||||||
app.router.add_post('/api/delete_model', routes.delete_model)
|
app.router.add_post('/api/delete_model', routes.delete_model)
|
||||||
|
app.router.add_post('/api/loras/exclude', routes.exclude_model) # Add new exclude endpoint
|
||||||
app.router.add_post('/api/fetch-civitai', routes.fetch_civitai)
|
app.router.add_post('/api/fetch-civitai', routes.fetch_civitai)
|
||||||
app.router.add_post('/api/replace_preview', routes.replace_preview)
|
app.router.add_post('/api/replace_preview', routes.replace_preview)
|
||||||
app.router.add_get('/api/loras', routes.get_loras)
|
app.router.add_get('/api/loras', routes.get_loras)
|
||||||
@@ -81,6 +82,12 @@ class ApiRoutes:
|
|||||||
self.scanner = await ServiceRegistry.get_lora_scanner()
|
self.scanner = await ServiceRegistry.get_lora_scanner()
|
||||||
return await ModelRouteUtils.handle_delete_model(request, self.scanner)
|
return await ModelRouteUtils.handle_delete_model(request, self.scanner)
|
||||||
|
|
||||||
|
async def exclude_model(self, request: web.Request) -> web.Response:
|
||||||
|
"""Handle model exclusion request"""
|
||||||
|
if self.scanner is None:
|
||||||
|
self.scanner = await ServiceRegistry.get_lora_scanner()
|
||||||
|
return await ModelRouteUtils.handle_exclude_model(request, self.scanner)
|
||||||
|
|
||||||
async def fetch_civitai(self, request: web.Request) -> web.Response:
|
async def fetch_civitai(self, request: web.Request) -> web.Response:
|
||||||
"""Handle CivitAI metadata fetch request"""
|
"""Handle CivitAI metadata fetch request"""
|
||||||
if self.scanner is None:
|
if self.scanner is None:
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ class CheckpointsRoutes:
|
|||||||
|
|
||||||
# Add new routes for model management similar to LoRA routes
|
# Add new routes for model management similar to LoRA routes
|
||||||
app.router.add_post('/api/checkpoints/delete', self.delete_model)
|
app.router.add_post('/api/checkpoints/delete', self.delete_model)
|
||||||
|
app.router.add_post('/api/checkpoints/exclude', self.exclude_model) # Add new exclude endpoint
|
||||||
app.router.add_post('/api/checkpoints/fetch-civitai', self.fetch_civitai)
|
app.router.add_post('/api/checkpoints/fetch-civitai', self.fetch_civitai)
|
||||||
app.router.add_post('/api/checkpoints/replace-preview', self.replace_preview)
|
app.router.add_post('/api/checkpoints/replace-preview', self.replace_preview)
|
||||||
app.router.add_post('/api/checkpoints/download', self.download_checkpoint)
|
app.router.add_post('/api/checkpoints/download', self.download_checkpoint)
|
||||||
@@ -500,6 +501,10 @@ class CheckpointsRoutes:
|
|||||||
"""Handle checkpoint model deletion request"""
|
"""Handle checkpoint model deletion request"""
|
||||||
return await ModelRouteUtils.handle_delete_model(request, self.scanner)
|
return await ModelRouteUtils.handle_delete_model(request, self.scanner)
|
||||||
|
|
||||||
|
async def exclude_model(self, request: web.Request) -> web.Response:
|
||||||
|
"""Handle checkpoint model exclusion request"""
|
||||||
|
return await ModelRouteUtils.handle_exclude_model(request, self.scanner)
|
||||||
|
|
||||||
async def fetch_civitai(self, request: web.Request) -> web.Response:
|
async def fetch_civitai(self, request: web.Request) -> web.Response:
|
||||||
"""Handle CivitAI metadata fetch request for checkpoints"""
|
"""Handle CivitAI metadata fetch request for checkpoints"""
|
||||||
return await ModelRouteUtils.handle_fetch_civitai(request, self.scanner)
|
return await ModelRouteUtils.handle_fetch_civitai(request, self.scanner)
|
||||||
@@ -653,7 +658,7 @@ class CheckpointsRoutes:
|
|||||||
model_type = response.get('type', '')
|
model_type = response.get('type', '')
|
||||||
|
|
||||||
# Check model type - should be Checkpoint
|
# Check model type - should be Checkpoint
|
||||||
if model_type.lower() != 'checkpoint':
|
if (model_type.lower() != 'checkpoint'):
|
||||||
return web.json_response({
|
return web.json_response({
|
||||||
'error': f"Model type mismatch. Expected Checkpoint, got {model_type}"
|
'error': f"Model type mismatch. Expected Checkpoint, got {model_type}"
|
||||||
}, status=400)
|
}, status=400)
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ class ModelScanner:
|
|||||||
self._hash_index = hash_index or ModelHashIndex()
|
self._hash_index = hash_index or ModelHashIndex()
|
||||||
self._tags_count = {} # Dictionary to store tag counts
|
self._tags_count = {} # Dictionary to store tag counts
|
||||||
self._is_initializing = False # Flag to track initialization state
|
self._is_initializing = False # Flag to track initialization state
|
||||||
|
self._excluded_models = [] # List to track excluded models
|
||||||
|
|
||||||
# Register this service
|
# Register this service
|
||||||
asyncio.create_task(self._register_service())
|
asyncio.create_task(self._register_service())
|
||||||
@@ -395,6 +396,9 @@ class ModelScanner:
|
|||||||
found_paths.add(file_path)
|
found_paths.add(file_path)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if file_path in self._excluded_models:
|
||||||
|
continue
|
||||||
|
|
||||||
# Try case-insensitive match on Windows
|
# Try case-insensitive match on Windows
|
||||||
if os.name == 'nt':
|
if os.name == 'nt':
|
||||||
lower_path = file_path.lower()
|
lower_path = file_path.lower()
|
||||||
@@ -586,6 +590,11 @@ class ModelScanner:
|
|||||||
|
|
||||||
model_data = metadata.to_dict()
|
model_data = metadata.to_dict()
|
||||||
|
|
||||||
|
# Skip excluded models
|
||||||
|
if model_data.get('exclude', False):
|
||||||
|
self._excluded_models.append(model_data['file_path'])
|
||||||
|
return None
|
||||||
|
|
||||||
await self._fetch_missing_metadata(file_path, model_data)
|
await self._fetch_missing_metadata(file_path, model_data)
|
||||||
rel_path = os.path.relpath(file_path, root_path)
|
rel_path = os.path.relpath(file_path, root_path)
|
||||||
folder = os.path.dirname(rel_path)
|
folder = os.path.dirname(rel_path)
|
||||||
@@ -905,6 +914,10 @@ class ModelScanner:
|
|||||||
logger.error(f"Error getting model info by name: {e}", exc_info=True)
|
logger.error(f"Error getting model info by name: {e}", exc_info=True)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_excluded_models(self) -> List[str]:
|
||||||
|
"""Get list of excluded model file paths"""
|
||||||
|
return self._excluded_models.copy()
|
||||||
|
|
||||||
async def update_preview_in_cache(self, file_path: str, preview_url: str) -> bool:
|
async def update_preview_in_cache(self, file_path: str, preview_url: str) -> bool:
|
||||||
"""Update preview URL in cache for a specific lora
|
"""Update preview URL in cache for a specific lora
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class BaseModelMetadata:
|
|||||||
modelDescription: str = "" # Full model description
|
modelDescription: str = "" # Full model description
|
||||||
civitai_deleted: bool = False # Whether deleted from Civitai
|
civitai_deleted: bool = False # Whether deleted from Civitai
|
||||||
favorite: bool = False # Whether the model is a favorite
|
favorite: bool = False # Whether the model is a favorite
|
||||||
|
exclude: bool = False # Whether to exclude this model from the cache
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
# Initialize empty lists to avoid mutable default parameter issue
|
# Initialize empty lists to avoid mutable default parameter issue
|
||||||
|
|||||||
@@ -425,6 +425,65 @@ class ModelRouteUtils:
|
|||||||
logger.error(f"Error replacing preview: {e}", exc_info=True)
|
logger.error(f"Error replacing preview: {e}", exc_info=True)
|
||||||
return web.Response(text=str(e), status=500)
|
return web.Response(text=str(e), status=500)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def handle_exclude_model(request: web.Request, scanner) -> web.Response:
|
||||||
|
"""Handle model exclusion request
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: The aiohttp request
|
||||||
|
scanner: The model scanner instance with cache management methods
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
web.Response: The HTTP response
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = await request.json()
|
||||||
|
file_path = data.get('file_path')
|
||||||
|
if not file_path:
|
||||||
|
return web.Response(text='Model path is required', status=400)
|
||||||
|
|
||||||
|
# Update metadata to mark as excluded
|
||||||
|
metadata_path = os.path.splitext(file_path)[0] + '.metadata.json'
|
||||||
|
metadata = await ModelRouteUtils.load_local_metadata(metadata_path)
|
||||||
|
metadata['exclude'] = True
|
||||||
|
|
||||||
|
# Save updated metadata
|
||||||
|
with open(metadata_path, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(metadata, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
# Update cache
|
||||||
|
cache = await scanner.get_cached_data()
|
||||||
|
|
||||||
|
# Find and remove model from cache
|
||||||
|
model_to_remove = next((item for item in cache.raw_data if item['file_path'] == file_path), None)
|
||||||
|
if model_to_remove:
|
||||||
|
# Update tags count
|
||||||
|
for tag in model_to_remove.get('tags', []):
|
||||||
|
if tag in scanner._tags_count:
|
||||||
|
scanner._tags_count[tag] = max(0, scanner._tags_count[tag] - 1)
|
||||||
|
if scanner._tags_count[tag] == 0:
|
||||||
|
del scanner._tags_count[tag]
|
||||||
|
|
||||||
|
# Remove from hash index if available
|
||||||
|
if hasattr(scanner, '_hash_index') and scanner._hash_index:
|
||||||
|
scanner._hash_index.remove_by_path(file_path)
|
||||||
|
|
||||||
|
# Remove from cache data
|
||||||
|
cache.raw_data = [item for item in cache.raw_data if item['file_path'] != file_path]
|
||||||
|
await cache.resort()
|
||||||
|
|
||||||
|
# Add to excluded models list
|
||||||
|
scanner._excluded_models.append(file_path)
|
||||||
|
|
||||||
|
return web.json_response({
|
||||||
|
'success': True,
|
||||||
|
'message': f"Model {os.path.basename(file_path)} excluded"
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error excluding model: {e}", exc_info=True)
|
||||||
|
return web.Response(text=str(e), status=500)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def handle_download_model(request: web.Request, download_manager: DownloadManager, model_type="lora") -> web.Response:
|
async def handle_download_model(request: web.Request, download_manager: DownloadManager, model_type="lora") -> web.Response:
|
||||||
"""Handle model download request
|
"""Handle model download request
|
||||||
|
|||||||
@@ -44,26 +44,12 @@ body.modal-open {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Delete Modal specific styles */
|
/* Delete Modal specific styles */
|
||||||
.delete-modal-content {
|
|
||||||
max-width: 500px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.delete-message {
|
.delete-message {
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
margin: var(--space-2) 0;
|
margin: var(--space-2) 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.delete-model-info {
|
|
||||||
background: var(--lora-surface);
|
|
||||||
border: 1px solid var(--lora-border);
|
|
||||||
border-radius: var(--border-radius-sm);
|
|
||||||
padding: var(--space-2);
|
|
||||||
margin: var(--space-2) 0;
|
|
||||||
color: var(--text-color);
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Update delete modal styles */
|
/* Update delete modal styles */
|
||||||
.delete-modal {
|
.delete-modal {
|
||||||
display: none; /* Set initial display to none */
|
display: none; /* Set initial display to none */
|
||||||
@@ -92,7 +78,8 @@ body.modal-open {
|
|||||||
animation: modalFadeIn 0.2s ease-out;
|
animation: modalFadeIn 0.2s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.delete-model-info {
|
.delete-model-info,
|
||||||
|
.exclude-model-info {
|
||||||
/* Update info display styling */
|
/* Update info display styling */
|
||||||
background: var(--lora-surface);
|
background: var(--lora-surface);
|
||||||
border: 1px solid var(--lora-border);
|
border: 1px solid var(--lora-border);
|
||||||
@@ -123,7 +110,7 @@ body.modal-open {
|
|||||||
margin-top: var(--space-3);
|
margin-top: var(--space-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cancel-btn, .delete-btn {
|
.cancel-btn, .delete-btn, .exclude-btn {
|
||||||
padding: 8px var(--space-2);
|
padding: 8px var(--space-2);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -143,6 +130,12 @@ body.modal-open {
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Style for exclude button - different from delete button */
|
||||||
|
.exclude-btn {
|
||||||
|
background: var(--lora-accent, #4f46e5);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
.cancel-btn:hover {
|
.cancel-btn:hover {
|
||||||
background: var(--lora-border);
|
background: var(--lora-border);
|
||||||
}
|
}
|
||||||
@@ -151,6 +144,11 @@ body.modal-open {
|
|||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.exclude-btn:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
background: oklch(from var(--lora-accent, #4f46e5) l c h / 85%);
|
||||||
|
}
|
||||||
|
|
||||||
.modal-content h2 {
|
.modal-content h2 {
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
margin-bottom: var(--space-2);
|
margin-bottom: var(--space-2);
|
||||||
@@ -587,7 +585,7 @@ input:checked + .toggle-slider:before {
|
|||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
background-color: var(--lora-surface);
|
background-color: var(--lora-surface);
|
||||||
color: var(--text-color);
|
color: var (--text-color);
|
||||||
font-size: 0.95em;
|
font-size: 0.95em;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,11 +151,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.qrcode-image {
|
.qrcode-image {
|
||||||
max-width: 100%;
|
max-width: 80%;
|
||||||
height: auto;
|
height: auto;
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
border: 1px solid var(--lora-border);
|
border: 1px solid var(--lora-border);
|
||||||
|
aspect-ratio: 1/1; /* Ensure proper aspect ratio for the square QR code */
|
||||||
}
|
}
|
||||||
|
|
||||||
.support-footer {
|
.support-footer {
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 225 KiB |
BIN
static/images/wechat-qr.webp
Normal file
BIN
static/images/wechat-qr.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 98 KiB |
@@ -208,13 +208,44 @@ export function replaceModelPreview(filePath, modelType = 'lora') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Delete a model (generic)
|
// Delete a model (generic)
|
||||||
export function deleteModel(filePath, modelType = 'lora') {
|
export async function deleteModel(filePath, modelType = 'lora') {
|
||||||
if (modelType === 'checkpoint') {
|
try {
|
||||||
confirmDelete('Are you sure you want to delete this checkpoint?', () => {
|
const endpoint = modelType === 'checkpoint'
|
||||||
performDelete(filePath, modelType);
|
? '/api/checkpoints/delete'
|
||||||
|
: '/api/delete_model';
|
||||||
|
|
||||||
|
const response = await fetch(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
file_path: filePath
|
||||||
|
})
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
showDeleteModal(filePath);
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to delete ${modelType}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
// Remove the card from UI
|
||||||
|
const card = document.querySelector(`.lora-card[data-filepath="${filePath}"]`);
|
||||||
|
if (card) {
|
||||||
|
card.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast(`${modelType} deleted successfully`, 'success');
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(data.error || `Failed to delete ${modelType}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error deleting ${modelType}:`, error);
|
||||||
|
showToast(`Failed to delete ${modelType}: ${error.message}`, 'error');
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,6 +425,48 @@ export async function refreshSingleModelMetadata(filePath, modelType = 'lora') {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generic function to exclude a model
|
||||||
|
export async function excludeModel(filePath, modelType = 'lora') {
|
||||||
|
try {
|
||||||
|
const endpoint = modelType === 'checkpoint'
|
||||||
|
? '/api/checkpoints/exclude'
|
||||||
|
: '/api/loras/exclude';
|
||||||
|
|
||||||
|
const response = await fetch(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
file_path: filePath
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to exclude ${modelType}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
// Remove the card from UI
|
||||||
|
const card = document.querySelector(`.lora-card[data-filepath="${filePath}"]`);
|
||||||
|
if (card) {
|
||||||
|
card.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast(`${modelType} excluded successfully`, 'success');
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(data.error || `Failed to exclude ${modelType}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error excluding ${modelType}:`, error);
|
||||||
|
showToast(`Failed to exclude ${modelType}: ${error.message}`, 'error');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Private methods
|
// Private methods
|
||||||
|
|
||||||
// Upload a preview image
|
// Upload a preview image
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import {
|
|||||||
deleteModel as baseDeleteModel,
|
deleteModel as baseDeleteModel,
|
||||||
replaceModelPreview,
|
replaceModelPreview,
|
||||||
fetchCivitaiMetadata,
|
fetchCivitaiMetadata,
|
||||||
refreshSingleModelMetadata
|
refreshSingleModelMetadata,
|
||||||
|
excludeModel as baseExcludeModel
|
||||||
} from './baseModelApi.js';
|
} from './baseModelApi.js';
|
||||||
|
|
||||||
// Load more checkpoints with pagination
|
// Load more checkpoints with pagination
|
||||||
@@ -86,3 +87,12 @@ export async function saveModelMetadata(filePath, data) {
|
|||||||
|
|
||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exclude a checkpoint model from being shown in the UI
|
||||||
|
* @param {string} filePath - File path of the checkpoint to exclude
|
||||||
|
* @returns {Promise<boolean>} Promise resolving to success status
|
||||||
|
*/
|
||||||
|
export function excludeCheckpoint(filePath) {
|
||||||
|
return baseExcludeModel(filePath, 'checkpoint');
|
||||||
|
}
|
||||||
@@ -6,7 +6,8 @@ import {
|
|||||||
deleteModel as baseDeleteModel,
|
deleteModel as baseDeleteModel,
|
||||||
replaceModelPreview,
|
replaceModelPreview,
|
||||||
fetchCivitaiMetadata,
|
fetchCivitaiMetadata,
|
||||||
refreshSingleModelMetadata
|
refreshSingleModelMetadata,
|
||||||
|
excludeModel as baseExcludeModel
|
||||||
} from './baseModelApi.js';
|
} from './baseModelApi.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,6 +35,15 @@ export async function saveModelMetadata(filePath, data) {
|
|||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exclude a lora model from being shown in the UI
|
||||||
|
* @param {string} filePath - File path of the model to exclude
|
||||||
|
* @returns {Promise<boolean>} Promise resolving to success status
|
||||||
|
*/
|
||||||
|
export async function excludeLora(filePath) {
|
||||||
|
return baseExcludeModel(filePath, 'lora');
|
||||||
|
}
|
||||||
|
|
||||||
export async function loadMoreLoras(resetPage = false, updateFolders = false) {
|
export async function loadMoreLoras(resetPage = false, updateFolders = false) {
|
||||||
return loadMoreModels({
|
return loadMoreModels({
|
||||||
resetPage,
|
resetPage,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { appCore } from './core.js';
|
import { appCore } from './core.js';
|
||||||
import { initializeInfiniteScroll } from './utils/infiniteScroll.js';
|
import { initializeInfiniteScroll } from './utils/infiniteScroll.js';
|
||||||
import { confirmDelete, closeDeleteModal } from './utils/modalUtils.js';
|
import { confirmDelete, closeDeleteModal, confirmExclude, closeExcludeModal } from './utils/modalUtils.js';
|
||||||
import { createPageControls } from './components/controls/index.js';
|
import { createPageControls } from './components/controls/index.js';
|
||||||
import { loadMoreCheckpoints } from './api/checkpointApi.js';
|
import { loadMoreCheckpoints } from './api/checkpointApi.js';
|
||||||
import { CheckpointDownloadManager } from './managers/CheckpointDownloadManager.js';
|
import { CheckpointDownloadManager } from './managers/CheckpointDownloadManager.js';
|
||||||
@@ -23,6 +23,8 @@ class CheckpointsPageManager {
|
|||||||
// Minimal set of functions that need to remain global
|
// Minimal set of functions that need to remain global
|
||||||
window.confirmDelete = confirmDelete;
|
window.confirmDelete = confirmDelete;
|
||||||
window.closeDeleteModal = closeDeleteModal;
|
window.closeDeleteModal = closeDeleteModal;
|
||||||
|
window.confirmExclude = confirmExclude;
|
||||||
|
window.closeExcludeModal = closeExcludeModal;
|
||||||
|
|
||||||
// Add loadCheckpoints function to window for FilterManager compatibility
|
// Add loadCheckpoints function to window for FilterManager compatibility
|
||||||
window.checkpointManager = {
|
window.checkpointManager = {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { state } from '../state/index.js';
|
|||||||
import { showCheckpointModal } from './checkpointModal/index.js';
|
import { showCheckpointModal } from './checkpointModal/index.js';
|
||||||
import { NSFW_LEVELS } from '../utils/constants.js';
|
import { NSFW_LEVELS } from '../utils/constants.js';
|
||||||
import { replaceCheckpointPreview as apiReplaceCheckpointPreview, saveModelMetadata } from '../api/checkpointApi.js';
|
import { replaceCheckpointPreview as apiReplaceCheckpointPreview, saveModelMetadata } from '../api/checkpointApi.js';
|
||||||
|
import { showDeleteModal } from '../utils/modalUtils.js';
|
||||||
|
|
||||||
export function createCheckpointCard(checkpoint) {
|
export function createCheckpointCard(checkpoint) {
|
||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
@@ -262,7 +263,7 @@ export function createCheckpointCard(checkpoint) {
|
|||||||
// Delete button click event
|
// Delete button click event
|
||||||
card.querySelector('.fa-trash')?.addEventListener('click', e => {
|
card.querySelector('.fa-trash')?.addEventListener('click', e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
deleteCheckpoint(checkpoint.file_path);
|
showDeleteModal(checkpoint.file_path);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Replace preview button click event
|
// Replace preview button click event
|
||||||
@@ -322,17 +323,6 @@ function openCivitai(modelName) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteCheckpoint(filePath) {
|
|
||||||
if (window.deleteCheckpoint) {
|
|
||||||
window.deleteCheckpoint(filePath);
|
|
||||||
} else {
|
|
||||||
// Use the modal delete functionality
|
|
||||||
import('../utils/modalUtils.js').then(({ showDeleteModal }) => {
|
|
||||||
showDeleteModal(filePath, 'checkpoint');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function replaceCheckpointPreview(filePath) {
|
function replaceCheckpointPreview(filePath) {
|
||||||
if (window.replaceCheckpointPreview) {
|
if (window.replaceCheckpointPreview) {
|
||||||
window.replaceCheckpointPreview(filePath);
|
window.replaceCheckpointPreview(filePath);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { refreshSingleCheckpointMetadata, saveModelMetadata } from '../../api/ch
|
|||||||
import { showToast, getNSFWLevelName } from '../../utils/uiHelpers.js';
|
import { showToast, getNSFWLevelName } from '../../utils/uiHelpers.js';
|
||||||
import { NSFW_LEVELS } from '../../utils/constants.js';
|
import { NSFW_LEVELS } from '../../utils/constants.js';
|
||||||
import { getStorageItem } from '../../utils/storageHelpers.js';
|
import { getStorageItem } from '../../utils/storageHelpers.js';
|
||||||
|
import { showExcludeModal } from '../../utils/modalUtils.js';
|
||||||
|
|
||||||
export class CheckpointContextMenu extends BaseContextMenu {
|
export class CheckpointContextMenu extends BaseContextMenu {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -61,6 +62,10 @@ export class CheckpointContextMenu extends BaseContextMenu {
|
|||||||
// Move to folder (placeholder)
|
// Move to folder (placeholder)
|
||||||
showToast('Move to folder feature coming soon', 'info');
|
showToast('Move to folder feature coming soon', 'info');
|
||||||
break;
|
break;
|
||||||
|
case 'exclude':
|
||||||
|
showExcludeModal(this.currentCard.dataset.filepath, 'checkpoint');
|
||||||
|
break;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { refreshSingleLoraMetadata, saveModelMetadata } from '../../api/loraApi.
|
|||||||
import { showToast, getNSFWLevelName } from '../../utils/uiHelpers.js';
|
import { showToast, getNSFWLevelName } from '../../utils/uiHelpers.js';
|
||||||
import { NSFW_LEVELS } from '../../utils/constants.js';
|
import { NSFW_LEVELS } from '../../utils/constants.js';
|
||||||
import { getStorageItem } from '../../utils/storageHelpers.js';
|
import { getStorageItem } from '../../utils/storageHelpers.js';
|
||||||
|
import { showExcludeModal } from '../../utils/modalUtils.js';
|
||||||
|
|
||||||
export class LoraContextMenu extends BaseContextMenu {
|
export class LoraContextMenu extends BaseContextMenu {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -51,6 +52,9 @@ export class LoraContextMenu extends BaseContextMenu {
|
|||||||
case 'set-nsfw':
|
case 'set-nsfw':
|
||||||
this.showNSFWLevelSelector(null, null, this.currentCard);
|
this.showNSFWLevelSelector(null, null, this.currentCard);
|
||||||
break;
|
break;
|
||||||
|
case 'exclude':
|
||||||
|
showExcludeModal(this.currentCard.dataset.filepath);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export class HeaderManager {
|
|||||||
|
|
||||||
const toggleText = qrToggle.querySelector('.toggle-text');
|
const toggleText = qrToggle.querySelector('.toggle-text');
|
||||||
if (qrContainer.classList.contains('show')) {
|
if (qrContainer.classList.contains('show')) {
|
||||||
toggleText.textContent = 'Hide QR Codes';
|
toggleText.textContent = 'Hide WeChat QR Code';
|
||||||
// Add small delay to ensure DOM is updated before scrolling
|
// Add small delay to ensure DOM is updated before scrolling
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const supportModal = document.querySelector('.support-modal');
|
const supportModal = document.querySelector('.support-modal');
|
||||||
@@ -102,7 +102,7 @@ export class HeaderManager {
|
|||||||
}
|
}
|
||||||
}, 250);
|
}, 250);
|
||||||
} else {
|
} else {
|
||||||
toggleText.textContent = 'Show QR Codes';
|
toggleText.textContent = 'Show WeChat QR Code';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { state } from '../state/index.js';
|
|||||||
import { showLoraModal } from './loraModal/index.js';
|
import { showLoraModal } from './loraModal/index.js';
|
||||||
import { bulkManager } from '../managers/BulkManager.js';
|
import { bulkManager } from '../managers/BulkManager.js';
|
||||||
import { NSFW_LEVELS } from '../utils/constants.js';
|
import { NSFW_LEVELS } from '../utils/constants.js';
|
||||||
import { replacePreview, deleteModel, saveModelMetadata } from '../api/loraApi.js'
|
import { replacePreview, saveModelMetadata } from '../api/loraApi.js'
|
||||||
|
import { showDeleteModal } from '../utils/modalUtils.js';
|
||||||
|
|
||||||
export function createLoraCard(lora) {
|
export function createLoraCard(lora) {
|
||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
@@ -260,7 +261,7 @@ export function createLoraCard(lora) {
|
|||||||
// Delete button click event
|
// Delete button click event
|
||||||
card.querySelector('.fa-trash')?.addEventListener('click', e => {
|
card.querySelector('.fa-trash')?.addEventListener('click', e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
deleteModel(lora.file_path);
|
showDeleteModal(lora.file_path);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Replace preview button click event
|
// Replace preview button click event
|
||||||
|
|||||||
@@ -328,6 +328,8 @@ function initMetadataPanelHandlers(container) {
|
|||||||
|
|
||||||
if (!metadataPanel || !mediaElement) return;
|
if (!metadataPanel || !mediaElement) return;
|
||||||
|
|
||||||
|
let isOverMetadataPanel = false;
|
||||||
|
|
||||||
// Add event listeners to the wrapper for mouse tracking
|
// Add event listeners to the wrapper for mouse tracking
|
||||||
wrapper.addEventListener('mousemove', (e) => {
|
wrapper.addEventListener('mousemove', (e) => {
|
||||||
// Get mouse position relative to wrapper
|
// Get mouse position relative to wrapper
|
||||||
@@ -346,8 +348,8 @@ function initMetadataPanelHandlers(container) {
|
|||||||
mouseY <= mediaRect.bottom
|
mouseY <= mediaRect.bottom
|
||||||
);
|
);
|
||||||
|
|
||||||
// Show metadata panel only when over actual media content
|
// Show metadata panel when over media content or metadata panel itself
|
||||||
if (isOverMedia) {
|
if (isOverMedia || isOverMetadataPanel) {
|
||||||
metadataPanel.classList.add('visible');
|
metadataPanel.classList.add('visible');
|
||||||
} else {
|
} else {
|
||||||
metadataPanel.classList.remove('visible');
|
metadataPanel.classList.remove('visible');
|
||||||
@@ -355,8 +357,36 @@ function initMetadataPanelHandlers(container) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
wrapper.addEventListener('mouseleave', () => {
|
wrapper.addEventListener('mouseleave', () => {
|
||||||
// Hide panel when mouse leaves the wrapper
|
// Only hide panel when mouse leaves the wrapper and not over the metadata panel
|
||||||
metadataPanel.classList.remove('visible');
|
if (!isOverMetadataPanel) {
|
||||||
|
metadataPanel.classList.remove('visible');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add mouse enter/leave events for the metadata panel itself
|
||||||
|
metadataPanel.addEventListener('mouseenter', () => {
|
||||||
|
isOverMetadataPanel = true;
|
||||||
|
metadataPanel.classList.add('visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
metadataPanel.addEventListener('mouseleave', () => {
|
||||||
|
isOverMetadataPanel = false;
|
||||||
|
// Only hide if mouse is not over the media
|
||||||
|
const rect = wrapper.getBoundingClientRect();
|
||||||
|
const mediaRect = getRenderedMediaRect(mediaElement, rect.width, rect.height);
|
||||||
|
const mouseX = event.clientX - rect.left;
|
||||||
|
const mouseY = event.clientY - rect.top;
|
||||||
|
|
||||||
|
const isOverMedia = (
|
||||||
|
mouseX >= mediaRect.left &&
|
||||||
|
mouseX <= mediaRect.right &&
|
||||||
|
mouseY >= mediaRect.top &&
|
||||||
|
mouseY <= mediaRect.bottom
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isOverMedia) {
|
||||||
|
metadataPanel.classList.remove('visible');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Prevent events from bubbling
|
// Prevent events from bubbling
|
||||||
@@ -386,8 +416,14 @@ function initMetadataPanelHandlers(container) {
|
|||||||
|
|
||||||
// Prevent panel scroll from causing modal scroll
|
// Prevent panel scroll from causing modal scroll
|
||||||
metadataPanel.addEventListener('wheel', (e) => {
|
metadataPanel.addEventListener('wheel', (e) => {
|
||||||
e.stopPropagation();
|
const isAtTop = metadataPanel.scrollTop === 0;
|
||||||
});
|
const isAtBottom = metadataPanel.scrollHeight - metadataPanel.scrollTop === metadataPanel.clientHeight;
|
||||||
|
|
||||||
|
// Only prevent default if scrolling would cause the panel to scroll
|
||||||
|
if ((e.deltaY < 0 && !isAtTop) || (e.deltaY > 0 && !isAtBottom)) {
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
}, { passive: true });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -335,6 +335,8 @@ function initMetadataPanelHandlers(container) {
|
|||||||
|
|
||||||
if (!metadataPanel || !mediaElement) return;
|
if (!metadataPanel || !mediaElement) return;
|
||||||
|
|
||||||
|
let isOverMetadataPanel = false;
|
||||||
|
|
||||||
// Add event listeners to the wrapper for mouse tracking
|
// Add event listeners to the wrapper for mouse tracking
|
||||||
wrapper.addEventListener('mousemove', (e) => {
|
wrapper.addEventListener('mousemove', (e) => {
|
||||||
// Get mouse position relative to wrapper
|
// Get mouse position relative to wrapper
|
||||||
@@ -353,8 +355,8 @@ function initMetadataPanelHandlers(container) {
|
|||||||
mouseY <= mediaRect.bottom
|
mouseY <= mediaRect.bottom
|
||||||
);
|
);
|
||||||
|
|
||||||
// Show metadata panel only when over actual media content
|
// Show metadata panel when over media content
|
||||||
if (isOverMedia) {
|
if (isOverMedia || isOverMetadataPanel) {
|
||||||
metadataPanel.classList.add('visible');
|
metadataPanel.classList.add('visible');
|
||||||
} else {
|
} else {
|
||||||
metadataPanel.classList.remove('visible');
|
metadataPanel.classList.remove('visible');
|
||||||
@@ -362,8 +364,36 @@ function initMetadataPanelHandlers(container) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
wrapper.addEventListener('mouseleave', () => {
|
wrapper.addEventListener('mouseleave', () => {
|
||||||
// Hide panel when mouse leaves the wrapper
|
// Only hide panel when mouse leaves the wrapper and not over the metadata panel
|
||||||
metadataPanel.classList.remove('visible');
|
if (!isOverMetadataPanel) {
|
||||||
|
metadataPanel.classList.remove('visible');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add mouse enter/leave events for the metadata panel itself
|
||||||
|
metadataPanel.addEventListener('mouseenter', () => {
|
||||||
|
isOverMetadataPanel = true;
|
||||||
|
metadataPanel.classList.add('visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
metadataPanel.addEventListener('mouseleave', () => {
|
||||||
|
isOverMetadataPanel = false;
|
||||||
|
// Only hide if mouse is not over the media
|
||||||
|
const rect = wrapper.getBoundingClientRect();
|
||||||
|
const mediaRect = getRenderedMediaRect(mediaElement, rect.width, rect.height);
|
||||||
|
const mouseX = event.clientX - rect.left;
|
||||||
|
const mouseY = event.clientY - rect.top;
|
||||||
|
|
||||||
|
const isOverMedia = (
|
||||||
|
mouseX >= mediaRect.left &&
|
||||||
|
mouseX <= mediaRect.right &&
|
||||||
|
mouseY >= mediaRect.top &&
|
||||||
|
mouseY <= mediaRect.bottom
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isOverMedia) {
|
||||||
|
metadataPanel.classList.remove('visible');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Prevent events from the metadata panel from bubbling
|
// Prevent events from the metadata panel from bubbling
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { DownloadManager } from './managers/DownloadManager.js';
|
|||||||
import { moveManager } from './managers/MoveManager.js';
|
import { moveManager } from './managers/MoveManager.js';
|
||||||
import { LoraContextMenu } from './components/ContextMenu/index.js';
|
import { LoraContextMenu } from './components/ContextMenu/index.js';
|
||||||
import { createPageControls } from './components/controls/index.js';
|
import { createPageControls } from './components/controls/index.js';
|
||||||
import { confirmDelete, closeDeleteModal } from './utils/modalUtils.js';
|
import { confirmDelete, closeDeleteModal, confirmExclude, closeExcludeModal } from './utils/modalUtils.js';
|
||||||
|
|
||||||
// Initialize the LoRA page
|
// Initialize the LoRA page
|
||||||
class LoraPageManager {
|
class LoraPageManager {
|
||||||
@@ -35,6 +35,8 @@ class LoraPageManager {
|
|||||||
window.showLoraModal = showLoraModal;
|
window.showLoraModal = showLoraModal;
|
||||||
window.confirmDelete = confirmDelete;
|
window.confirmDelete = confirmDelete;
|
||||||
window.closeDeleteModal = closeDeleteModal;
|
window.closeDeleteModal = closeDeleteModal;
|
||||||
|
window.confirmExclude = confirmExclude;
|
||||||
|
window.closeExcludeModal = closeExcludeModal;
|
||||||
window.downloadManager = this.downloadManager;
|
window.downloadManager = this.downloadManager;
|
||||||
window.moveManager = moveManager;
|
window.moveManager = moveManager;
|
||||||
window.toggleShowcase = toggleShowcase;
|
window.toggleShowcase = toggleShowcase;
|
||||||
|
|||||||
@@ -60,6 +60,19 @@ export class ModalManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add excludeModal registration
|
||||||
|
const excludeModal = document.getElementById('excludeModal');
|
||||||
|
if (excludeModal) {
|
||||||
|
this.registerModal('excludeModal', {
|
||||||
|
element: excludeModal,
|
||||||
|
onClose: () => {
|
||||||
|
this.getModal('excludeModal').element.classList.remove('show');
|
||||||
|
document.body.classList.remove('modal-open');
|
||||||
|
},
|
||||||
|
closeOnOutsideClick: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Add downloadModal registration
|
// Add downloadModal registration
|
||||||
const downloadModal = document.getElementById('downloadModal');
|
const downloadModal = document.getElementById('downloadModal');
|
||||||
if (downloadModal) {
|
if (downloadModal) {
|
||||||
@@ -208,7 +221,7 @@ export class ModalManager {
|
|||||||
// Store current scroll position before showing modal
|
// Store current scroll position before showing modal
|
||||||
this.scrollPosition = window.scrollY;
|
this.scrollPosition = window.scrollY;
|
||||||
|
|
||||||
if (id === 'deleteModal') {
|
if (id === 'deleteModal' || id === 'excludeModal') {
|
||||||
modal.element.classList.add('show');
|
modal.element.classList.add('show');
|
||||||
} else {
|
} else {
|
||||||
modal.element.style.display = 'block';
|
modal.element.style.display = 'block';
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import { modalManager } from '../managers/ModalManager.js';
|
import { modalManager } from '../managers/ModalManager.js';
|
||||||
|
import { excludeLora, deleteModel as deleteLora } from '../api/loraApi.js';
|
||||||
|
import { excludeCheckpoint, deleteCheckpoint } from '../api/checkpointApi.js';
|
||||||
|
|
||||||
let pendingDeletePath = null;
|
let pendingDeletePath = null;
|
||||||
let pendingModelType = null;
|
let pendingModelType = null;
|
||||||
|
let pendingExcludePath = null;
|
||||||
|
let pendingExcludeModelType = null;
|
||||||
|
|
||||||
export function showDeleteModal(filePath, modelType = 'lora') {
|
export function showDeleteModal(filePath, modelType = 'lora') {
|
||||||
// event.stopPropagation();
|
|
||||||
pendingDeletePath = filePath;
|
pendingDeletePath = filePath;
|
||||||
pendingModelType = modelType;
|
pendingModelType = modelType;
|
||||||
|
|
||||||
const card = document.querySelector(`.lora-card[data-filepath="${filePath}"]`);
|
const card = document.querySelector(`.lora-card[data-filepath="${filePath}"]`);
|
||||||
const modelName = card.dataset.name;
|
const modelName = card ? card.dataset.name : filePath.split('/').pop();
|
||||||
const modal = modalManager.getModal('deleteModal').element;
|
const modal = modalManager.getModal('deleteModal').element;
|
||||||
const modelInfo = modal.querySelector('.delete-model-info');
|
const modelInfo = modal.querySelector('.delete-model-info');
|
||||||
|
|
||||||
@@ -28,31 +31,19 @@ export async function confirmDelete() {
|
|||||||
const card = document.querySelector(`.lora-card[data-filepath="${pendingDeletePath}"]`);
|
const card = document.querySelector(`.lora-card[data-filepath="${pendingDeletePath}"]`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use the appropriate endpoint based on model type
|
// Use appropriate delete function based on model type
|
||||||
const endpoint = pendingModelType === 'checkpoint' ?
|
if (pendingModelType === 'checkpoint') {
|
||||||
'/api/checkpoints/delete' :
|
await deleteCheckpoint(pendingDeletePath);
|
||||||
'/api/delete_model';
|
|
||||||
|
|
||||||
const response = await fetch(endpoint, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
file_path: pendingDeletePath
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
if (card) {
|
|
||||||
card.remove();
|
|
||||||
}
|
|
||||||
closeDeleteModal();
|
|
||||||
} else {
|
} else {
|
||||||
const error = await response.text();
|
await deleteLora(pendingDeletePath);
|
||||||
alert(`Failed to delete model: ${error}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (card) {
|
||||||
|
card.remove();
|
||||||
|
}
|
||||||
|
closeDeleteModal();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('Error deleting model:', error);
|
||||||
alert(`Error deleting model: ${error}`);
|
alert(`Error deleting model: ${error}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,3 +53,45 @@ export function closeDeleteModal() {
|
|||||||
pendingDeletePath = null;
|
pendingDeletePath = null;
|
||||||
pendingModelType = null;
|
pendingModelType = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Functions for the exclude modal
|
||||||
|
export function showExcludeModal(filePath, modelType = 'lora') {
|
||||||
|
pendingExcludePath = filePath;
|
||||||
|
pendingExcludeModelType = modelType;
|
||||||
|
|
||||||
|
const card = document.querySelector(`.lora-card[data-filepath="${filePath}"]`);
|
||||||
|
const modelName = card ? card.dataset.name : filePath.split('/').pop();
|
||||||
|
const modal = modalManager.getModal('excludeModal').element;
|
||||||
|
const modelInfo = modal.querySelector('.exclude-model-info');
|
||||||
|
|
||||||
|
modelInfo.innerHTML = `
|
||||||
|
<strong>Model:</strong> ${modelName}
|
||||||
|
<br>
|
||||||
|
<strong>File:</strong> ${filePath}
|
||||||
|
`;
|
||||||
|
|
||||||
|
modalManager.showModal('excludeModal');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeExcludeModal() {
|
||||||
|
modalManager.closeModal('excludeModal');
|
||||||
|
pendingExcludePath = null;
|
||||||
|
pendingExcludeModelType = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function confirmExclude() {
|
||||||
|
if (!pendingExcludePath) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use appropriate exclude function based on model type
|
||||||
|
if (pendingExcludeModelType === 'checkpoint') {
|
||||||
|
await excludeCheckpoint(pendingExcludePath);
|
||||||
|
} else {
|
||||||
|
await excludeLora(pendingExcludePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
closeExcludeModal();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error excluding model:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
{% include 'components/checkpoint_modals.html' %}
|
{% include 'components/checkpoint_modals.html' %}
|
||||||
|
|
||||||
<div id="checkpointContextMenu" class="context-menu" style="display: none;">
|
<div id="checkpointContextMenu" class="context-menu" style="display: none;">
|
||||||
<div class="context-menu-item" data-action="details"><i class="fas fa-info-circle"></i> View Details</div>
|
<!-- <div class="context-menu-item" data-action="details"><i class="fas fa-info-circle"></i> View Details</div> -->
|
||||||
<div class="context-menu-item" data-action="civitai"><i class="fas fa-external-link-alt"></i> View on CivitAI</div>
|
<div class="context-menu-item" data-action="civitai"><i class="fas fa-external-link-alt"></i> View on CivitAI</div>
|
||||||
<div class="context-menu-item" data-action="refresh-metadata"><i class="fas fa-sync"></i> Refresh Civitai Data</div>
|
<div class="context-menu-item" data-action="refresh-metadata"><i class="fas fa-sync"></i> Refresh Civitai Data</div>
|
||||||
<div class="context-menu-item" data-action="copyname"><i class="fas fa-copy"></i> Copy Model Filename</div>
|
<div class="context-menu-item" data-action="copyname"><i class="fas fa-copy"></i> Copy Model Filename</div>
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
<div class="context-menu-item" data-action="set-nsfw"><i class="fas fa-exclamation-triangle"></i> Set Content Rating</div>
|
<div class="context-menu-item" data-action="set-nsfw"><i class="fas fa-exclamation-triangle"></i> Set Content Rating</div>
|
||||||
<div class="context-menu-separator"></div>
|
<div class="context-menu-separator"></div>
|
||||||
<div class="context-menu-item" data-action="move"><i class="fas fa-folder-open"></i> Move to Folder</div>
|
<div class="context-menu-item" data-action="move"><i class="fas fa-folder-open"></i> Move to Folder</div>
|
||||||
|
<div class="context-menu-item" data-action="exclude"><i class="fas fa-eye-slash"></i> Exclude Model</div>
|
||||||
<div class="context-menu-item delete-item" data-action="delete"><i class="fas fa-trash"></i> Delete Model</div>
|
<div class="context-menu-item delete-item" data-action="delete"><i class="fas fa-trash"></i> Delete Model</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<div id="loraContextMenu" class="context-menu">
|
<div id="loraContextMenu" class="context-menu">
|
||||||
<div class="context-menu-item" data-action="detail">
|
<!-- <div class="context-menu-item" data-action="detail">
|
||||||
<i class="fas fa-info-circle"></i> Show Details
|
<i class="fas fa-info-circle"></i> Show Details
|
||||||
</div>
|
</div> -->
|
||||||
<div class="context-menu-item" data-action="civitai">
|
<div class="context-menu-item" data-action="civitai">
|
||||||
<i class="fas fa-external-link-alt"></i> View on Civitai
|
<i class="fas fa-external-link-alt"></i> View on Civitai
|
||||||
</div>
|
</div>
|
||||||
@@ -21,6 +21,9 @@
|
|||||||
<div class="context-menu-item" data-action="move">
|
<div class="context-menu-item" data-action="move">
|
||||||
<i class="fas fa-folder-open"></i> Move to Folder
|
<i class="fas fa-folder-open"></i> Move to Folder
|
||||||
</div>
|
</div>
|
||||||
|
<div class="context-menu-item" data-action="exclude">
|
||||||
|
<i class="fas fa-eye-slash"></i> Exclude Model
|
||||||
|
</div>
|
||||||
<div class="context-menu-item delete-item" data-action="delete">
|
<div class="context-menu-item delete-item" data-action="delete">
|
||||||
<i class="fas fa-trash"></i> Delete Model
|
<i class="fas fa-trash"></i> Delete Model
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,6 +11,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Exclude Confirmation Modal -->
|
||||||
|
<div id="excludeModal" class="modal delete-modal">
|
||||||
|
<div class="modal-content delete-modal-content">
|
||||||
|
<h2>Exclude Model</h2>
|
||||||
|
<p class="delete-message">Are you sure you want to exclude this model? Excluded models won't appear in searches or model lists.</p>
|
||||||
|
<div class="exclude-model-info"></div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="cancel-btn" onclick="closeExcludeModal()">Cancel</button>
|
||||||
|
<button class="exclude-btn" onclick="confirmExclude()">Exclude</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Settings Modal -->
|
<!-- Settings Modal -->
|
||||||
<div id="settingsModal" class="modal">
|
<div id="settingsModal" class="modal">
|
||||||
<div class="modal-content settings-modal">
|
<div class="modal-content settings-modal">
|
||||||
@@ -232,15 +245,15 @@
|
|||||||
|
|
||||||
<!-- New section for Chinese payment methods -->
|
<!-- New section for Chinese payment methods -->
|
||||||
<div class="support-section">
|
<div class="support-section">
|
||||||
<h3><i class="fas fa-qrcode"></i> WeChat & Alipay Support</h3>
|
<h3><i class="fas fa-qrcode"></i> WeChat Support</h3>
|
||||||
<p>For users in China, you can support via WeChat Pay or Alipay:</p>
|
<p>For users in China, you can support via WeChat:</p>
|
||||||
<button class="secondary-btn qrcode-toggle" id="toggleQRCode">
|
<button class="secondary-btn qrcode-toggle" id="toggleQRCode">
|
||||||
<i class="fas fa-qrcode"></i>
|
<i class="fas fa-qrcode"></i>
|
||||||
<span class="toggle-text">Show QR Codes</span>
|
<span class="toggle-text">Show WeChat QR Code</span>
|
||||||
<i class="fas fa-chevron-down toggle-icon"></i>
|
<i class="fas fa-chevron-down toggle-icon"></i>
|
||||||
</button>
|
</button>
|
||||||
<div class="qrcode-container" id="qrCodeContainer">
|
<div class="qrcode-container" id="qrCodeContainer">
|
||||||
<img src="/loras_static/images/combined-qr.webp" alt="WeChat Pay & Alipay QR Codes" class="qrcode-image">
|
<img src="/loras_static/images/wechat-qr.webp" alt="WeChat Pay QR Code" class="qrcode-image">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
{% include 'components/recipe_modal.html' %}
|
{% include 'components/recipe_modal.html' %}
|
||||||
|
|
||||||
<div id="recipeContextMenu" class="context-menu" style="display: none;">
|
<div id="recipeContextMenu" class="context-menu" style="display: none;">
|
||||||
<div class="context-menu-item" data-action="details"><i class="fas fa-info-circle"></i> View Details</div>
|
<!-- <div class="context-menu-item" data-action="details"><i class="fas fa-info-circle"></i> View Details</div> -->
|
||||||
<div class="context-menu-item" data-action="share"><i class="fas fa-share-alt"></i> Share Recipe</div>
|
<div class="context-menu-item" data-action="share"><i class="fas fa-share-alt"></i> Share Recipe</div>
|
||||||
<div class="context-menu-item" data-action="copy"><i class="fas fa-copy"></i> Copy Recipe Syntax</div>
|
<div class="context-menu-item" data-action="copy"><i class="fas fa-copy"></i> Copy Recipe Syntax</div>
|
||||||
<div class="context-menu-item" data-action="viewloras"><i class="fas fa-layer-group"></i> View All LoRAs</div>
|
<div class="context-menu-item" data-action="viewloras"><i class="fas fa-layer-group"></i> View All LoRAs</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user