From cd07ceb061ca51c21e63c64b947a15e1a1dcd6fa Mon Sep 17 00:00:00 2001 From: mx57 <38256814+mx57@users.noreply.github.com> Date: Thu, 9 Jul 2026 05:24:47 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20UI/UX:=20Refactor=20StatsDialog?= =?UTF-8?q?=20with=20QTabWidget=20and=20Event=20History?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Refactored `StatsDialog` to use `QTabWidget`, separating "Progress" and "History". - Implemented event history view displaying the last 30 activities with icons. - Integrated event logging for level-ups, achievements, feeding, and Pomodoro sessions. - Improved petting detection precision by using dynamic window center instead of hardcoded offsets. - Added UI verification script in `tests/`. --- src/core/input_manager.py | 8 +++- src/ui/stats_dialog.py | 78 ++++++++++++++++++++++++++++++--------- src/ui/tray_menu.py | 6 +++ tests/verify_ui_script.py | 66 +++++++++++++++++++++++++++++++++ 4 files changed, 139 insertions(+), 19 deletions(-) create mode 100644 tests/verify_ui_script.py diff --git a/src/core/input_manager.py b/src/core/input_manager.py index a626371..73070d3 100644 --- a/src/core/input_manager.py +++ b/src/core/input_manager.py @@ -170,6 +170,7 @@ def add_points(self, points): self.flush_points() # Обязательно сбрасываем перед уведомлением self.window.show_message(f"Уровень дружбы повышен: {new_level} ❤️") self.window.sound_manager.play_sound("happy") + self.db.log_event("level_up", f"Уровень повышен до {new_level}") self.check_for_achievements() def flush_points(self): @@ -242,6 +243,7 @@ def check_for_achievements(self): ach = ACHIEVEMENTS[ach_id] self.window.show_message(f"Достижение: {ach['icon']} {ach['title']}", duration=5000) self.window.sound_manager.play_sound("happy") + self.db.log_event("achievement", f"Разблокировано: {ach['title']}") def _reset_idle_state(self): """Возвращает котика в idle, если он спал или думал.""" @@ -312,8 +314,10 @@ def handle_mouse(self, x, y): # Проверка "поглаживания" pet_pos = self.window.get_cached_pos() - dx_pet = x - (pet_pos.x() + 50) - dy_pet = y - (pet_pos.y() + 50) + center_x = pet_pos.x() + self.window.width() // 2 + center_y = pet_pos.y() + self.window.height() // 2 + dx_pet = x - center_x + dy_pet = y - center_y dist_sq_pet = dx_pet * dx_pet + dy_pet * dy_pet # Оптимизация: сравнение квадрата расстояния (порог 60px -> 3600) diff --git a/src/ui/stats_dialog.py b/src/ui/stats_dialog.py index 30961ba..28f9e15 100644 --- a/src/ui/stats_dialog.py +++ b/src/ui/stats_dialog.py @@ -1,4 +1,6 @@ -from PySide6.QtWidgets import QDialog, QVBoxLayout, QLabel, QProgressBar, QPushButton, QHBoxLayout, QScrollArea, QWidget +from PySide6.QtWidgets import (QDialog, QVBoxLayout, QLabel, QProgressBar, + QPushButton, QHBoxLayout, QScrollArea, QWidget, + QTabWidget, QListWidget, QListWidgetItem) from PySide6.QtCore import Qt from src.utils.bonding_utils import get_level_info, ACHIEVEMENTS @@ -6,10 +8,32 @@ class StatsDialog(QDialog): def __init__(self, db, parent=None): super().__init__(parent) self.db = db - self.setWindowTitle("Статистика привязанности") - self.setFixedWidth(300) + self.setWindowTitle("Статистика Котика") + self.setFixedWidth(350) + self.setFixedHeight(500) - layout = QVBoxLayout(self) + main_layout = QVBoxLayout(self) + + self.tabs = QTabWidget() + main_layout.addWidget(self.tabs) + + # Вкладка Прогресса + self.progress_tab = QWidget() + self.setup_progress_tab() + self.tabs.addTab(self.progress_tab, "Прогресс") + + # Вкладка Истории + self.history_tab = QWidget() + self.setup_history_tab() + self.tabs.addTab(self.history_tab, "История") + + # Кнопка закрытия + close_btn = QPushButton("Закрыть") + close_btn.clicked.connect(self.accept) + main_layout.addWidget(close_btn) + + def setup_progress_tab(self): + layout = QVBoxLayout(self.progress_tab) points = self.db.get_affection_points() level, title, points_in_level, points_for_next_level = get_level_info(points) @@ -17,7 +41,7 @@ def __init__(self, db, parent=None): # Заголовок title_label = QLabel(f"Уровень {level}: {title}") title_label.setAlignment(Qt.AlignCenter) - title_label.setStyleSheet("font-size: 16px; font-weight: bold; margin-bottom: 10px;") + title_label.setStyleSheet("font-size: 16px; font-weight: bold; margin-bottom: 5px;") layout.addWidget(title_label) # Общие очки @@ -27,9 +51,9 @@ def __init__(self, db, parent=None): # Макс скорость печати max_kps = self.db.get_stat("max_kps") - kps_label = QLabel(f"Макс. скорость: {max_kps} кл/сек ⚡") + kps_label = QLabel(f"Рекорд скорости: {max_kps} кл/сек ⚡") kps_label.setAlignment(Qt.AlignCenter) - kps_label.setStyleSheet("color: #555; font-size: 11px;") + kps_label.setStyleSheet("color: #555; font-size: 11px; margin-bottom: 5px;") layout.addWidget(kps_label) # Прогресс бар @@ -43,9 +67,7 @@ def __init__(self, db, parent=None): else: layout.addWidget(QLabel("Максимальный уровень достигнут! 🎉")) - layout.addSpacing(20) - - # Раздел достижений + layout.addSpacing(10) layout.addWidget(QLabel("Достижения:")) scroll = QScrollArea() @@ -72,7 +94,6 @@ def __init__(self, db, parent=None): # Расчет прогресса progress_text = "" if not is_unlocked and 'goal' in ach_info and 'stat' in ach_info: - # Если у нас есть InputManager, берем актуальные данные (включая буфер) if self.parent() and hasattr(self.parent(), 'input_manager') and self.parent().input_manager: im = self.parent().input_manager if ach_info['stat'] == 'bonding_points': @@ -102,12 +123,35 @@ def __init__(self, db, parent=None): scroll_layout.addWidget(ach_widget) scroll.setWidget(scroll_content) - scroll.setFixedHeight(250) layout.addWidget(scroll) - layout.addSpacing(20) + def setup_history_tab(self): + layout = QVBoxLayout(self.history_tab) - # Кнопка закрытия - close_btn = QPushButton("Закрыть") - close_btn.clicked.connect(self.accept) - layout.addWidget(close_btn) + layout.addWidget(QLabel("Последние события:")) + + self.history_list = QListWidget() + + events = self.db.get_recent_activity(30) + for event in events: + # event = (id, timestamp, event_type, description) + _, timestamp, event_type, desc = event + + # Красивое форматирование времени (убираем секунды) + time_str = timestamp.split(' ')[1][:5] if ' ' in timestamp else timestamp + + icon = "ℹ️" + if event_type == "level_up": icon = "🆙" + elif event_type == "achievement": icon = "🏆" + elif event_type == "feeding": icon = "🐟" + elif event_type == "pomodoro_start": icon = "⏱" + elif event_type == "app_start": icon = "🚀" + + item_text = f"[{time_str}] {icon} {desc if desc else event_type}" + item = QListWidgetItem(item_text) + self.history_list.addItem(item) + + layout.addWidget(self.history_list) + + if not events: + layout.addWidget(QLabel("История пока пуста...")) diff --git a/src/ui/tray_menu.py b/src/ui/tray_menu.py index b4f5979..55a1d1a 100644 --- a/src/ui/tray_menu.py +++ b/src/ui/tray_menu.py @@ -118,11 +118,15 @@ def start_work_timer(self, checked=False): if self.window.timer_system: self.window.timer_system.start_pomodoro("work") self.window.show_message("Пора работать! 🛠") + if self.window.input_manager and self.window.input_manager.db: + self.window.input_manager.db.log_event("pomodoro_start", "Начата сессия работы") def start_break_timer(self, checked=False): if self.window.timer_system: self.window.timer_system.start_pomodoro("break") self.window.show_message("Отдыхаем! ☕") + if self.window.input_manager and self.window.input_manager.db: + self.window.input_manager.db.log_event("pomodoro_start", "Начата сессия отдыха") def show_settings(self, checked=False): dialog = SettingsDialog(self.window.config, self.window) @@ -147,6 +151,8 @@ def feed_pet(self, checked=False): self.window.input_manager.add_points(5) self.window.show_message("Мням! +5 ❤️") self.window.input_manager.pending_stats["total_feedings"] += 1 + if self.window.input_manager.db: + self.window.input_manager.db.log_event("feeding", "Котик покормлен") self.window.input_manager.check_for_achievements() def toggle_laser(self, checked): diff --git a/tests/verify_ui_script.py b/tests/verify_ui_script.py new file mode 100644 index 0000000..7fd12df --- /dev/null +++ b/tests/verify_ui_script.py @@ -0,0 +1,66 @@ +import sys +import os + +# Add src to path +sys.path.append(os.getcwd()) + +from PySide6.QtWidgets import QApplication +from src.ui.stats_dialog import StatsDialog +from src.utils.data_store import DataStore +from src.utils.config_manager import ConfigManager + +def verify_ui(): + # Use offscreen platform for headless environments + os.environ["QT_QPA_PLATFORM"] = "offscreen" + + app = QApplication(sys.argv) + + # Setup temporary DB + db_path = "test_verify.db" + if os.path.exists(db_path): + os.remove(db_path) + + db = DataStore(db_path) + + # Populate with some data + db.add_affection_points(150) # Level 1 + db.set_stat("max_kps", 10) + db.set_stat("total_clicks", 500) + db.set_stat("work_seconds", 300) + db.add_achievement("first_friend") + + # Log some events + db.log_event("app_start", "Приложение замурчало") + db.log_event("feeding", "Покормили рыбкой") + db.log_event("level_up", "Уровень повышен до 1") + db.log_event("achievement", "Разблокировано: Первый друг") + db.log_event("pomodoro_start", "Начата сессия работы") + + # Mock window parent for StatsDialog if needed, but it should work with None + dialog = StatsDialog(db) + dialog.show() + + # Ensure UI is processed + app.processEvents() + + # Take screenshots of both tabs + os.makedirs("verification/screenshots", exist_ok=True) + + # Progress tab (default) + dialog.tabs.setCurrentIndex(0) + app.processEvents() + dialog.grab().save("verification/screenshots/stats_progress.png") + + # History tab + dialog.tabs.setCurrentIndex(1) + app.processEvents() + dialog.grab().save("verification/screenshots/stats_history.png") + + print("Screenshots saved to verification/screenshots/") + + db.close() + if os.path.exists(db_path): + os.remove(db_path) + +if __name__ == "__main__": + verify_ui()