You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
299 lines
10 KiB
299 lines
10 KiB
"""
|
|
Progress Panel - Right sidebar with progress tracking and statistics
|
|
"""
|
|
|
|
from PyQt5.QtWidgets import (
|
|
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
|
QProgressBar, QPushButton, QFrame, QScrollArea
|
|
)
|
|
from PyQt5.QtCore import Qt
|
|
from PyQt5.QtGui import QFont
|
|
|
|
from app import config
|
|
from app.models import Course
|
|
|
|
|
|
class ProgressPanel(QWidget):
|
|
"""Right sidebar panel with progress statistics and tracking"""
|
|
|
|
def __init__(self, course: Course, parent=None):
|
|
super().__init__(parent)
|
|
self.course = course
|
|
|
|
self.init_ui()
|
|
|
|
def init_ui(self):
|
|
"""Initialize the UI components"""
|
|
# Main layout
|
|
main_layout = QVBoxLayout(self)
|
|
main_layout.setContentsMargins(10, 10, 10, 10)
|
|
main_layout.setSpacing(10)
|
|
|
|
# Title
|
|
title = QLabel("Your Progress")
|
|
title.setStyleSheet(f"font-size: 14pt; font-weight: bold; color: {config.COLOR_PRIMARY};")
|
|
main_layout.addWidget(title)
|
|
|
|
# Scroll area for content
|
|
scroll = QScrollArea()
|
|
scroll.setWidgetResizable(True)
|
|
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
|
scroll.setFrameShape(QFrame.NoFrame)
|
|
|
|
# Content widget
|
|
content_widget = QWidget()
|
|
layout = QVBoxLayout(content_widget)
|
|
layout.setContentsMargins(0, 0, 5, 0)
|
|
layout.setSpacing(15)
|
|
|
|
# === Overall Progress Section ===
|
|
layout.addWidget(self._create_section_header("Overall Progress"))
|
|
|
|
self.overall_progress_bar = QProgressBar()
|
|
self.overall_progress_bar.setStyleSheet(f"""
|
|
QProgressBar {{
|
|
border: 2px solid {config.COLOR_PRIMARY};
|
|
border-radius: 5px;
|
|
text-align: center;
|
|
height: 25px;
|
|
}}
|
|
QProgressBar::chunk {{
|
|
background-color: {config.COLOR_SUCCESS};
|
|
}}
|
|
""")
|
|
layout.addWidget(self.overall_progress_bar)
|
|
|
|
self.overall_stats_label = QLabel("0 / 30 lessons completed")
|
|
self.overall_stats_label.setStyleSheet("font-size: 10pt; color: #666;")
|
|
layout.addWidget(self.overall_stats_label)
|
|
|
|
layout.addWidget(self._create_separator())
|
|
|
|
# === Points and Level Section ===
|
|
layout.addWidget(self._create_section_header("Points & Level"))
|
|
|
|
points_layout = QHBoxLayout()
|
|
self.points_label = QLabel("0 pts")
|
|
self.points_label.setStyleSheet(f"font-size: 24pt; font-weight: bold; color: {config.COLOR_WARNING};")
|
|
points_layout.addWidget(self.points_label)
|
|
points_layout.addStretch()
|
|
layout.addLayout(points_layout)
|
|
|
|
self.level_label = QLabel("Level 1: Novice")
|
|
self.level_label.setStyleSheet("font-size: 11pt; color: #666;")
|
|
layout.addWidget(self.level_label)
|
|
|
|
self.level_progress_bar = QProgressBar()
|
|
self.level_progress_bar.setStyleSheet(f"""
|
|
QProgressBar {{
|
|
border: 1px solid #ccc;
|
|
border-radius: 3px;
|
|
text-align: center;
|
|
height: 15px;
|
|
}}
|
|
QProgressBar::chunk {{
|
|
background-color: {config.COLOR_WARNING};
|
|
}}
|
|
""")
|
|
layout.addWidget(self.level_progress_bar)
|
|
|
|
layout.addWidget(self._create_separator())
|
|
|
|
# === Part Progress Section ===
|
|
layout.addWidget(self._create_section_header("Progress by Part"))
|
|
|
|
self.part_progress_widgets = []
|
|
for part in self.course.parts:
|
|
part_widget = self._create_part_progress(part.number, part.title, 0)
|
|
self.part_progress_widgets.append(part_widget)
|
|
layout.addWidget(part_widget)
|
|
|
|
layout.addWidget(self._create_separator())
|
|
|
|
# === Study Stats Section ===
|
|
layout.addWidget(self._create_section_header("Study Statistics"))
|
|
|
|
stats_grid = QVBoxLayout()
|
|
stats_grid.setSpacing(8)
|
|
|
|
self.time_stat = self._create_stat_row("⏱", "Total Time", "0 min")
|
|
self.streak_stat = self._create_stat_row("🔥", "Streak", "0 days")
|
|
self.exercises_stat = self._create_stat_row("📝", "Exercises", "0 / 18")
|
|
|
|
stats_grid.addWidget(self.time_stat)
|
|
stats_grid.addWidget(self.streak_stat)
|
|
stats_grid.addWidget(self.exercises_stat)
|
|
|
|
layout.addLayout(stats_grid)
|
|
|
|
layout.addWidget(self._create_separator())
|
|
|
|
# === Current Lesson Section ===
|
|
layout.addWidget(self._create_section_header("Current Lesson"))
|
|
|
|
self.current_lesson_label = QLabel("No lesson selected")
|
|
self.current_lesson_label.setStyleSheet("font-size: 10pt; color: #666; padding: 10px;")
|
|
self.current_lesson_label.setWordWrap(True)
|
|
layout.addWidget(self.current_lesson_label)
|
|
|
|
# Push everything to top
|
|
layout.addStretch()
|
|
|
|
# Set content widget to scroll area
|
|
scroll.setWidget(content_widget)
|
|
main_layout.addWidget(scroll, 1)
|
|
|
|
# Initialize with default values
|
|
self.update_progress(0, 0, 0)
|
|
|
|
self.setMinimumWidth(config.PROGRESS_PANEL_MIN_WIDTH)
|
|
|
|
def _create_section_header(self, text: str) -> QLabel:
|
|
"""Create a section header label"""
|
|
label = QLabel(text)
|
|
label.setStyleSheet(f"font-size: 11pt; font-weight: bold; color: {config.COLOR_SECONDARY};")
|
|
return label
|
|
|
|
def _create_separator(self) -> QFrame:
|
|
"""Create a horizontal separator line"""
|
|
line = QFrame()
|
|
line.setFrameShape(QFrame.HLine)
|
|
line.setFrameShadow(QFrame.Sunken)
|
|
line.setStyleSheet("color: #ddd;")
|
|
return line
|
|
|
|
def _create_part_progress(self, part_number: int, part_title: str, progress: int) -> QWidget:
|
|
"""Create a part progress widget"""
|
|
widget = QWidget()
|
|
layout = QVBoxLayout(widget)
|
|
layout.setContentsMargins(0, 5, 0, 5)
|
|
layout.setSpacing(5)
|
|
|
|
# Part title
|
|
title_label = QLabel(f"Part {part_number}: {part_title[:30]}...")
|
|
title_label.setStyleSheet("font-size: 9pt; font-weight: bold;")
|
|
layout.addWidget(title_label)
|
|
|
|
# Progress bar
|
|
progress_bar = QProgressBar()
|
|
progress_bar.setValue(progress)
|
|
progress_bar.setStyleSheet(f"""
|
|
QProgressBar {{
|
|
border: 1px solid #ccc;
|
|
border-radius: 3px;
|
|
text-align: center;
|
|
height: 12px;
|
|
font-size: 8pt;
|
|
}}
|
|
QProgressBar::chunk {{
|
|
background-color: {config.COLOR_PRIMARY};
|
|
}}
|
|
""")
|
|
layout.addWidget(progress_bar)
|
|
|
|
# Store reference for updates
|
|
widget.progress_bar = progress_bar
|
|
|
|
return widget
|
|
|
|
def _create_stat_row(self, icon: str, label: str, value: str) -> QWidget:
|
|
"""Create a statistics row"""
|
|
widget = QWidget()
|
|
layout = QHBoxLayout(widget)
|
|
layout.setContentsMargins(5, 5, 5, 5)
|
|
layout.setSpacing(10)
|
|
|
|
# Icon
|
|
icon_label = QLabel(icon)
|
|
icon_label.setStyleSheet("font-size: 16pt;")
|
|
layout.addWidget(icon_label)
|
|
|
|
# Label
|
|
text_label = QLabel(label)
|
|
text_label.setStyleSheet("font-size: 10pt; color: #666;")
|
|
layout.addWidget(text_label)
|
|
|
|
layout.addStretch()
|
|
|
|
# Value
|
|
value_label = QLabel(value)
|
|
value_label.setStyleSheet("font-size: 10pt; font-weight: bold;")
|
|
layout.addWidget(value_label)
|
|
|
|
# Store reference for updates
|
|
widget.value_label = value_label
|
|
|
|
return widget
|
|
|
|
def update_progress(self, completed_lessons: int, total_points: int, total_time_minutes: int):
|
|
"""Update overall progress display"""
|
|
# Overall progress
|
|
total_lessons = self.course.total_lessons
|
|
progress_percent = int((completed_lessons / total_lessons) * 100) if total_lessons > 0 else 0
|
|
self.overall_progress_bar.setValue(progress_percent)
|
|
self.overall_stats_label.setText(f"{completed_lessons} / {total_lessons} lessons completed")
|
|
|
|
# Points
|
|
self.points_label.setText(f"{total_points} pts")
|
|
|
|
# Level
|
|
level_info = self._get_level_info(total_points)
|
|
self.level_label.setText(f"Level {level_info['level']}: {level_info['title']}")
|
|
self.level_progress_bar.setValue(level_info['progress'])
|
|
|
|
# Time
|
|
if total_time_minutes < 60:
|
|
time_str = f"{total_time_minutes} min"
|
|
else:
|
|
hours = total_time_minutes // 60
|
|
minutes = total_time_minutes % 60
|
|
time_str = f"{hours}h {minutes}m"
|
|
self.time_stat.value_label.setText(time_str)
|
|
|
|
def update_part_progress(self, part_number: int, completed: int, total: int):
|
|
"""Update progress for a specific part"""
|
|
if 0 < part_number <= len(self.part_progress_widgets):
|
|
widget = self.part_progress_widgets[part_number - 1]
|
|
progress = int((completed / total) * 100) if total > 0 else 0
|
|
widget.progress_bar.setValue(progress)
|
|
widget.progress_bar.setFormat(f"{completed}/{total} ({progress}%)")
|
|
|
|
def update_streak(self, days: int):
|
|
"""Update study streak"""
|
|
self.streak_stat.value_label.setText(f"{days} days")
|
|
|
|
def update_exercises(self, completed: int, total: int):
|
|
"""Update exercise completion"""
|
|
self.exercises_stat.value_label.setText(f"{completed} / {total}")
|
|
|
|
def update_current_lesson(self, lesson_title: str, lesson_points: int, estimated_time: int):
|
|
"""Update current lesson information"""
|
|
text = f"""
|
|
<b>{lesson_title}</b><br>
|
|
Points: {lesson_points} | Est. time: {estimated_time} min
|
|
"""
|
|
self.current_lesson_label.setText(text)
|
|
|
|
def _get_level_info(self, points: int) -> dict:
|
|
"""Get level information based on points"""
|
|
for i, (threshold, title, subtitle) in enumerate(config.LEVELS):
|
|
if i < len(config.LEVELS) - 1:
|
|
next_threshold = config.LEVELS[i + 1][0]
|
|
if points < next_threshold:
|
|
progress = int(((points - threshold) / (next_threshold - threshold)) * 100)
|
|
return {
|
|
'level': i + 1,
|
|
'title': title,
|
|
'subtitle': subtitle,
|
|
'progress': progress,
|
|
'next_threshold': next_threshold
|
|
}
|
|
|
|
# Max level
|
|
return {
|
|
'level': len(config.LEVELS),
|
|
'title': config.LEVELS[-1][1],
|
|
'subtitle': config.LEVELS[-1][2],
|
|
'progress': 100,
|
|
'next_threshold': config.LEVELS[-1][0]
|
|
}
|