From c402f5325841436e18486506d71f470aa6d178b0 Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Sun, 23 Mar 2025 14:45:11 +0800 Subject: [PATCH] Implement early access handling and UI enhancements for LoRA downloads - Added error handling for early access restrictions in the API routes, returning appropriate status codes and messages. - Enhanced the Civitai client to log unauthorized access attempts and provide user-friendly error messages. - Updated the download manager to check for early access requirements and log warnings accordingly. - Introduced UI elements to indicate early access status for LoRAs, including badges and warning messages in the import manager. - Improved toast notifications to inform users about early access download failures and provide relevant information. --- py/routes/api_routes.py | 28 +++- py/routes/recipe_routes.py | 4 +- py/services/civitai_client.py | 11 ++ py/services/download_manager.py | 23 ++++ py/utils/recipe_parsers.py | 21 +++ static/css/components/download-modal.css | 1 + static/css/components/import-modal.css | 51 ++++++++ static/css/components/shared.css | 53 ++++++++ static/css/components/toast.css | 59 +++++++++ static/js/managers/DownloadManager.js | 22 +++- static/js/managers/ImportManager.js | 155 ++++++++++++++++++++--- static/js/utils/uiHelpers.js | 47 ++++++- 12 files changed, 448 insertions(+), 27 deletions(-) diff --git a/py/routes/api_routes.py b/py/routes/api_routes.py index 1d255141..ab4feace 100644 --- a/py/routes/api_routes.py +++ b/py/routes/api_routes.py @@ -579,16 +579,36 @@ class ApiRoutes: download_url=data.get('download_url'), save_dir=data.get('lora_root'), relative_path=data.get('relative_path'), - progress_callback=progress_callback # Add progress callback + progress_callback=progress_callback ) if not result.get('success', False): - return web.Response(status=500, text=result.get('error', 'Unknown error')) + error_message = result.get('error', 'Unknown error') + + # Return 401 for early access errors + if 'early access' in error_message.lower(): + logger.warning(f"Early access download failed: {error_message}") + return web.Response( + status=401, # Use 401 status code to match Civitai's response + text=f"Early Access Restriction: {error_message}" + ) + + return web.Response(status=500, text=error_message) return web.json_response(result) except Exception as e: - logger.error(f"Error downloading LoRA: {e}") - return web.Response(status=500, text=str(e)) + error_message = str(e) + + # Check if this might be an early access error + if '401' in error_message: + logger.warning(f"Early access error (401): {error_message}") + return web.Response( + status=401, + text="Early Access Restriction: This LoRA requires purchase. Please buy early access on Civitai.com." + ) + + logger.error(f"Error downloading LoRA: {error_message}") + return web.Response(status=500, text=error_message) async def update_settings(self, request: web.Request) -> web.Response: """Update application settings""" diff --git a/py/routes/recipe_routes.py b/py/routes/recipe_routes.py index dc56c7b1..e447ac06 100644 --- a/py/routes/recipe_routes.py +++ b/py/routes/recipe_routes.py @@ -1,4 +1,5 @@ import os +import time import logging from aiohttp import web from typing import Dict @@ -13,7 +14,7 @@ from ..services.recipe_scanner import RecipeScanner from ..services.lora_scanner import LoraScanner from ..config import config from ..workflow.parser import WorkflowParser -import time # Add this import at the top +from ..utils.utils import download_twitter_image logger = logging.getLogger(__name__) @@ -234,7 +235,6 @@ class RecipeRoutes: }, status=400) # Download image from URL - from ..utils.utils import download_twitter_image temp_path = download_twitter_image(url) if not temp_path: diff --git a/py/services/civitai_client.py b/py/services/civitai_client.py index 91387ebe..13fc55c6 100644 --- a/py/services/civitai_client.py +++ b/py/services/civitai_client.py @@ -76,6 +76,17 @@ class CivitaiClient: headers = self._get_request_headers() async with session.get(url, headers=headers, allow_redirects=True) as response: if response.status != 200: + # Handle early access 401 unauthorized responses + if response.status == 401: + logger.warning(f"Unauthorized access to resource: {url} (Status 401)") + return False, "Early access restriction: You must purchase early access to download this LoRA." + + # Handle other client errors that might be permission-related + if response.status == 403: + logger.warning(f"Forbidden access to resource: {url} (Status 403)") + return False, "Access forbidden: You don't have permission to download this file." + + # Generic error response for other status codes return False, f"Download failed with status {response.status}" # Get filename from content-disposition header diff --git a/py/services/download_manager.py b/py/services/download_manager.py index 66330a68..5e6746f7 100644 --- a/py/services/download_manager.py +++ b/py/services/download_manager.py @@ -28,6 +28,25 @@ class DownloadManager: if not version_info: return {'success': False, 'error': 'Failed to fetch model metadata'} + # Check if this is an early access LoRA + if 'earlyAccessEndsAt' in version_info: + early_access_date = version_info.get('earlyAccessEndsAt', '') + # Convert to a readable date if possible + try: + from datetime import datetime + date_obj = datetime.fromisoformat(early_access_date.replace('Z', '+00:00')) + formatted_date = date_obj.strftime('%Y-%m-%d') + early_access_msg = f"This LoRA requires early access payment (until {formatted_date}). " + except: + early_access_msg = "This LoRA requires early access payment. " + + early_access_msg += "Please ensure you have purchased early access and are logged in to Civitai." + logger.warning(f"Early access LoRA detected: {version_info.get('name', 'Unknown')}") + + # We'll still try to download, but log a warning and prepare for potential failure + if progress_callback: + await progress_callback(1) # Show minimal progress to indicate we're trying + # Report initial progress if progress_callback: await progress_callback(0) @@ -82,6 +101,10 @@ class DownloadManager: except Exception as e: logger.error(f"Error in download_from_civitai: {e}", exc_info=True) + # Check if this might be an early access error + error_str = str(e).lower() + if "403" in error_str or "401" in error_str or "unauthorized" in error_str or "early access" in error_str: + return {'success': False, 'error': f"Early access restriction: {str(e)}. Please ensure you have purchased early access and are logged in to Civitai."} return {'success': False, 'error': str(e)} async def _execute_download(self, download_url: str, save_dir: str, diff --git a/py/utils/recipe_parsers.py b/py/utils/recipe_parsers.py index 581eda74..c83ed587 100644 --- a/py/utils/recipe_parsers.py +++ b/py/utils/recipe_parsers.py @@ -111,6 +111,13 @@ class RecipeFormatParser(RecipeMetadataParser): try: civitai_info = await civitai_client.get_model_version_info(lora['modelVersionId']) if civitai_info and civitai_info.get("error") != "Model not found": + # Check if this is an early access lora + if 'earlyAccessEndsAt' in civitai_info: + # Convert earlyAccessEndsAt to a human-readable date + early_access_date = civitai_info.get('earlyAccessEndsAt', '') + lora_entry['isEarlyAccess'] = True + lora_entry['earlyAccessEndsAt'] = early_access_date + # Get thumbnail URL from first image if 'images' in civitai_info and civitai_info['images']: lora_entry['thumbnailUrl'] = civitai_info['images'][0].get('url', '') @@ -219,6 +226,13 @@ class StandardMetadataParser(RecipeMetadataParser): # Check if this LoRA exists locally by SHA256 hash if civitai_info and civitai_info.get("error") != "Model not found": + # Check if this is an early access lora + if 'earlyAccessEndsAt' in civitai_info: + # Convert earlyAccessEndsAt to a human-readable date + early_access_date = civitai_info.get('earlyAccessEndsAt', '') + lora_entry['isEarlyAccess'] = True + lora_entry['earlyAccessEndsAt'] = early_access_date + # LoRA exists on Civitai, process its information if 'files' in civitai_info: # Find the model file (type="Model") in the files list @@ -427,6 +441,13 @@ class A1111MetadataParser(RecipeMetadataParser): try: civitai_info = await civitai_client.get_model_by_hash(hash_value) if civitai_info and civitai_info.get("error") != "Model not found": + # Check if this is an early access lora + if 'earlyAccessEndsAt' in civitai_info: + # Convert earlyAccessEndsAt to a human-readable date + early_access_date = civitai_info.get('earlyAccessEndsAt', '') + lora_entry['isEarlyAccess'] = True + lora_entry['earlyAccessEndsAt'] = early_access_date + # Get model version ID lora_entry['id'] = civitai_info.get('id', '') diff --git a/static/css/components/download-modal.css b/static/css/components/download-modal.css index 6ba34053..e220a104 100644 --- a/static/css/components/download-modal.css +++ b/static/css/components/download-modal.css @@ -98,6 +98,7 @@ .version-info { display: flex; flex-wrap: wrap; + flex-direction: row !important; gap: 8px; align-items: center; font-size: 0.9em; diff --git a/static/css/components/import-modal.css b/static/css/components/import-modal.css index 76711dc4..4349c749 100644 --- a/static/css/components/import-modal.css +++ b/static/css/components/import-modal.css @@ -207,6 +207,15 @@ border-left: 4px solid var(--lora-warning); } +.lora-item.is-early-access { + background: rgba(0, 184, 122, 0.05); + border-left: 4px solid #00B87A; +} + +.lora-item.missing-locally { + border-left: 4px solid var(--lora-error); +} + .lora-thumbnail { width: 80px; height: 80px; @@ -297,6 +306,12 @@ border-bottom: none; } +.missing-lora-item.is-early-access { + background: rgba(0, 184, 122, 0.05); + border-left: 3px solid #00B87A; + padding-left: 10px; +} + .missing-badge { display: inline-flex; align-items: center; @@ -682,3 +697,39 @@ min-height: 20px; /* Ensure there's always space for the error message */ font-weight: 500; } + +.early-access-warning { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 12px 16px; + background: rgba(0, 184, 122, 0.1); + border: 1px solid #00B87A; + border-radius: var(--border-radius-sm); + color: var(--text-color); + margin-bottom: var(--space-2); +} + +/* Add special styling for early access badge in the missing loras list */ +.missing-lora-item .early-access-badge { + padding: 2px 6px; + font-size: 0.75em; + margin-top: 4px; + display: inline-flex; +} + +/* Specific styling for the early access warning container in import modal */ +.early-access-warning .warning-icon { + color: #00B87A; + font-size: 1.2em; +} + +.early-access-warning .warning-title { + font-weight: 600; + margin-bottom: 4px; +} + +.early-access-warning .warning-text { + font-size: 0.9em; + line-height: 1.4; +} diff --git a/static/css/components/shared.css b/static/css/components/shared.css index d1ba7c3f..8d436ec8 100644 --- a/static/css/components/shared.css +++ b/static/css/components/shared.css @@ -21,6 +21,59 @@ font-size: 0.9em; } +/* Early Access Badge */ +.early-access-badge { + display: inline-flex; + align-items: center; + background: #00B87A; /* Green for early access */ + color: white; + padding: 4px 8px; + border-radius: var(--border-radius-xs); + font-size: 0.8em; + font-weight: 500; + white-space: nowrap; + flex-shrink: 0; + position: relative; + /* Force hardware acceleration to prevent Chrome scroll issues */ + transform: translateZ(0); + will-change: transform; +} + +.early-access-badge i { + margin-right: 4px; + font-size: 0.9em; +} + +.early-access-info { + display: none; + position: absolute; + top: 100%; + right: 0; + background: var(--card-bg); + border: 1px solid #00B87A; + border-radius: var(--border-radius-xs); + padding: var(--space-1); + margin-top: 4px; + font-size: 0.9em; + color: var(--text-color); + white-space: normal; + word-break: break-all; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + z-index: 100; /* Higher z-index to ensure it's above other elements */ + min-width: 300px; + max-width: 300px; + /* Create a separate layer with hardware acceleration */ + transform: translateZ(0); + /* Use a fixed position to ensure it's in a separate layer from scrollable content */ + position: fixed; + pointer-events: none; /* Don't block mouse events */ +} + +.early-access-badge:hover .early-access-info { + display: block; + pointer-events: auto; /* Allow interaction with the tooltip when visible */ +} + .local-path { display: none; position: absolute; diff --git a/static/css/components/toast.css b/static/css/components/toast.css index 955c13fc..f8b692f8 100644 --- a/static/css/components/toast.css +++ b/static/css/components/toast.css @@ -120,4 +120,63 @@ .tooltip:hover::after { opacity: 1; +} + +/* Toast Container for stacked notifications */ +.toast-container { + position: fixed; + top: 0; + right: 0; + z-index: calc(var(--z-overlay) + 10); + display: flex; + flex-direction: column; + gap: 10px; + padding: 20px; + pointer-events: none; /* Allow clicking through the container */ + width: 400px; + max-width: 100%; +} + +/* Ensure each toast has pointer events */ +.toast-container .toast { + pointer-events: auto; + position: relative; /* Override fixed positioning */ + top: 0 !important; /* Let the container handle positioning */ + right: 0 !important; + margin-bottom: 10px; +} + +/* Add missing warning toast style */ +.toast-warning { + border-left: 4px solid var(--lora-warning); +} + +.toast-warning::before { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23ff9800'%3E%3Cpath d='M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z'/%3E%3C/svg%3E"); +} + +/* Improve toast animation */ +.toast { + transform: translateX(120%); + opacity: 0; + transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), + opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.toast.show { + transform: translateX(0); + opacity: 1; +} + +/* Responsive adjustments */ +@media (max-width: 480px) { + .toast-container { + width: 100%; + padding: 10px; + } + + .toast { + width: 100%; + max-width: none; + } } \ No newline at end of file diff --git a/static/js/managers/DownloadManager.js b/static/js/managers/DownloadManager.js index 2e3f5fc8..b6ccf89b 100644 --- a/static/js/managers/DownloadManager.js +++ b/static/js/managers/DownloadManager.js @@ -130,7 +130,22 @@ export class DownloadManager { const existsLocally = version.existsLocally; const localPath = version.localPath; - // 更新本地状态指示器为badge样式 + // Check if this is an early access version + const isEarlyAccess = version.availability === 'EarlyAccess'; + + // Create early access badge if needed + let earlyAccessBadge = ''; + if (isEarlyAccess) { + earlyAccessBadge = ` +