Files
Jackify/jackify/frontends/gui/widgets/file_progress_item.py
2026-02-07 18:26:54 +00:00

196 lines
7.6 KiB
Python

"""
File progress item widget for a single file's progress display.
"""
from PySide6.QtWidgets import (
QWidget, QHBoxLayout, QLabel, QProgressBar, QSizePolicy
)
from PySide6.QtCore import Qt, QTimer
from jackify.shared.progress_models import FileProgress, OperationType
from ..shared_theme import JACKIFY_COLOR_BLUE
class FileProgressItem(QWidget):
"""Widget representing a single file's progress."""
def __init__(self, file_progress: FileProgress, parent=None):
super().__init__(parent)
self.file_progress = file_progress
self._target_percent = file_progress.percent
self._current_display_percent = file_progress.percent
self._spinner_position = 0
self._is_indeterminate = False
self._animation_timer = QTimer(self)
self._animation_timer.timeout.connect(self._animate_progress)
self._animation_timer.setInterval(16)
self._setup_ui()
self._update_display()
def _setup_ui(self):
layout = QHBoxLayout(self)
layout.setContentsMargins(4, 2, 4, 2)
layout.setSpacing(8)
operation_label = QLabel(self._get_operation_symbol())
operation_label.setFixedWidth(20)
operation_label.setAlignment(Qt.AlignCenter)
operation_label.setStyleSheet(f"color: {JACKIFY_COLOR_BLUE}; font-weight: bold;")
layout.addWidget(operation_label)
filename_label = QLabel(self._truncate_filename(self.file_progress.filename))
filename_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
filename_label.setToolTip(self.file_progress.filename)
filename_label.setStyleSheet("color: #ccc; font-size: 11px;")
layout.addWidget(filename_label, 1)
self.filename_label = filename_label
percent_label = QLabel()
percent_label.setFixedWidth(40)
percent_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
percent_label.setStyleSheet("color: #aaa; font-size: 11px;")
layout.addWidget(percent_label)
self.percent_label = percent_label
progress_bar = QProgressBar()
progress_bar.setFixedHeight(12)
progress_bar.setFixedWidth(80)
progress_bar.setTextVisible(False)
progress_bar.setStyleSheet(f"""
QProgressBar {{
border: 1px solid #444;
border-radius: 2px;
background-color: #1a1a1a;
}}
QProgressBar::chunk {{
background-color: {JACKIFY_COLOR_BLUE};
border-radius: 1px;
}}
""")
layout.addWidget(progress_bar)
self.progress_bar = progress_bar
def _get_operation_symbol(self) -> str:
symbols = {
OperationType.DOWNLOAD: "",
OperationType.EXTRACT: "",
OperationType.VALIDATE: "",
OperationType.INSTALL: "",
}
return symbols.get(self.file_progress.operation, "")
def _truncate_filename(self, filename: str, max_length: int = 40) -> str:
if len(filename) <= max_length:
return filename
return filename[:max_length-3] + "..."
def _update_display(self):
is_summary = hasattr(self.file_progress, '_is_summary') and self.file_progress._is_summary
no_progress_bar = hasattr(self.file_progress, '_no_progress_bar') and self.file_progress._no_progress_bar
if 'Installing Files' in self.file_progress.filename or 'Converting Texture' in self.file_progress.filename or 'BSA:' in self.file_progress.filename:
name_display = self.file_progress.filename
elif self.file_progress.filename.startswith('Wine component:'):
rest = self.file_progress.filename.split(':', 1)[1].strip()
comp_id = rest.split('|')[0].strip() if '|' in rest else rest
name_display = f"Installing {comp_id}..."
else:
name_display = self._truncate_filename(self.file_progress.filename)
if not is_summary and not no_progress_bar:
size_display = self.file_progress.size_display
if size_display:
name_display = f"{name_display} ({size_display})"
self.filename_label.setText(name_display)
self.filename_label.setToolTip(self.file_progress.filename)
if no_progress_bar:
self._animation_timer.stop()
self.percent_label.setText("")
self.progress_bar.setVisible(False)
return
self.progress_bar.setVisible(True)
if is_summary:
summary_step = getattr(self.file_progress, '_summary_step', 0)
summary_max = getattr(self.file_progress, '_summary_max', 0)
if summary_max > 0:
percent = (summary_step / summary_max) * 100.0
self._target_percent = max(0, min(100, percent))
if not self._animation_timer.isActive():
self._animation_timer.start()
self.progress_bar.setRange(0, 100)
else:
self._is_indeterminate = True
self.percent_label.setText("")
self.progress_bar.setRange(0, 100)
if not self._animation_timer.isActive():
self._animation_timer.start()
return
is_queued = (
self.file_progress.total_size > 0 and
self.file_progress.percent == 0 and
self.file_progress.current_size == 0 and
self.file_progress.speed <= 0
)
if is_queued:
self._is_indeterminate = False
self._animation_timer.stop()
self.percent_label.setText("Queued")
self.progress_bar.setRange(0, 100)
self.progress_bar.setValue(0)
return
has_meaningful_progress = (
self.file_progress.percent > 0 or
(self.file_progress.total_size > 0 and self.file_progress.current_size > 0) or
(self.file_progress.speed > 0 and self.file_progress.percent >= 0)
)
if has_meaningful_progress:
self._is_indeterminate = False
self._target_percent = max(0, self.file_progress.percent)
if not self._animation_timer.isActive():
self._animation_timer.start()
self.progress_bar.setRange(0, 100)
else:
self._is_indeterminate = True
self.percent_label.setText("")
self.progress_bar.setRange(0, 100)
if not self._animation_timer.isActive():
self._animation_timer.start()
def _animate_progress(self):
if self._is_indeterminate:
self._spinner_position = (self._spinner_position + 4) % 200
if self._spinner_position < 100:
display_value = self._spinner_position
else:
display_value = 200 - self._spinner_position
self.progress_bar.setValue(display_value)
else:
diff = self._target_percent - self._current_display_percent
if abs(diff) >= 0.1:
self._current_display_percent += diff * 0.2
self._current_display_percent = max(0, min(100, self._current_display_percent))
display_percent = self._current_display_percent
self.progress_bar.setValue(int(display_percent))
if self.file_progress.percent > 0:
self.percent_label.setText(f"{display_percent:.0f}%")
else:
self.percent_label.setText("")
def update_progress(self, file_progress: FileProgress):
self.file_progress = file_progress
self._update_display()
def cleanup(self):
if self._animation_timer.isActive():
self._animation_timer.stop()