Files
Jackify/jackify/frontends/gui/widgets/progress_indicator.py

268 lines
13 KiB
Python

"""
Progress Indicator Widget
Enhanced status banner widget that displays overall installation progress.
R&D NOTE: This is experimental code for investigation purposes.
"""
from PySide6.QtWidgets import QWidget, QHBoxLayout, QLabel, QProgressBar, QSizePolicy
from PySide6.QtCore import Qt
from PySide6.QtGui import QFont
from jackify.shared.progress_models import InstallationProgress
from ..shared_theme import JACKIFY_COLOR_BLUE
class OverallProgressIndicator(QWidget):
"""
Enhanced progress indicator widget showing:
- Phase name
- Step progress [12/14]
- Data progress (1.1GB/56.3GB)
- Overall percentage
- Optional progress bar
"""
def __init__(self, parent=None, show_progress_bar=True):
"""
Initialize progress indicator.
Args:
parent: Parent widget
show_progress_bar: If True, show visual progress bar in addition to text
"""
super().__init__(parent)
self.show_progress_bar = show_progress_bar
self._setup_ui()
def _setup_ui(self):
"""Set up the UI components."""
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(8)
# Status text label (similar to TTW status banner)
self.status_label = QLabel("Ready to install")
self.status_label.setAlignment(Qt.AlignCenter)
self.status_label.setStyleSheet(f"""
background-color: #2a2a2a;
color: {JACKIFY_COLOR_BLUE};
padding: 6px 8px;
border-radius: 4px;
font-weight: bold;
font-size: 13px;
""")
self.status_label.setMaximumHeight(34)
self.status_label.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
# Progress bar (optional, shown below or integrated)
if self.show_progress_bar:
self.progress_bar = QProgressBar()
self.progress_bar.setRange(0, 100)
self.progress_bar.setValue(0)
self.progress_bar.setFormat("%p%")
# Use white text with shadow/outline effect for readability on both dark and blue backgrounds
self.progress_bar.setStyleSheet(f"""
QProgressBar {{
border: 1px solid #444;
border-radius: 4px;
text-align: center;
background-color: #1a1a1a;
color: #fff;
font-weight: bold;
height: 20px;
}}
QProgressBar::chunk {{
background-color: {JACKIFY_COLOR_BLUE};
border-radius: 3px;
}}
""")
self.progress_bar.setMaximumHeight(20)
self.progress_bar.setVisible(True)
# Layout: text on left, progress bar on right (or stacked)
if self.show_progress_bar:
# Horizontal layout: status text takes available space, progress bar fixed width
layout.addWidget(self.status_label, 1)
layout.addWidget(self.progress_bar, 0) # Fixed width
self.progress_bar.setFixedWidth(100) # Fixed width for progress bar
else:
# Just the status label, full width
layout.addWidget(self.status_label, 1)
# Constrain widget height to prevent unwanted vertical expansion
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.setMaximumHeight(34) # Match status label height
def update_progress(self, progress: InstallationProgress):
"""
Update the progress indicator with new progress state.
Args:
progress: InstallationProgress object with current state
"""
# Update status text
display_text = progress.display_text
if not display_text or display_text == "Processing...":
display_text = progress.phase_name or progress.phase.value.title() or "Processing..."
# Add total download size, remaining size (MB/GB), and ETA for download phase
from jackify.shared.progress_models import InstallationPhase, FileProgress
if progress.phase == InstallationPhase.DOWNLOAD:
# Try to get overall download totals - either from data_total or aggregate from active_files
total_bytes = progress.data_total
processed_bytes = progress.data_processed
using_aggregated = False
# If data_total is 0, try to aggregate from active_files
if total_bytes == 0 and progress.active_files:
total_bytes = sum(f.total_size for f in progress.active_files if f.total_size > 0)
processed_bytes = sum(f.current_size for f in progress.active_files if f.current_size > 0)
using_aggregated = True
# Add remaining download size (MB or GB) if available
if total_bytes > 0:
remaining_bytes = total_bytes - processed_bytes
if remaining_bytes > 0:
# Format as MB if less than 1GB, otherwise GB
if remaining_bytes < (1024.0 ** 3):
remaining_mb = remaining_bytes / (1024.0 ** 2)
display_text += f" | {remaining_mb:.1f}MB remaining"
else:
remaining_gb = remaining_bytes / (1024.0 ** 3)
display_text += f" | {remaining_gb:.1f}GB remaining"
# Calculate ETA - prefer aggregated calculation for concurrent downloads
eta_seconds = -1.0
if using_aggregated:
# For concurrent downloads: sum all active download speeds (not average)
# This gives us the combined throughput
active_speeds = [f.speed for f in progress.active_files if f.speed > 0]
if active_speeds:
combined_speed = sum(active_speeds) # Sum speeds for concurrent downloads
if combined_speed > 0:
eta_seconds = remaining_bytes / combined_speed
else:
# Use the standard ETA calculation from progress model
eta_seconds = progress.get_eta_seconds(use_smoothing=True)
# Format and display ETA
if eta_seconds > 0:
if eta_seconds < 60:
display_text += f" | ETA: {int(eta_seconds)}s"
elif eta_seconds < 3600:
mins = int(eta_seconds // 60)
secs = int(eta_seconds % 60)
if secs > 0:
display_text += f" | ETA: {mins}m {secs}s"
else:
display_text += f" | ETA: {mins}m"
else:
hours = int(eta_seconds // 3600)
mins = int((eta_seconds % 3600) // 60)
if mins > 0:
display_text += f" | ETA: {hours}h {mins}m"
else:
display_text += f" | ETA: {hours}h"
else:
# No total size available - try to show ETA if we have speed info from active files
if progress.active_files:
active_speeds = [f.speed for f in progress.active_files if f.speed > 0]
if active_speeds:
# Can't calculate accurate ETA without total size, but could show speed
pass
# Fallback to standard ETA if available
if not using_aggregated:
eta_display = progress.eta_display
if eta_display:
display_text += f" | ETA: {eta_display}"
self.status_label.setText(display_text)
# Update progress bar if enabled
if self.show_progress_bar and hasattr(self, 'progress_bar'):
# Calculate progress - prioritize data progress, then step progress, then overall_percent
display_percent = 0.0
# Check if we're in BSA building phase (detected by phase label)
from jackify.shared.progress_models import InstallationPhase
is_bsa_building = progress.get_phase_label() == "Building BSAs"
# For install/extract/download/BSA building phases, prefer step-based progress (more accurate)
# This prevents carrying over 100% from previous phases (e.g., .wabbajack download)
if progress.phase in (InstallationPhase.INSTALL, InstallationPhase.EXTRACT, InstallationPhase.DOWNLOAD) or is_bsa_building:
if progress.phase_max_steps > 0:
display_percent = (progress.phase_step / progress.phase_max_steps) * 100.0
elif progress.data_total > 0 and progress.data_processed > 0:
display_percent = (progress.data_processed / progress.data_total) * 100.0
else:
# If no step/data info, use overall_percent but only if it's reasonable
# Don't carry over 100% from previous phase
if progress.overall_percent > 0 and progress.overall_percent < 100.0:
display_percent = progress.overall_percent
else:
display_percent = 0.0 # Reset if we don't have valid progress
else:
# For other phases, prefer data progress, then overall_percent, then step progress
if progress.data_total > 0 and progress.data_processed > 0:
display_percent = (progress.data_processed / progress.data_total) * 100.0
elif progress.overall_percent > 0:
display_percent = progress.overall_percent
elif progress.phase_max_steps > 0:
display_percent = (progress.phase_step / progress.phase_max_steps) * 100.0
self.progress_bar.setValue(int(display_percent))
# Update tooltip with detailed information
tooltip_parts = []
if progress.phase_name:
tooltip_parts.append(f"Phase: {progress.phase_name}")
if progress.phase_progress_text:
tooltip_parts.append(f"Step: {progress.phase_progress_text}")
if progress.data_progress_text:
tooltip_parts.append(f"Data: {progress.data_progress_text}")
# Add total download size in GB for download phase
from jackify.shared.progress_models import InstallationPhase
if progress.phase == InstallationPhase.DOWNLOAD and progress.data_total > 0:
total_gb = progress.total_download_size_gb
remaining_gb = progress.remaining_download_size_gb
if total_gb > 0:
tooltip_parts.append(f"Total Download: {total_gb:.2f}GB")
if remaining_gb > 0:
tooltip_parts.append(f"Remaining: {remaining_gb:.2f}GB")
# Add ETA for download phase
if progress.phase == InstallationPhase.DOWNLOAD:
eta_display = progress.eta_display
if eta_display:
tooltip_parts.append(f"Estimated Time Remaining: {eta_display}")
if progress.overall_percent > 0:
tooltip_parts.append(f"Overall: {progress.overall_percent:.1f}%")
if tooltip_parts:
self.progress_bar.setToolTip("\n".join(tooltip_parts))
self.status_label.setToolTip("\n".join(tooltip_parts))
def set_status(self, text: str, percent: int = None):
"""
Set status text directly without full progress update.
Args:
text: Status text to display
percent: Optional progress percentage (0-100)
"""
self.status_label.setText(text)
if percent is not None and self.show_progress_bar and hasattr(self, 'progress_bar'):
self.progress_bar.setValue(int(percent))
def reset(self):
"""Reset the progress indicator to initial state."""
self.status_label.setText("Ready to install")
if self.show_progress_bar and hasattr(self, 'progress_bar'):
self.progress_bar.setValue(0)
self.progress_bar.setToolTip("")
self.status_label.setToolTip("")