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()