mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 07:05:43 -03:00
feat: Add initialization progress WebSocket and UI components
- Implement WebSocket route for initialization progress updates - Create initialization component with progress bar and stages - Add styles for initialization UI - Update base template to include initialization component - Enhance model scanner to broadcast progress during initialization
This commit is contained in:
@@ -46,6 +46,7 @@ class ApiRoutes:
|
|||||||
app.router.add_get('/api/loras', routes.get_loras)
|
app.router.add_get('/api/loras', routes.get_loras)
|
||||||
app.router.add_post('/api/fetch-all-civitai', routes.fetch_all_civitai)
|
app.router.add_post('/api/fetch-all-civitai', routes.fetch_all_civitai)
|
||||||
app.router.add_get('/ws/fetch-progress', ws_manager.handle_connection)
|
app.router.add_get('/ws/fetch-progress', ws_manager.handle_connection)
|
||||||
|
app.router.add_get('/ws/init-progress', ws_manager.handle_init_connection) # Add new WebSocket route
|
||||||
app.router.add_get('/api/lora-roots', routes.get_lora_roots)
|
app.router.add_get('/api/lora-roots', routes.get_lora_roots)
|
||||||
app.router.add_get('/api/folders', routes.get_folders)
|
app.router.add_get('/api/folders', routes.get_folders)
|
||||||
app.router.add_get('/api/civitai/versions/{model_id}', routes.get_civitai_versions)
|
app.router.add_get('/api/civitai/versions/{model_id}', routes.get_civitai_versions)
|
||||||
|
|||||||
@@ -428,15 +428,16 @@ class CheckpointsRoutes:
|
|||||||
async def handle_checkpoints_page(self, request: web.Request) -> web.Response:
|
async def handle_checkpoints_page(self, request: web.Request) -> web.Response:
|
||||||
"""Handle GET /checkpoints request"""
|
"""Handle GET /checkpoints request"""
|
||||||
try:
|
try:
|
||||||
# 检查缓存初始化状态,根据initialize_in_background的工作方式调整判断逻辑
|
# Check if the CheckpointScanner is initializing
|
||||||
|
# It's initializing if the cache object doesn't exist yet,
|
||||||
|
# OR if the scanner explicitly says it's initializing (background task running).
|
||||||
is_initializing = (
|
is_initializing = (
|
||||||
self.scanner._cache is None or
|
self.scanner._cache is None or
|
||||||
len(self.scanner._cache.raw_data) == 0 or
|
(hasattr(self.scanner, '_is_initializing') and self.scanner._is_initializing)
|
||||||
hasattr(self.scanner, '_is_initializing') and self.scanner._is_initializing
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if is_initializing:
|
if is_initializing:
|
||||||
# 如果正在初始化,返回一个只包含加载提示的页面
|
# If still initializing, return loading page
|
||||||
template = self.template_env.get_template('checkpoints.html')
|
template = self.template_env.get_template('checkpoints.html')
|
||||||
rendered = template.render(
|
rendered = template.render(
|
||||||
folders=[], # 空文件夹列表
|
folders=[], # 空文件夹列表
|
||||||
|
|||||||
@@ -67,10 +67,11 @@ class LoraRoutes:
|
|||||||
await self.init_services()
|
await self.init_services()
|
||||||
|
|
||||||
# Check if the LoraScanner is initializing
|
# Check if the LoraScanner is initializing
|
||||||
|
# It's initializing if the cache object doesn't exist yet,
|
||||||
|
# OR if the scanner explicitly says it's initializing (background task running).
|
||||||
is_initializing = (
|
is_initializing = (
|
||||||
self.scanner._cache is None or
|
self.scanner._cache is None or
|
||||||
len(self.scanner._cache.raw_data) == 0 or
|
(hasattr(self.scanner, '_is_initializing') and self.scanner._is_initializing)
|
||||||
hasattr(self.scanner, '_is_initializing') and self.scanner._is_initializing
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if is_initializing:
|
if is_initializing:
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from .model_cache import ModelCache
|
|||||||
from .model_hash_index import ModelHashIndex
|
from .model_hash_index import ModelHashIndex
|
||||||
from ..utils.constants import PREVIEW_EXTENSIONS
|
from ..utils.constants import PREVIEW_EXTENSIONS
|
||||||
from .service_registry import ServiceRegistry
|
from .service_registry import ServiceRegistry
|
||||||
|
from .websocket_manager import ws_manager
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -61,21 +62,99 @@ class ModelScanner:
|
|||||||
# Set initializing flag to true
|
# Set initializing flag to true
|
||||||
self._is_initializing = True
|
self._is_initializing = True
|
||||||
|
|
||||||
start_time = time.time()
|
# First, count all model files to track progress
|
||||||
# Use thread pool to execute CPU-intensive operations
|
await ws_manager.broadcast_init_progress({
|
||||||
|
'stage': 'scan_folders',
|
||||||
|
'progress': 0,
|
||||||
|
'details': f"Scanning {self.model_type} folders..."
|
||||||
|
})
|
||||||
|
|
||||||
|
# Count files in a separate thread to avoid blocking
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
|
total_files = await loop.run_in_executor(
|
||||||
|
None, # Use default thread pool
|
||||||
|
self._count_model_files # Run file counting in thread
|
||||||
|
)
|
||||||
|
|
||||||
|
await ws_manager.broadcast_init_progress({
|
||||||
|
'stage': 'count_models',
|
||||||
|
'progress': 1, # Changed from 10 to 1
|
||||||
|
'details': f"Found {total_files} {self.model_type} files"
|
||||||
|
})
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
# Use thread pool to execute CPU-intensive operations with progress reporting
|
||||||
await loop.run_in_executor(
|
await loop.run_in_executor(
|
||||||
None, # Use default thread pool
|
None, # Use default thread pool
|
||||||
self._initialize_cache_sync # Run synchronous version in thread
|
self._initialize_cache_sync, # Run synchronous version in thread
|
||||||
|
total_files # Pass the total file count for progress reporting
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Send final progress update
|
||||||
|
await ws_manager.broadcast_init_progress({
|
||||||
|
'stage': 'finalizing',
|
||||||
|
'progress': 99, # Changed from 95 to 99
|
||||||
|
'details': f"Finalizing {self.model_type} cache..."
|
||||||
|
})
|
||||||
|
|
||||||
logger.info(f"{self.model_type.capitalize()} cache initialized in {time.time() - start_time:.2f} seconds. Found {len(self._cache.raw_data)} models")
|
logger.info(f"{self.model_type.capitalize()} cache initialized in {time.time() - start_time:.2f} seconds. Found {len(self._cache.raw_data)} models")
|
||||||
|
|
||||||
|
# Send completion message
|
||||||
|
await asyncio.sleep(0.5) # Small delay to ensure final progress message is sent
|
||||||
|
await ws_manager.broadcast_init_progress({
|
||||||
|
'stage': 'finalizing',
|
||||||
|
'progress': 100,
|
||||||
|
'status': 'complete',
|
||||||
|
'details': f"Completed! Found {len(self._cache.raw_data)} {self.model_type} files."
|
||||||
|
})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"{self.model_type.capitalize()} Scanner: Error initializing cache in background: {e}")
|
logger.error(f"{self.model_type.capitalize()} Scanner: Error initializing cache in background: {e}")
|
||||||
finally:
|
finally:
|
||||||
# Always clear the initializing flag when done
|
# Always clear the initializing flag when done
|
||||||
self._is_initializing = False
|
self._is_initializing = False
|
||||||
|
|
||||||
def _initialize_cache_sync(self):
|
def _count_model_files(self) -> int:
|
||||||
|
"""Count all model files with supported extensions in all roots
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: Total number of model files found
|
||||||
|
"""
|
||||||
|
total_files = 0
|
||||||
|
visited_real_paths = set()
|
||||||
|
|
||||||
|
for root_path in self.get_model_roots():
|
||||||
|
if not os.path.exists(root_path):
|
||||||
|
continue
|
||||||
|
|
||||||
|
def count_recursive(path):
|
||||||
|
nonlocal total_files
|
||||||
|
try:
|
||||||
|
real_path = os.path.realpath(path)
|
||||||
|
if real_path in visited_real_paths:
|
||||||
|
return
|
||||||
|
visited_real_paths.add(real_path)
|
||||||
|
|
||||||
|
with os.scandir(path) as it:
|
||||||
|
for entry in it:
|
||||||
|
try:
|
||||||
|
if entry.is_file(follow_symlinks=True):
|
||||||
|
ext = os.path.splitext(entry.name)[1].lower()
|
||||||
|
if ext in self.file_extensions:
|
||||||
|
total_files += 1
|
||||||
|
elif entry.is_dir(follow_symlinks=True):
|
||||||
|
count_recursive(entry.path)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error counting files in entry {entry.path}: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error counting files in {path}: {e}")
|
||||||
|
|
||||||
|
count_recursive(root_path)
|
||||||
|
|
||||||
|
return total_files
|
||||||
|
|
||||||
|
def _initialize_cache_sync(self, total_files=0):
|
||||||
"""Synchronous version of cache initialization for thread pool execution"""
|
"""Synchronous version of cache initialization for thread pool execution"""
|
||||||
try:
|
try:
|
||||||
# Create a new event loop for this thread
|
# Create a new event loop for this thread
|
||||||
@@ -84,8 +163,83 @@ class ModelScanner:
|
|||||||
|
|
||||||
# Create a synchronous method to bypass the async lock
|
# Create a synchronous method to bypass the async lock
|
||||||
def sync_initialize_cache():
|
def sync_initialize_cache():
|
||||||
# Directly call the scan method to avoid lock issues
|
# Track progress
|
||||||
raw_data = loop.run_until_complete(self.scan_all_models())
|
processed_files = 0
|
||||||
|
last_progress_time = time.time()
|
||||||
|
last_progress_percent = 0
|
||||||
|
|
||||||
|
# We need a wrapper around scan_all_models to track progress
|
||||||
|
# This is a local function that will run in our thread's event loop
|
||||||
|
async def scan_with_progress():
|
||||||
|
nonlocal processed_files, last_progress_time, last_progress_percent
|
||||||
|
|
||||||
|
# For storing raw model data
|
||||||
|
all_models = []
|
||||||
|
|
||||||
|
# Process each model root
|
||||||
|
for root_path in self.get_model_roots():
|
||||||
|
if not os.path.exists(root_path):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Track visited paths to avoid symlink loops
|
||||||
|
visited_paths = set()
|
||||||
|
|
||||||
|
# Recursively process directory
|
||||||
|
async def scan_dir_with_progress(path):
|
||||||
|
nonlocal processed_files, last_progress_time, last_progress_percent
|
||||||
|
|
||||||
|
try:
|
||||||
|
real_path = os.path.realpath(path)
|
||||||
|
if real_path in visited_paths:
|
||||||
|
return
|
||||||
|
visited_paths.add(real_path)
|
||||||
|
|
||||||
|
with os.scandir(path) as it:
|
||||||
|
entries = list(it)
|
||||||
|
for entry in entries:
|
||||||
|
try:
|
||||||
|
if entry.is_file(follow_symlinks=True):
|
||||||
|
ext = os.path.splitext(entry.name)[1].lower()
|
||||||
|
if ext in self.file_extensions:
|
||||||
|
file_path = entry.path.replace(os.sep, "/")
|
||||||
|
result = await self._process_model_file(file_path, root_path)
|
||||||
|
if result:
|
||||||
|
all_models.append(result)
|
||||||
|
|
||||||
|
# Update progress counter
|
||||||
|
processed_files += 1
|
||||||
|
|
||||||
|
# Update progress periodically (not every file to avoid excessive updates)
|
||||||
|
current_time = time.time()
|
||||||
|
if total_files > 0 and (current_time - last_progress_time > 0.5 or processed_files == total_files):
|
||||||
|
# Adjusted progress calculation
|
||||||
|
progress_percent = min(99, int(1 + (processed_files / total_files) * 98))
|
||||||
|
if progress_percent > last_progress_percent:
|
||||||
|
last_progress_percent = progress_percent
|
||||||
|
last_progress_time = current_time
|
||||||
|
|
||||||
|
# Send progress update through websocket
|
||||||
|
await ws_manager.broadcast_init_progress({
|
||||||
|
'stage': 'process_models',
|
||||||
|
'progress': progress_percent,
|
||||||
|
'details': f"Processing {self.model_type} files: {processed_files}/{total_files}"
|
||||||
|
})
|
||||||
|
|
||||||
|
elif entry.is_dir(follow_symlinks=True):
|
||||||
|
await scan_dir_with_progress(entry.path)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error processing entry {entry.path}: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error scanning {path}: {e}")
|
||||||
|
|
||||||
|
# Process the root path
|
||||||
|
await scan_dir_with_progress(root_path)
|
||||||
|
|
||||||
|
return all_models
|
||||||
|
|
||||||
|
# Run the progress-tracking scan function
|
||||||
|
raw_data = loop.run_until_complete(scan_with_progress())
|
||||||
|
|
||||||
# Update hash index and tags count
|
# Update hash index and tags count
|
||||||
for model_data in raw_data:
|
for model_data in raw_data:
|
||||||
@@ -136,6 +290,7 @@ class ModelScanner:
|
|||||||
|
|
||||||
async def _initialize_cache(self) -> None:
|
async def _initialize_cache(self) -> None:
|
||||||
"""Initialize or refresh the cache"""
|
"""Initialize or refresh the cache"""
|
||||||
|
self._is_initializing = True # Set flag
|
||||||
try:
|
try:
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
# Clear existing hash index
|
# Clear existing hash index
|
||||||
@@ -171,15 +326,20 @@ class ModelScanner:
|
|||||||
logger.info(f"{self.model_type.capitalize()} Scanner: Cache initialization completed in {time.time() - start_time:.2f} seconds, found {len(raw_data)} models")
|
logger.info(f"{self.model_type.capitalize()} Scanner: Cache initialization completed in {time.time() - start_time:.2f} seconds, found {len(raw_data)} models")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"{self.model_type.capitalize()} Scanner: Error initializing cache: {e}")
|
logger.error(f"{self.model_type.capitalize()} Scanner: Error initializing cache: {e}")
|
||||||
self._cache = ModelCache(
|
# Ensure cache is at least an empty structure on error
|
||||||
raw_data=[],
|
if self._cache is None:
|
||||||
sorted_by_name=[],
|
self._cache = ModelCache(
|
||||||
sorted_by_date=[],
|
raw_data=[],
|
||||||
folders=[]
|
sorted_by_name=[],
|
||||||
)
|
sorted_by_date=[],
|
||||||
|
folders=[]
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
self._is_initializing = False # Unset flag
|
||||||
|
|
||||||
async def _reconcile_cache(self) -> None:
|
async def _reconcile_cache(self) -> None:
|
||||||
"""Fast cache reconciliation - only process differences between cache and filesystem"""
|
"""Fast cache reconciliation - only process differences between cache and filesystem"""
|
||||||
|
self._is_initializing = True # Set flag for reconciliation duration
|
||||||
try:
|
try:
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
logger.info(f"{self.model_type.capitalize()} Scanner: Starting fast cache reconciliation...")
|
logger.info(f"{self.model_type.capitalize()} Scanner: Starting fast cache reconciliation...")
|
||||||
@@ -306,6 +466,8 @@ class ModelScanner:
|
|||||||
logger.info(f"{self.model_type.capitalize()} Scanner: Cache reconciliation completed in {time.time() - start_time:.2f} seconds. Added {total_added}, removed {total_removed} models.")
|
logger.info(f"{self.model_type.capitalize()} Scanner: Cache reconciliation completed in {time.time() - start_time:.2f} seconds. Added {total_added}, removed {total_removed} models.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"{self.model_type.capitalize()} Scanner: Error reconciling cache: {e}", exc_info=True)
|
logger.error(f"{self.model_type.capitalize()} Scanner: Error reconciling cache: {e}", exc_info=True)
|
||||||
|
finally:
|
||||||
|
self._is_initializing = False # Unset flag
|
||||||
|
|
||||||
# These methods should be implemented in child classes
|
# These methods should be implemented in child classes
|
||||||
async def scan_all_models(self) -> List[Dict]:
|
async def scan_all_models(self) -> List[Dict]:
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class WebSocketManager:
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._websockets: Set[web.WebSocketResponse] = set()
|
self._websockets: Set[web.WebSocketResponse] = set()
|
||||||
|
self._init_websockets: Set[web.WebSocketResponse] = set() # New set for initialization progress clients
|
||||||
|
|
||||||
async def handle_connection(self, request: web.Request) -> web.WebSocketResponse:
|
async def handle_connection(self, request: web.Request) -> web.WebSocketResponse:
|
||||||
"""Handle new WebSocket connection"""
|
"""Handle new WebSocket connection"""
|
||||||
@@ -23,6 +24,20 @@ class WebSocketManager:
|
|||||||
finally:
|
finally:
|
||||||
self._websockets.discard(ws)
|
self._websockets.discard(ws)
|
||||||
return ws
|
return ws
|
||||||
|
|
||||||
|
async def handle_init_connection(self, request: web.Request) -> web.WebSocketResponse:
|
||||||
|
"""Handle new WebSocket connection for initialization progress"""
|
||||||
|
ws = web.WebSocketResponse()
|
||||||
|
await ws.prepare(request)
|
||||||
|
self._init_websockets.add(ws)
|
||||||
|
|
||||||
|
try:
|
||||||
|
async for msg in ws:
|
||||||
|
if msg.type == web.WSMsgType.ERROR:
|
||||||
|
logger.error(f'Init WebSocket error: {ws.exception()}')
|
||||||
|
finally:
|
||||||
|
self._init_websockets.discard(ws)
|
||||||
|
return ws
|
||||||
|
|
||||||
async def broadcast(self, data: Dict):
|
async def broadcast(self, data: Dict):
|
||||||
"""Broadcast message to all connected clients"""
|
"""Broadcast message to all connected clients"""
|
||||||
@@ -34,10 +49,25 @@ class WebSocketManager:
|
|||||||
await ws.send_json(data)
|
await ws.send_json(data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error sending progress: {e}")
|
logger.error(f"Error sending progress: {e}")
|
||||||
|
|
||||||
|
async def broadcast_init_progress(self, data: Dict):
|
||||||
|
"""Broadcast initialization progress to connected clients"""
|
||||||
|
if not self._init_websockets:
|
||||||
|
return
|
||||||
|
|
||||||
|
for ws in self._init_websockets:
|
||||||
|
try:
|
||||||
|
await ws.send_json(data)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error sending initialization progress: {e}")
|
||||||
|
|
||||||
def get_connected_clients_count(self) -> int:
|
def get_connected_clients_count(self) -> int:
|
||||||
"""Get number of connected clients"""
|
"""Get number of connected clients"""
|
||||||
return len(self._websockets)
|
return len(self._websockets)
|
||||||
|
|
||||||
|
def get_init_clients_count(self) -> int:
|
||||||
|
"""Get number of initialization progress clients"""
|
||||||
|
return len(self._init_websockets)
|
||||||
|
|
||||||
# Global instance
|
# Global instance
|
||||||
ws_manager = WebSocketManager()
|
ws_manager = WebSocketManager()
|
||||||
259
static/css/components/initialization.css
Normal file
259
static/css/components/initialization.css
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
/* Initialization Component Styles */
|
||||||
|
|
||||||
|
.initialization-container {
|
||||||
|
width: 100%;
|
||||||
|
padding: var(--space-3);
|
||||||
|
background: var(--lora-surface);
|
||||||
|
border-radius: var(--border-radius-base);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
animation: fadeIn 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.initialization-content {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.initialization-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.initialization-header h2 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
margin-bottom: var(--space-1);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.init-subtitle {
|
||||||
|
color: var(--text-color);
|
||||||
|
opacity: 0.8;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Progress Bar Styles */
|
||||||
|
.initialization-progress {
|
||||||
|
margin-bottom: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
background-color: var(--lora-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, var(--lora-accent) 0%, rgba(var(--lora-accent), 0.8) 100%);
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
width: 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-details {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stages Styles */
|
||||||
|
.initialization-stages {
|
||||||
|
margin-bottom: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: var(--space-2);
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
|
margin-bottom: var(--space-1);
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-item.active {
|
||||||
|
background-color: rgba(var(--lora-accent), 0.1);
|
||||||
|
border-color: var(--lora-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-item.completed {
|
||||||
|
background-color: rgba(0, 150, 0, 0.05);
|
||||||
|
border-color: rgba(0, 150, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: var(--lora-border);
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-item.active .stage-icon {
|
||||||
|
background: var(--lora-accent);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-item.completed .stage-icon {
|
||||||
|
background: rgb(0, 150, 0);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-content h4 {
|
||||||
|
margin: 0 0 5px 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-details {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-color);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-status.pending {
|
||||||
|
color: var(--text-color);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-status.in-progress {
|
||||||
|
color: var(--lora-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-status.completed {
|
||||||
|
color: rgb(0, 150, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tips Styles */
|
||||||
|
.initialization-tips {
|
||||||
|
background: rgba(var(--lora-accent), 0.05);
|
||||||
|
border-radius: var(--border-radius-base);
|
||||||
|
padding: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: var(--space-1);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips-header i {
|
||||||
|
margin-right: 8px;
|
||||||
|
color: var(--lora-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips-header h3 {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip-carousel {
|
||||||
|
position: relative;
|
||||||
|
height: 60px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip-item {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.5s ease;
|
||||||
|
padding: 0 var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip-item.active {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip-item p {
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip-navigation {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--lora-border);
|
||||||
|
margin: 0 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip-dot.active {
|
||||||
|
background-color: var(--lora-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Different stage status animations */
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-item.active .stage-icon i {
|
||||||
|
animation: pulse 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Adjustments */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.initialization-container {
|
||||||
|
padding: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-item {
|
||||||
|
padding: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
min-width: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
@import 'components/bulk.css';
|
@import 'components/bulk.css';
|
||||||
@import 'components/shared.css';
|
@import 'components/shared.css';
|
||||||
@import 'components/filter-indicator.css';
|
@import 'components/filter-indicator.css';
|
||||||
|
@import 'components/initialization.css';
|
||||||
|
|
||||||
.initialization-notice {
|
.initialization-notice {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
404
static/js/components/initialization.js
Normal file
404
static/js/components/initialization.js
Normal file
@@ -0,0 +1,404 @@
|
|||||||
|
/**
|
||||||
|
* Initialization Component
|
||||||
|
* Manages the display of initialization progress and status
|
||||||
|
*/
|
||||||
|
|
||||||
|
class InitializationManager {
|
||||||
|
constructor() {
|
||||||
|
this.currentTipIndex = 0;
|
||||||
|
this.tipInterval = null;
|
||||||
|
this.websocket = null;
|
||||||
|
this.currentStage = null;
|
||||||
|
this.progress = 0;
|
||||||
|
this.stages = [
|
||||||
|
{ id: 'stageScanFolders', name: 'scan_folders' },
|
||||||
|
{ id: 'stageCountModels', name: 'count_models' },
|
||||||
|
{ id: 'stageProcessModels', name: 'process_models' },
|
||||||
|
{ id: 'stageFinalizing', name: 'finalizing' }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the component
|
||||||
|
*/
|
||||||
|
initialize() {
|
||||||
|
// Setup the tip carousel
|
||||||
|
this.setupTipCarousel();
|
||||||
|
|
||||||
|
// Connect to WebSocket for progress updates
|
||||||
|
this.connectWebSocket();
|
||||||
|
|
||||||
|
// Add event listeners for tip navigation
|
||||||
|
this.setupTipNavigation();
|
||||||
|
|
||||||
|
// Show first tip as active
|
||||||
|
document.querySelector('.tip-item').classList.add('active');
|
||||||
|
|
||||||
|
// Set the first stage as active
|
||||||
|
this.updateStage('scan_folders');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect to WebSocket for initialization progress updates
|
||||||
|
*/
|
||||||
|
connectWebSocket() {
|
||||||
|
try {
|
||||||
|
const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||||
|
this.websocket = new WebSocket(`${wsProtocol}${window.location.host}/ws/init-progress`);
|
||||||
|
|
||||||
|
this.websocket.onopen = () => {
|
||||||
|
console.log('Connected to initialization progress WebSocket');
|
||||||
|
};
|
||||||
|
|
||||||
|
this.websocket.onmessage = (event) => {
|
||||||
|
this.handleProgressUpdate(JSON.parse(event.data));
|
||||||
|
};
|
||||||
|
|
||||||
|
this.websocket.onerror = (error) => {
|
||||||
|
console.error('WebSocket error:', error);
|
||||||
|
// Fall back to polling if WebSocket fails
|
||||||
|
this.fallbackToPolling();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.websocket.onclose = () => {
|
||||||
|
console.log('WebSocket connection closed');
|
||||||
|
// Check if we need to fall back to polling
|
||||||
|
if (!this.pollingActive) {
|
||||||
|
this.fallbackToPolling();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to connect to WebSocket:', error);
|
||||||
|
this.fallbackToPolling();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fall back to polling if WebSocket connection fails
|
||||||
|
*/
|
||||||
|
fallbackToPolling() {
|
||||||
|
this.pollingActive = true;
|
||||||
|
this.pollProgress();
|
||||||
|
|
||||||
|
// Set a simulated progress that moves forward slowly
|
||||||
|
// This gives users feedback even if the backend isn't providing updates
|
||||||
|
let simulatedProgress = 0;
|
||||||
|
const simulateInterval = setInterval(() => {
|
||||||
|
simulatedProgress += 0.5;
|
||||||
|
if (simulatedProgress > 95) {
|
||||||
|
clearInterval(simulateInterval);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only use simulated progress if we haven't received a real update
|
||||||
|
if (this.progress < simulatedProgress) {
|
||||||
|
this.updateProgress(simulatedProgress);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Poll for progress updates from the server
|
||||||
|
*/
|
||||||
|
pollProgress() {
|
||||||
|
const checkProgress = () => {
|
||||||
|
fetch('/api/init-status')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
this.handleProgressUpdate(data);
|
||||||
|
|
||||||
|
// If initialization is complete, stop polling
|
||||||
|
if (data.status !== 'complete') {
|
||||||
|
setTimeout(checkProgress, 2000);
|
||||||
|
} else {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error polling for progress:', error);
|
||||||
|
setTimeout(checkProgress, 3000); // Try again after a longer delay
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
checkProgress();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle progress updates from WebSocket or polling
|
||||||
|
*/
|
||||||
|
handleProgressUpdate(data) {
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
|
// Update progress percentage
|
||||||
|
if (data.progress !== undefined) {
|
||||||
|
this.updateProgress(data.progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update current stage
|
||||||
|
if (data.stage) {
|
||||||
|
this.updateStage(data.stage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update stage-specific details
|
||||||
|
if (data.details) {
|
||||||
|
this.updateStageDetails(data.stage, data.details);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If initialization is complete, reload the page
|
||||||
|
if (data.status === 'complete') {
|
||||||
|
this.showCompletionMessage();
|
||||||
|
|
||||||
|
// Give the user a moment to see the completion message
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the progress bar and percentage
|
||||||
|
*/
|
||||||
|
updateProgress(progress) {
|
||||||
|
this.progress = progress;
|
||||||
|
const progressBar = document.getElementById('initProgressBar');
|
||||||
|
const progressPercentage = document.getElementById('progressPercentage');
|
||||||
|
|
||||||
|
if (progressBar && progressPercentage) {
|
||||||
|
progressBar.style.width = `${progress}%`;
|
||||||
|
progressPercentage.textContent = `${Math.round(progress)}%`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the current stage
|
||||||
|
*/
|
||||||
|
updateStage(stageName) {
|
||||||
|
// Mark the previous stage as completed if it exists
|
||||||
|
if (this.currentStage) {
|
||||||
|
const previousStageElement = document.getElementById(this.currentStage);
|
||||||
|
if (previousStageElement) {
|
||||||
|
previousStageElement.classList.remove('active');
|
||||||
|
previousStageElement.classList.add('completed');
|
||||||
|
|
||||||
|
// Update the stage status icon to completed
|
||||||
|
const statusElement = previousStageElement.querySelector('.stage-status');
|
||||||
|
if (statusElement) {
|
||||||
|
statusElement.className = 'stage-status completed';
|
||||||
|
statusElement.innerHTML = '<i class="fas fa-check"></i>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find and activate the new current stage
|
||||||
|
const stageInfo = this.stages.find(s => s.name === stageName);
|
||||||
|
if (stageInfo) {
|
||||||
|
this.currentStage = stageInfo.id;
|
||||||
|
const currentStageElement = document.getElementById(stageInfo.id);
|
||||||
|
|
||||||
|
if (currentStageElement) {
|
||||||
|
currentStageElement.classList.add('active');
|
||||||
|
|
||||||
|
// Update the stage status icon to in-progress
|
||||||
|
const statusElement = currentStageElement.querySelector('.stage-status');
|
||||||
|
if (statusElement) {
|
||||||
|
statusElement.className = 'stage-status in-progress';
|
||||||
|
statusElement.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the progress status message
|
||||||
|
const progressStatus = document.getElementById('progressStatus');
|
||||||
|
if (progressStatus) {
|
||||||
|
progressStatus.textContent = `${this.stageNameToDisplay(stageName)}...`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert stage name to display text
|
||||||
|
*/
|
||||||
|
stageNameToDisplay(stageName) {
|
||||||
|
switch (stageName) {
|
||||||
|
case 'scan_folders':
|
||||||
|
return 'Scanning folders';
|
||||||
|
case 'count_models':
|
||||||
|
return 'Counting models';
|
||||||
|
case 'process_models':
|
||||||
|
return 'Processing models';
|
||||||
|
case 'finalizing':
|
||||||
|
return 'Finalizing';
|
||||||
|
default:
|
||||||
|
return 'Initializing';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update stage-specific details
|
||||||
|
*/
|
||||||
|
updateStageDetails(stageName, details) {
|
||||||
|
const detailsMap = {
|
||||||
|
'scan_folders': 'scanFoldersDetails',
|
||||||
|
'count_models': 'countModelsDetails',
|
||||||
|
'process_models': 'processModelsDetails',
|
||||||
|
'finalizing': 'finalizingDetails'
|
||||||
|
};
|
||||||
|
|
||||||
|
const detailsElementId = detailsMap[stageName];
|
||||||
|
if (detailsElementId) {
|
||||||
|
const detailsElement = document.getElementById(detailsElementId);
|
||||||
|
if (detailsElement && details) {
|
||||||
|
detailsElement.textContent = details;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup the tip carousel to rotate through tips
|
||||||
|
*/
|
||||||
|
setupTipCarousel() {
|
||||||
|
const tipItems = document.querySelectorAll('.tip-item');
|
||||||
|
if (tipItems.length === 0) return;
|
||||||
|
|
||||||
|
// Show the first tip
|
||||||
|
tipItems[0].classList.add('active');
|
||||||
|
|
||||||
|
// Set up automatic rotation
|
||||||
|
this.tipInterval = setInterval(() => {
|
||||||
|
this.showNextTip();
|
||||||
|
}, 8000); // Change tip every 8 seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup tip navigation dots
|
||||||
|
*/
|
||||||
|
setupTipNavigation() {
|
||||||
|
const tipDots = document.querySelectorAll('.tip-dot');
|
||||||
|
|
||||||
|
tipDots.forEach((dot, index) => {
|
||||||
|
dot.addEventListener('click', () => {
|
||||||
|
this.showTipByIndex(index);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the next tip in the carousel
|
||||||
|
*/
|
||||||
|
showNextTip() {
|
||||||
|
const tipItems = document.querySelectorAll('.tip-item');
|
||||||
|
const tipDots = document.querySelectorAll('.tip-dot');
|
||||||
|
|
||||||
|
if (tipItems.length === 0) return;
|
||||||
|
|
||||||
|
// Hide current tip
|
||||||
|
tipItems[this.currentTipIndex].classList.remove('active');
|
||||||
|
tipDots[this.currentTipIndex].classList.remove('active');
|
||||||
|
|
||||||
|
// Calculate next index
|
||||||
|
this.currentTipIndex = (this.currentTipIndex + 1) % tipItems.length;
|
||||||
|
|
||||||
|
// Show next tip
|
||||||
|
tipItems[this.currentTipIndex].classList.add('active');
|
||||||
|
tipDots[this.currentTipIndex].classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a specific tip by index
|
||||||
|
*/
|
||||||
|
showTipByIndex(index) {
|
||||||
|
const tipItems = document.querySelectorAll('.tip-item');
|
||||||
|
const tipDots = document.querySelectorAll('.tip-dot');
|
||||||
|
|
||||||
|
if (index >= tipItems.length || index < 0) return;
|
||||||
|
|
||||||
|
// Hide current tip
|
||||||
|
tipItems[this.currentTipIndex].classList.remove('active');
|
||||||
|
tipDots[this.currentTipIndex].classList.remove('active');
|
||||||
|
|
||||||
|
// Update index and show selected tip
|
||||||
|
this.currentTipIndex = index;
|
||||||
|
|
||||||
|
// Show selected tip
|
||||||
|
tipItems[this.currentTipIndex].classList.add('active');
|
||||||
|
tipDots[this.currentTipIndex].classList.add('active');
|
||||||
|
|
||||||
|
// Reset interval to prevent quick tip change
|
||||||
|
if (this.tipInterval) {
|
||||||
|
clearInterval(this.tipInterval);
|
||||||
|
this.tipInterval = setInterval(() => {
|
||||||
|
this.showNextTip();
|
||||||
|
}, 8000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show completion message
|
||||||
|
*/
|
||||||
|
showCompletionMessage() {
|
||||||
|
// Mark all stages as completed
|
||||||
|
this.stages.forEach(stage => {
|
||||||
|
const stageElement = document.getElementById(stage.id);
|
||||||
|
if (stageElement) {
|
||||||
|
stageElement.classList.remove('active');
|
||||||
|
stageElement.classList.add('completed');
|
||||||
|
|
||||||
|
const statusElement = stageElement.querySelector('.stage-status');
|
||||||
|
if (statusElement) {
|
||||||
|
statusElement.className = 'stage-status completed';
|
||||||
|
statusElement.innerHTML = '<i class="fas fa-check"></i>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update progress to 100%
|
||||||
|
this.updateProgress(100);
|
||||||
|
|
||||||
|
// Update status message
|
||||||
|
const progressStatus = document.getElementById('progressStatus');
|
||||||
|
if (progressStatus) {
|
||||||
|
progressStatus.textContent = 'Initialization complete!';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update title and subtitle
|
||||||
|
const initTitle = document.getElementById('initTitle');
|
||||||
|
const initSubtitle = document.getElementById('initSubtitle');
|
||||||
|
|
||||||
|
if (initTitle) {
|
||||||
|
initTitle.textContent = 'Initialization Complete';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (initSubtitle) {
|
||||||
|
initSubtitle.textContent = 'Reloading page...';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up resources when the component is destroyed
|
||||||
|
*/
|
||||||
|
cleanup() {
|
||||||
|
if (this.tipInterval) {
|
||||||
|
clearInterval(this.tipInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {
|
||||||
|
this.websocket.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and export the initialization manager
|
||||||
|
export const initManager = new InitializationManager();
|
||||||
|
|
||||||
|
// Initialize the component when the DOM is loaded
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// Only initialize if we're in initialization mode
|
||||||
|
const initContainer = document.getElementById('initializationContainer');
|
||||||
|
if (initContainer) {
|
||||||
|
initManager.initialize();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean up when the page is unloaded
|
||||||
|
window.addEventListener('beforeunload', () => {
|
||||||
|
initManager.cleanup();
|
||||||
|
});
|
||||||
@@ -37,49 +37,6 @@
|
|||||||
<link rel="preconnect" href="https://civitai.com">
|
<link rel="preconnect" href="https://civitai.com">
|
||||||
<link rel="preconnect" href="https://cdnjs.cloudflare.com">
|
<link rel="preconnect" href="https://cdnjs.cloudflare.com">
|
||||||
|
|
||||||
<!-- Add styles for initialization notice -->
|
|
||||||
{% if is_initializing %}
|
|
||||||
<style>
|
|
||||||
.initialization-notice {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-color: rgba(0, 0, 0, 0.85);
|
|
||||||
z-index: 9999;
|
|
||||||
margin-top: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
.notice-content {
|
|
||||||
background-color: rgba(30, 30, 30, 0.9);
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 30px;
|
|
||||||
text-align: center;
|
|
||||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
|
|
||||||
max-width: 500px;
|
|
||||||
width: 80%;
|
|
||||||
}
|
|
||||||
.loading-spinner {
|
|
||||||
border: 5px solid rgba(255, 255, 255, 0.3);
|
|
||||||
border-radius: 50%;
|
|
||||||
border-top: 5px solid #fff;
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
margin: 0 auto 20px;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
@keyframes spin {
|
|
||||||
0% { transform: rotate(0deg); }
|
|
||||||
100% { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// 计算滚动条宽度并设置CSS变量
|
// 计算滚动条宽度并设置CSS变量
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
@@ -95,15 +52,7 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body data-page="{% block page_id %}base{% endblock %}">
|
<body data-page="{% block page_id %}base{% endblock %}">
|
||||||
{% if is_initializing %}
|
<!-- Header is always visible, even during initialization -->
|
||||||
<div class="initialization-notice">
|
|
||||||
<div class="notice-content">
|
|
||||||
<div class="loading-spinner"></div>
|
|
||||||
<h2>{% block init_title %}Initializing{% endblock %}</h2>
|
|
||||||
<p>{% block init_message %}Scanning and building cache. This may take a few minutes...{% endblock %}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
{% include 'components/header.html' %}
|
{% include 'components/header.html' %}
|
||||||
|
|
||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
@@ -113,37 +62,23 @@
|
|||||||
{% block additional_components %}{% endblock %}
|
{% block additional_components %}{% endblock %}
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
{% block content %}{% endblock %}
|
{% if is_initializing %}
|
||||||
|
<!-- Show initialization component when initializing -->
|
||||||
|
{% include 'components/initialization.html' %}
|
||||||
|
{% else %}
|
||||||
|
<!-- Show regular content when not initializing -->
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% block overlay %}{% endblock %}
|
{% block overlay %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% block main_script %}{% endblock %}
|
|
||||||
|
|
||||||
{% if is_initializing %}
|
{% if is_initializing %}
|
||||||
<script>
|
<!-- Load initialization JavaScript -->
|
||||||
// 检查初始化状态并设置自动刷新
|
<script type="module" src="/loras_static/js/components/initialization.js"></script>
|
||||||
async function checkInitStatus() {
|
{% else %}
|
||||||
try {
|
{% block main_script %}{% endblock %}
|
||||||
const response = await fetch('{% block init_check_url %}/api/loras?page=1&page_size=1{% endblock %}');
|
|
||||||
if (response.ok) {
|
|
||||||
// 如果成功获取数据,说明初始化完成,刷新页面
|
|
||||||
window.location.reload();
|
|
||||||
} else {
|
|
||||||
// 如果还未完成,继续轮询
|
|
||||||
setTimeout(checkInitStatus, 2000); // 每2秒检查一次
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// 如果出错,继续轮询
|
|
||||||
setTimeout(checkInitStatus, 2000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 启动状态检查
|
|
||||||
setTimeout(checkInitStatus, 1000); // 给页面完全加载的时间
|
|
||||||
</script>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% block additional_scripts %}{% endblock %}
|
{% block additional_scripts %}{% endblock %}
|
||||||
|
|||||||
104
templates/components/initialization.html
Normal file
104
templates/components/initialization.html
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
<!-- Initialization Component -->
|
||||||
|
<div class="initialization-container" id="initializationContainer">
|
||||||
|
<div class="initialization-content">
|
||||||
|
<div class="initialization-header">
|
||||||
|
<h2 id="initTitle">{% block init_title %}Initializing{% endblock %}</h2>
|
||||||
|
<p class="init-subtitle" id="initSubtitle">{% block init_message %}Preparing your workspace...{% endblock %}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="initialization-progress">
|
||||||
|
<div class="progress-bar-container">
|
||||||
|
<div class="progress-bar" id="initProgressBar"></div>
|
||||||
|
</div>
|
||||||
|
<div class="progress-details">
|
||||||
|
<span class="progress-percentage" id="progressPercentage">0%</span>
|
||||||
|
<span class="progress-status" id="progressStatus">Starting initialization...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="initialization-stages">
|
||||||
|
<div class="stage-item" id="stageScanFolders">
|
||||||
|
<div class="stage-icon">
|
||||||
|
<i class="fas fa-folder-open"></i>
|
||||||
|
</div>
|
||||||
|
<div class="stage-content">
|
||||||
|
<h4>Scanning Folders</h4>
|
||||||
|
<div class="stage-details" id="scanFoldersDetails">Discovering model directories...</div>
|
||||||
|
</div>
|
||||||
|
<div class="stage-status pending">
|
||||||
|
<i class="fas fa-clock"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stage-item" id="stageCountModels">
|
||||||
|
<div class="stage-icon">
|
||||||
|
<i class="fas fa-calculator"></i>
|
||||||
|
</div>
|
||||||
|
<div class="stage-content">
|
||||||
|
<h4>Counting Models</h4>
|
||||||
|
<div class="stage-details" id="countModelsDetails">Analyzing files...</div>
|
||||||
|
</div>
|
||||||
|
<div class="stage-status pending">
|
||||||
|
<i class="fas fa-clock"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stage-item" id="stageProcessModels">
|
||||||
|
<div class="stage-icon">
|
||||||
|
<i class="fas fa-cogs"></i>
|
||||||
|
</div>
|
||||||
|
<div class="stage-content">
|
||||||
|
<h4>Processing Models</h4>
|
||||||
|
<div class="stage-details" id="processModelsDetails">Reading model metadata...</div>
|
||||||
|
</div>
|
||||||
|
<div class="stage-status pending">
|
||||||
|
<i class="fas fa-clock"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stage-item" id="stageFinalizing">
|
||||||
|
<div class="stage-icon">
|
||||||
|
<i class="fas fa-check-circle"></i>
|
||||||
|
</div>
|
||||||
|
<div class="stage-content">
|
||||||
|
<h4>Finalizing</h4>
|
||||||
|
<div class="stage-details" id="finalizingDetails">Building cache and optimizing...</div>
|
||||||
|
</div>
|
||||||
|
<div class="stage-status pending">
|
||||||
|
<i class="fas fa-clock"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="initialization-tips">
|
||||||
|
<div class="tips-header">
|
||||||
|
<i class="fas fa-lightbulb"></i>
|
||||||
|
<h3>Tips</h3>
|
||||||
|
</div>
|
||||||
|
<div class="tip-carousel" id="tipCarousel">
|
||||||
|
<div class="tip-item">
|
||||||
|
<p>You can drag and drop LoRA files into your folders to automatically import them.</p>
|
||||||
|
</div>
|
||||||
|
<div class="tip-item">
|
||||||
|
<p>Use Civitai URLs to quickly download and install new models.</p>
|
||||||
|
</div>
|
||||||
|
<div class="tip-item">
|
||||||
|
<p>Create recipes to save your favorite model combinations for future use.</p>
|
||||||
|
</div>
|
||||||
|
<div class="tip-item">
|
||||||
|
<p>Filter models by tags or base model type using the filter button in the header.</p>
|
||||||
|
</div>
|
||||||
|
<div class="tip-item">
|
||||||
|
<p>Press Ctrl+F (Cmd+F on Mac) to quickly search within your current view.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tip-navigation">
|
||||||
|
<span class="tip-dot active" data-index="0"></span>
|
||||||
|
<span class="tip-dot" data-index="1"></span>
|
||||||
|
<span class="tip-dot" data-index="2"></span>
|
||||||
|
<span class="tip-dot" data-index="3"></span>
|
||||||
|
<span class="tip-dot" data-index="4"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user