mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 15:15:44 -03:00
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.
This commit is contained in:
@@ -579,16 +579,36 @@ class ApiRoutes:
|
|||||||
download_url=data.get('download_url'),
|
download_url=data.get('download_url'),
|
||||||
save_dir=data.get('lora_root'),
|
save_dir=data.get('lora_root'),
|
||||||
relative_path=data.get('relative_path'),
|
relative_path=data.get('relative_path'),
|
||||||
progress_callback=progress_callback # Add progress callback
|
progress_callback=progress_callback
|
||||||
)
|
)
|
||||||
|
|
||||||
if not result.get('success', False):
|
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)
|
return web.json_response(result)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error downloading LoRA: {e}")
|
error_message = str(e)
|
||||||
return web.Response(status=500, text=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:
|
async def update_settings(self, request: web.Request) -> web.Response:
|
||||||
"""Update application settings"""
|
"""Update application settings"""
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
import time
|
||||||
import logging
|
import logging
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
@@ -13,7 +14,7 @@ from ..services.recipe_scanner import RecipeScanner
|
|||||||
from ..services.lora_scanner import LoraScanner
|
from ..services.lora_scanner import LoraScanner
|
||||||
from ..config import config
|
from ..config import config
|
||||||
from ..workflow.parser import WorkflowParser
|
from ..workflow.parser import WorkflowParser
|
||||||
import time # Add this import at the top
|
from ..utils.utils import download_twitter_image
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -234,7 +235,6 @@ class RecipeRoutes:
|
|||||||
}, status=400)
|
}, status=400)
|
||||||
|
|
||||||
# Download image from URL
|
# Download image from URL
|
||||||
from ..utils.utils import download_twitter_image
|
|
||||||
temp_path = download_twitter_image(url)
|
temp_path = download_twitter_image(url)
|
||||||
|
|
||||||
if not temp_path:
|
if not temp_path:
|
||||||
|
|||||||
@@ -76,6 +76,17 @@ class CivitaiClient:
|
|||||||
headers = self._get_request_headers()
|
headers = self._get_request_headers()
|
||||||
async with session.get(url, headers=headers, allow_redirects=True) as response:
|
async with session.get(url, headers=headers, allow_redirects=True) as response:
|
||||||
if response.status != 200:
|
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}"
|
return False, f"Download failed with status {response.status}"
|
||||||
|
|
||||||
# Get filename from content-disposition header
|
# Get filename from content-disposition header
|
||||||
|
|||||||
@@ -28,6 +28,25 @@ class DownloadManager:
|
|||||||
if not version_info:
|
if not version_info:
|
||||||
return {'success': False, 'error': 'Failed to fetch model metadata'}
|
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
|
# Report initial progress
|
||||||
if progress_callback:
|
if progress_callback:
|
||||||
await progress_callback(0)
|
await progress_callback(0)
|
||||||
@@ -82,6 +101,10 @@ class DownloadManager:
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in download_from_civitai: {e}", exc_info=True)
|
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)}
|
return {'success': False, 'error': str(e)}
|
||||||
|
|
||||||
async def _execute_download(self, download_url: str, save_dir: str,
|
async def _execute_download(self, download_url: str, save_dir: str,
|
||||||
|
|||||||
@@ -111,6 +111,13 @@ class RecipeFormatParser(RecipeMetadataParser):
|
|||||||
try:
|
try:
|
||||||
civitai_info = await civitai_client.get_model_version_info(lora['modelVersionId'])
|
civitai_info = await civitai_client.get_model_version_info(lora['modelVersionId'])
|
||||||
if civitai_info and civitai_info.get("error") != "Model not found":
|
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
|
# Get thumbnail URL from first image
|
||||||
if 'images' in civitai_info and civitai_info['images']:
|
if 'images' in civitai_info and civitai_info['images']:
|
||||||
lora_entry['thumbnailUrl'] = civitai_info['images'][0].get('url', '')
|
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
|
# Check if this LoRA exists locally by SHA256 hash
|
||||||
if civitai_info and civitai_info.get("error") != "Model not found":
|
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
|
# LoRA exists on Civitai, process its information
|
||||||
if 'files' in civitai_info:
|
if 'files' in civitai_info:
|
||||||
# Find the model file (type="Model") in the files list
|
# Find the model file (type="Model") in the files list
|
||||||
@@ -427,6 +441,13 @@ class A1111MetadataParser(RecipeMetadataParser):
|
|||||||
try:
|
try:
|
||||||
civitai_info = await civitai_client.get_model_by_hash(hash_value)
|
civitai_info = await civitai_client.get_model_by_hash(hash_value)
|
||||||
if civitai_info and civitai_info.get("error") != "Model not found":
|
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
|
# Get model version ID
|
||||||
lora_entry['id'] = civitai_info.get('id', '')
|
lora_entry['id'] = civitai_info.get('id', '')
|
||||||
|
|
||||||
|
|||||||
@@ -98,6 +98,7 @@
|
|||||||
.version-info {
|
.version-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
flex-direction: row !important;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
|
|||||||
@@ -207,6 +207,15 @@
|
|||||||
border-left: 4px solid var(--lora-warning);
|
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 {
|
.lora-thumbnail {
|
||||||
width: 80px;
|
width: 80px;
|
||||||
height: 80px;
|
height: 80px;
|
||||||
@@ -297,6 +306,12 @@
|
|||||||
border-bottom: none;
|
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 {
|
.missing-badge {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -682,3 +697,39 @@
|
|||||||
min-height: 20px; /* Ensure there's always space for the error message */
|
min-height: 20px; /* Ensure there's always space for the error message */
|
||||||
font-weight: 500;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,6 +21,59 @@
|
|||||||
font-size: 0.9em;
|
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 {
|
.local-path {
|
||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@@ -120,4 +120,63 @@
|
|||||||
|
|
||||||
.tooltip:hover::after {
|
.tooltip:hover::after {
|
||||||
opacity: 1;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -130,7 +130,22 @@ export class DownloadManager {
|
|||||||
const existsLocally = version.existsLocally;
|
const existsLocally = version.existsLocally;
|
||||||
const localPath = version.localPath;
|
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 = `
|
||||||
|
<div class="early-access-badge" title="Early access required">
|
||||||
|
<i class="fas fa-clock"></i> Early Access
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(earlyAccessBadge);
|
||||||
|
|
||||||
|
// Status badge for local models
|
||||||
const localStatus = existsLocally ?
|
const localStatus = existsLocally ?
|
||||||
`<div class="local-badge">
|
`<div class="local-badge">
|
||||||
<i class="fas fa-check"></i> In Library
|
<i class="fas fa-check"></i> In Library
|
||||||
@@ -138,7 +153,9 @@ export class DownloadManager {
|
|||||||
</div>` : '';
|
</div>` : '';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="version-item ${this.currentVersion?.id === version.id ? 'selected' : ''} ${existsLocally ? 'exists-locally' : ''}"
|
<div class="version-item ${this.currentVersion?.id === version.id ? 'selected' : ''}
|
||||||
|
${existsLocally ? 'exists-locally' : ''}
|
||||||
|
${isEarlyAccess ? 'is-early-access' : ''}"
|
||||||
onclick="downloadManager.selectVersion('${version.id}')">
|
onclick="downloadManager.selectVersion('${version.id}')">
|
||||||
<div class="version-thumbnail">
|
<div class="version-thumbnail">
|
||||||
<img src="${thumbnailUrl}" alt="Version preview">
|
<img src="${thumbnailUrl}" alt="Version preview">
|
||||||
@@ -150,6 +167,7 @@ export class DownloadManager {
|
|||||||
</div>
|
</div>
|
||||||
<div class="version-info">
|
<div class="version-info">
|
||||||
${version.baseModel ? `<div class="base-model">${version.baseModel}</div>` : ''}
|
${version.baseModel ? `<div class="base-model">${version.baseModel}</div>` : ''}
|
||||||
|
${earlyAccessBadge}
|
||||||
</div>
|
</div>
|
||||||
<div class="version-meta">
|
<div class="version-meta">
|
||||||
<span><i class="fas fa-calendar"></i> ${new Date(version.createdAt).toLocaleDateString()}</span>
|
<span><i class="fas fa-calendar"></i> ${new Date(version.createdAt).toLocaleDateString()}</span>
|
||||||
|
|||||||
@@ -265,11 +265,6 @@ export class ImportManager {
|
|||||||
throw new Error('No LoRA information found in this image');
|
throw new Error('No LoRA information found in this image');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store generation parameters if available
|
|
||||||
if (this.recipeData.gen_params) {
|
|
||||||
console.log('Generation parameters found:', this.recipeData.gen_params);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find missing LoRAs
|
// Find missing LoRAs
|
||||||
this.missingLoras = this.recipeData.loras.filter(lora => !lora.existsLocally);
|
this.missingLoras = this.recipeData.loras.filter(lora => !lora.existsLocally);
|
||||||
|
|
||||||
@@ -418,6 +413,7 @@ export class ImportManager {
|
|||||||
lorasList.innerHTML = this.recipeData.loras.map(lora => {
|
lorasList.innerHTML = this.recipeData.loras.map(lora => {
|
||||||
const existsLocally = lora.existsLocally;
|
const existsLocally = lora.existsLocally;
|
||||||
const isDeleted = lora.isDeleted;
|
const isDeleted = lora.isDeleted;
|
||||||
|
const isEarlyAccess = lora.isEarlyAccess;
|
||||||
const localPath = lora.localPath || '';
|
const localPath = lora.localPath || '';
|
||||||
|
|
||||||
// Create status badge based on LoRA status
|
// Create status badge based on LoRA status
|
||||||
@@ -437,19 +433,43 @@ export class ImportManager {
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Early access badge (shown additionally with other badges)
|
||||||
|
let earlyAccessBadge = '';
|
||||||
|
if (isEarlyAccess) {
|
||||||
|
// Format the early access end date if available
|
||||||
|
let earlyAccessInfo = 'This LoRA requires early access payment to download.';
|
||||||
|
if (lora.earlyAccessEndsAt) {
|
||||||
|
try {
|
||||||
|
const endDate = new Date(lora.earlyAccessEndsAt);
|
||||||
|
const formattedDate = endDate.toLocaleDateString();
|
||||||
|
earlyAccessInfo += ` Early access ends on ${formattedDate}.`;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to format early access date', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
earlyAccessBadge = `<div class="early-access-badge">
|
||||||
|
<i class="fas fa-clock"></i> Early Access
|
||||||
|
<div class="early-access-info">${earlyAccessInfo} Verify that you have purchased early access before downloading.</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
// Format size if available
|
// Format size if available
|
||||||
const sizeDisplay = lora.size ?
|
const sizeDisplay = lora.size ?
|
||||||
`<div class="size-badge">${this.formatFileSize(lora.size)}</div>` : '';
|
`<div class="size-badge">${this.formatFileSize(lora.size)}</div>` : '';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="lora-item ${existsLocally ? 'exists-locally' : isDeleted ? 'is-deleted' : 'missing-locally'}">
|
<div class="lora-item ${existsLocally ? 'exists-locally' : isDeleted ? 'is-deleted' : 'missing-locally'} ${isEarlyAccess ? 'is-early-access' : ''}">
|
||||||
<div class="lora-thumbnail">
|
<div class="lora-thumbnail">
|
||||||
<img src="${lora.thumbnailUrl || '/loras_static/images/no-preview.png'}" alt="LoRA preview">
|
<img src="${lora.thumbnailUrl || '/loras_static/images/no-preview.png'}" alt="LoRA preview">
|
||||||
</div>
|
</div>
|
||||||
<div class="lora-content">
|
<div class="lora-content">
|
||||||
<div class="lora-header">
|
<div class="lora-header">
|
||||||
<h3>${lora.name}</h3>
|
<h3>${lora.name}</h3>
|
||||||
<div class="badge-container">${statusBadge}</div>
|
<div class="badge-container">
|
||||||
|
${statusBadge}
|
||||||
|
${earlyAccessBadge}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
${lora.version ? `<div class="lora-version">${lora.version}</div>` : ''}
|
${lora.version ? `<div class="lora-version">${lora.version}</div>` : ''}
|
||||||
<div class="lora-info">
|
<div class="lora-info">
|
||||||
@@ -463,6 +483,41 @@ export class ImportManager {
|
|||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for early access loras and show warning if any exist
|
||||||
|
const earlyAccessLoras = this.recipeData.loras.filter(lora =>
|
||||||
|
lora.isEarlyAccess && !lora.existsLocally && !lora.isDeleted);
|
||||||
|
if (earlyAccessLoras.length > 0) {
|
||||||
|
// Show a warning about early access loras
|
||||||
|
const warningMessage = `
|
||||||
|
<div class="early-access-warning">
|
||||||
|
<div class="warning-icon"><i class="fas fa-clock"></i></div>
|
||||||
|
<div class="warning-content">
|
||||||
|
<div class="warning-title">${earlyAccessLoras.length} LoRA(s) require Early Access</div>
|
||||||
|
<div class="warning-text">
|
||||||
|
These LoRAs require a payment to access. Download will fail if you haven't purchased access.
|
||||||
|
You may need to log in to your Civitai account in browser settings.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Show the warning message
|
||||||
|
const buttonsContainer = document.querySelector('#detailsStep .modal-actions');
|
||||||
|
if (buttonsContainer) {
|
||||||
|
// Remove existing warning if any
|
||||||
|
const existingWarning = document.getElementById('earlyAccessWarning');
|
||||||
|
if (existingWarning) {
|
||||||
|
existingWarning.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new warning
|
||||||
|
const warningContainer = document.createElement('div');
|
||||||
|
warningContainer.id = 'earlyAccessWarning';
|
||||||
|
warningContainer.innerHTML = warningMessage;
|
||||||
|
buttonsContainer.parentNode.insertBefore(warningContainer, buttonsContainer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update Next button state based on missing LoRAs
|
// Update Next button state based on missing LoRAs
|
||||||
this.updateNextButtonState();
|
this.updateNextButtonState();
|
||||||
}
|
}
|
||||||
@@ -581,6 +636,40 @@ export class ImportManager {
|
|||||||
// Update missing LoRAs list to exclude deleted LoRAs
|
// Update missing LoRAs list to exclude deleted LoRAs
|
||||||
this.missingLoras = this.recipeData.loras.filter(lora =>
|
this.missingLoras = this.recipeData.loras.filter(lora =>
|
||||||
!lora.existsLocally && !lora.isDeleted);
|
!lora.existsLocally && !lora.isDeleted);
|
||||||
|
|
||||||
|
// Check for early access loras and show warning if any exist
|
||||||
|
const earlyAccessLoras = this.missingLoras.filter(lora => lora.isEarlyAccess);
|
||||||
|
if (earlyAccessLoras.length > 0) {
|
||||||
|
// Show a warning about early access loras
|
||||||
|
const warningMessage = `
|
||||||
|
<div class="early-access-warning">
|
||||||
|
<div class="warning-icon"><i class="fas fa-clock"></i></div>
|
||||||
|
<div class="warning-content">
|
||||||
|
<div class="warning-title">${earlyAccessLoras.length} LoRA(s) require Early Access</div>
|
||||||
|
<div class="warning-text">
|
||||||
|
These LoRAs require a payment to access. Download will fail if you haven't purchased access.
|
||||||
|
You may need to log in to your Civitai account in browser settings.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Show the warning message
|
||||||
|
const buttonsContainer = document.querySelector('#detailsStep .modal-actions');
|
||||||
|
if (buttonsContainer) {
|
||||||
|
// Remove existing warning if any
|
||||||
|
const existingWarning = document.getElementById('earlyAccessWarning');
|
||||||
|
if (existingWarning) {
|
||||||
|
existingWarning.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new warning
|
||||||
|
const warningContainer = document.createElement('div');
|
||||||
|
warningContainer.id = 'earlyAccessWarning';
|
||||||
|
warningContainer.innerHTML = warningMessage;
|
||||||
|
buttonsContainer.parentNode.insertBefore(warningContainer, buttonsContainer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If we have downloadable missing LoRAs, go to location step
|
// If we have downloadable missing LoRAs, go to location step
|
||||||
if (this.missingLoras.length > 0) {
|
if (this.missingLoras.length > 0) {
|
||||||
@@ -646,12 +735,22 @@ export class ImportManager {
|
|||||||
missingLorasList.innerHTML = this.downloadableLoRAs.map(lora => {
|
missingLorasList.innerHTML = this.downloadableLoRAs.map(lora => {
|
||||||
const sizeDisplay = lora.size ? this.formatFileSize(lora.size) : 'Unknown size';
|
const sizeDisplay = lora.size ? this.formatFileSize(lora.size) : 'Unknown size';
|
||||||
const baseModel = lora.baseModel ? `<span class="lora-base-model">${lora.baseModel}</span>` : '';
|
const baseModel = lora.baseModel ? `<span class="lora-base-model">${lora.baseModel}</span>` : '';
|
||||||
|
const isEarlyAccess = lora.isEarlyAccess;
|
||||||
|
|
||||||
|
// Early access badge
|
||||||
|
let earlyAccessBadge = '';
|
||||||
|
if (isEarlyAccess) {
|
||||||
|
earlyAccessBadge = `<span class="early-access-badge">
|
||||||
|
<i class="fas fa-clock"></i> Early Access
|
||||||
|
</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="missing-lora-item">
|
<div class="missing-lora-item ${isEarlyAccess ? 'is-early-access' : ''}">
|
||||||
<div class="missing-lora-info">
|
<div class="missing-lora-info">
|
||||||
<div class="missing-lora-name">${lora.name}</div>
|
<div class="missing-lora-name">${lora.name}</div>
|
||||||
${baseModel}
|
${baseModel}
|
||||||
|
${earlyAccessBadge}
|
||||||
</div>
|
</div>
|
||||||
<div class="missing-lora-size">${sizeDisplay}</div>
|
<div class="missing-lora-size">${sizeDisplay}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -822,6 +921,8 @@ export class ImportManager {
|
|||||||
const updateProgress = this.loadingManager.showDownloadProgress(this.downloadableLoRAs.length);
|
const updateProgress = this.loadingManager.showDownloadProgress(this.downloadableLoRAs.length);
|
||||||
|
|
||||||
let completedDownloads = 0;
|
let completedDownloads = 0;
|
||||||
|
let failedDownloads = 0;
|
||||||
|
let earlyAccessFailures = 0;
|
||||||
let currentLoraProgress = 0;
|
let currentLoraProgress = 0;
|
||||||
|
|
||||||
// Set up progress tracking for current download
|
// Set up progress tracking for current download
|
||||||
@@ -832,7 +933,7 @@ export class ImportManager {
|
|||||||
currentLoraProgress = data.progress;
|
currentLoraProgress = data.progress;
|
||||||
|
|
||||||
// Get current LoRA name
|
// Get current LoRA name
|
||||||
const currentLora = this.downloadableLoRAs[completedDownloads];
|
const currentLora = this.downloadableLoRAs[completedDownloads + failedDownloads];
|
||||||
const loraName = currentLora ? currentLora.name : '';
|
const loraName = currentLora ? currentLora.name : '';
|
||||||
|
|
||||||
// Update progress display
|
// Update progress display
|
||||||
@@ -841,19 +942,19 @@ export class ImportManager {
|
|||||||
// Add more detailed status messages based on progress
|
// Add more detailed status messages based on progress
|
||||||
if (currentLoraProgress < 3) {
|
if (currentLoraProgress < 3) {
|
||||||
this.loadingManager.setStatus(
|
this.loadingManager.setStatus(
|
||||||
`Preparing download for LoRA ${completedDownloads+1}/${this.downloadableLoRAs.length}`
|
`Preparing download for LoRA ${completedDownloads + failedDownloads + 1}/${this.downloadableLoRAs.length}`
|
||||||
);
|
);
|
||||||
} else if (currentLoraProgress === 3) {
|
} else if (currentLoraProgress === 3) {
|
||||||
this.loadingManager.setStatus(
|
this.loadingManager.setStatus(
|
||||||
`Downloaded preview for LoRA ${completedDownloads+1}/${this.downloadableLoRAs.length}`
|
`Downloaded preview for LoRA ${completedDownloads + failedDownloads + 1}/${this.downloadableLoRAs.length}`
|
||||||
);
|
);
|
||||||
} else if (currentLoraProgress > 3 && currentLoraProgress < 100) {
|
} else if (currentLoraProgress > 3 && currentLoraProgress < 100) {
|
||||||
this.loadingManager.setStatus(
|
this.loadingManager.setStatus(
|
||||||
`Downloading LoRA ${completedDownloads+1}/${this.downloadableLoRAs.length}`
|
`Downloading LoRA ${completedDownloads + failedDownloads + 1}/${this.downloadableLoRAs.length}`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.loadingManager.setStatus(
|
this.loadingManager.setStatus(
|
||||||
`Finalizing LoRA ${completedDownloads+1}/${this.downloadableLoRAs.length}`
|
`Finalizing LoRA ${completedDownloads + failedDownloads + 1}/${this.downloadableLoRAs.length}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -884,6 +985,18 @@ export class ImportManager {
|
|||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorText = await response.text();
|
const errorText = await response.text();
|
||||||
console.error(`Failed to download LoRA ${lora.name}: ${errorText}`);
|
console.error(`Failed to download LoRA ${lora.name}: ${errorText}`);
|
||||||
|
|
||||||
|
// Check if this is an early access error (status 401 is the key indicator)
|
||||||
|
if (response.status === 401 ||
|
||||||
|
(errorText.toLowerCase().includes('early access') ||
|
||||||
|
errorText.toLowerCase().includes('purchase'))) {
|
||||||
|
earlyAccessFailures++;
|
||||||
|
this.loadingManager.setStatus(
|
||||||
|
`Failed to download ${lora.name}: Early Access required`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
failedDownloads++;
|
||||||
// Continue with next download
|
// Continue with next download
|
||||||
} else {
|
} else {
|
||||||
completedDownloads++;
|
completedDownloads++;
|
||||||
@@ -891,7 +1004,7 @@ export class ImportManager {
|
|||||||
// Update progress to show completion of current LoRA
|
// Update progress to show completion of current LoRA
|
||||||
updateProgress(100, completedDownloads, '');
|
updateProgress(100, completedDownloads, '');
|
||||||
|
|
||||||
if (completedDownloads < this.downloadableLoRAs.length) {
|
if (completedDownloads + failedDownloads < this.downloadableLoRAs.length) {
|
||||||
this.loadingManager.setStatus(
|
this.loadingManager.setStatus(
|
||||||
`Completed ${completedDownloads}/${this.downloadableLoRAs.length} LoRAs. Starting next download...`
|
`Completed ${completedDownloads}/${this.downloadableLoRAs.length} LoRAs. Starting next download...`
|
||||||
);
|
);
|
||||||
@@ -899,6 +1012,7 @@ export class ImportManager {
|
|||||||
}
|
}
|
||||||
} catch (downloadError) {
|
} catch (downloadError) {
|
||||||
console.error(`Error downloading LoRA ${lora.name}:`, downloadError);
|
console.error(`Error downloading LoRA ${lora.name}:`, downloadError);
|
||||||
|
failedDownloads++;
|
||||||
// Continue with next download
|
// Continue with next download
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -906,11 +1020,18 @@ export class ImportManager {
|
|||||||
// Close WebSocket
|
// Close WebSocket
|
||||||
ws.close();
|
ws.close();
|
||||||
|
|
||||||
// Show final completion message
|
// Show appropriate completion message based on results
|
||||||
if (completedDownloads === this.downloadableLoRAs.length) {
|
if (failedDownloads === 0) {
|
||||||
showToast(`All ${completedDownloads} LoRAs downloaded successfully`, 'success');
|
showToast(`All ${completedDownloads} LoRAs downloaded successfully`, 'success');
|
||||||
} else {
|
} else {
|
||||||
showToast(`Downloaded ${completedDownloads} of ${this.downloadableLoRAs.length} LoRAs`, 'warning');
|
if (earlyAccessFailures > 0) {
|
||||||
|
showToast(
|
||||||
|
`Downloaded ${completedDownloads} of ${this.downloadableLoRAs.length} LoRAs. ${earlyAccessFailures} failed due to Early Access restrictions.`,
|
||||||
|
'error'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
showToast(`Downloaded ${completedDownloads} of ${this.downloadableLoRAs.length} LoRAs`, 'error');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,54 @@ export function showToast(message, type = 'info') {
|
|||||||
const toast = document.createElement('div');
|
const toast = document.createElement('div');
|
||||||
toast.className = `toast toast-${type}`;
|
toast.className = `toast toast-${type}`;
|
||||||
toast.textContent = message;
|
toast.textContent = message;
|
||||||
document.body.append(toast);
|
|
||||||
|
// Get or create toast container
|
||||||
|
let toastContainer = document.querySelector('.toast-container');
|
||||||
|
if (!toastContainer) {
|
||||||
|
toastContainer = document.createElement('div');
|
||||||
|
toastContainer.className = 'toast-container';
|
||||||
|
document.body.append(toastContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
toastContainer.append(toast);
|
||||||
|
|
||||||
|
// Calculate vertical position for stacked toasts
|
||||||
|
const existingToasts = Array.from(toastContainer.querySelectorAll('.toast'));
|
||||||
|
const toastIndex = existingToasts.indexOf(toast);
|
||||||
|
const topOffset = 20; // Base offset from top
|
||||||
|
const spacing = 10; // Space between toasts
|
||||||
|
|
||||||
|
// Set position based on existing toasts
|
||||||
|
toast.style.top = `${topOffset + (toastIndex * (toast.offsetHeight || 60 + spacing))}px`;
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
toast.classList.add('show');
|
toast.classList.add('show');
|
||||||
setTimeout(() => toast.remove(), 2300);
|
|
||||||
|
// Set timeout based on type
|
||||||
|
let timeout = 2000; // Default (info)
|
||||||
|
if (type === 'warning' || type === 'error') {
|
||||||
|
timeout = 5000;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.classList.remove('show');
|
||||||
|
toast.addEventListener('transitionend', () => {
|
||||||
|
toast.remove();
|
||||||
|
|
||||||
|
// Reposition remaining toasts
|
||||||
|
if (toastContainer) {
|
||||||
|
const remainingToasts = Array.from(toastContainer.querySelectorAll('.toast'));
|
||||||
|
remainingToasts.forEach((t, index) => {
|
||||||
|
t.style.top = `${topOffset + (index * (t.offsetHeight || 60 + spacing))}px`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove container if empty
|
||||||
|
if (remainingToasts.length === 0) {
|
||||||
|
toastContainer.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, timeout);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user