diff --git a/.jules/bolt.md b/.jules/bolt.md index 2eea3c9..c35890c 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -54,3 +54,11 @@ ## 2026-07-03 - [State Reset Logic for Idle States] **Инсайт:** При введении новых фоновых состояний (thinking, sleeping) важно обеспечить их мгновенный сброс при любом пользовательском вводе. Отсутствие централизованного метода сброса ведет к дублированию кода в handle_key и handle_mouse. **Действие:** Использовать вспомогательный метод `_reset_idle_state()` для перевода питомца из пассивных состояний в 'idle' при активности пользователя. + +## 2026-07-04 - [Atomic Batch Updates for Statistics] +**Инсайт:** Использование отдельных SQL-запросов для обновления каждой характеристики (очки, клики, KPS) при сбросе буфера (flush) неэффективно. Группировка всех обновлений в одну транзакцию через BEGIN TRANSACTION/COMMIT (метод update_stats_batch) значительно снижает нагрузку на диск и гарантирует атомарность данных. +**Действие:** При необходимости обновления нескольких полей в БД SQLite (например, при сбросе игрового прогресса из памяти) всегда использовать транзакционный пакетный метод вместо последовательных вызовов increment_stat. + +## 2026-07-04 - [Activity History for User Engagement] +**Инсайт:** Пользователи ценят возможность видеть историю своих действий (кормление, достижения). Разделение статистики на «Прогресс» и «Историю» через QTabWidget позволяет сохранить компактность окна, предоставляя при этом больше информации. +**Действие:** Для логов и списков истории использовать QTabWidget с QListWidget, чтобы не перегружать основной экран статистики. diff --git a/src/core/input_manager.py b/src/core/input_manager.py index a626371..333290e 100644 --- a/src/core/input_manager.py +++ b/src/core/input_manager.py @@ -170,6 +170,8 @@ def add_points(self, points): self.flush_points() # Обязательно сбрасываем перед уведомлением self.window.show_message(f"Уровень дружбы повышен: {new_level} ❤️") self.window.sound_manager.play_sound("happy") + if self.db: + self.db.log_event("level_up", f"Достигнут уровень {new_level}") self.check_for_achievements() def flush_points(self): @@ -181,25 +183,31 @@ def flush_all(self): if not self.db: return - if self.pending_points > 0: - self.db.add_affection_points(self.pending_points) - self.last_affection_points += self.pending_points - self.stats_cache["bonding_points"] = self.last_affection_points - self.pending_points = 0 + if hasattr(self.db, 'update_stats_batch'): + self.db.update_stats_batch(self.pending_points, self.pending_stats, self.max_kps) + else: + # Fallback для старой версии БД + if self.pending_points > 0: + self.db.add_affection_points(self.pending_points) + for key, value in self.pending_stats.items(): + if value > 0: + self.db.increment_stat(key, value) + if hasattr(self.db, 'set_stat'): + self.db.set_stat("max_kps", self.max_kps) + + # Обновляем локальные кэши + self.last_affection_points += self.pending_points + self.stats_cache["bonding_points"] = self.last_affection_points + self.pending_points = 0 for key, value in self.pending_stats.items(): if value > 0: - self.db.increment_stat(key, value) if key == "total_clicks": self.total_clicks_cache += value - - # Обновляем кэш вручную вместо дорогого get_all_stats() self.stats_cache[key] = self.stats_cache.get(key, 0) + value self.pending_stats[key] = 0 - if hasattr(self.db, 'set_stat'): - self.db.set_stat("max_kps", self.max_kps) - self.stats_cache["max_kps"] = self.max_kps + self.stats_cache["max_kps"] = self.max_kps def add_shake(self): """Регистрирует встряхивание котика.""" @@ -240,6 +248,7 @@ def check_for_achievements(self): self.unlocked_achievements.append(ach_id) self.db.add_achievement(ach_id) ach = ACHIEVEMENTS[ach_id] + self.db.log_event("achievement", f"Открыто достижение: {ach['title']}") self.window.show_message(f"Достижение: {ach['icon']} {ach['title']}", duration=5000) self.window.sound_manager.play_sound("happy") diff --git a/src/ui/stats_dialog.py b/src/ui/stats_dialog.py index 30961ba..5510591 100644 --- a/src/ui/stats_dialog.py +++ b/src/ui/stats_dialog.py @@ -1,4 +1,5 @@ -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,31 +7,53 @@ 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) layout = QVBoxLayout(self) + self.tabs = QTabWidget() + layout.addWidget(self.tabs) + + # Вкладка 1: Прогресс и Достижения + self.progress_tab = QWidget() + self.setup_progress_tab() + self.tabs.addTab(self.progress_tab, "🎯 Прогресс") + + # Вкладка 2: История активности + self.history_tab = QWidget() + self.setup_history_tab() + self.tabs.addTab(self.history_tab, "📜 История") + + # Кнопка закрытия + close_btn = QPushButton("Закрыть") + close_btn.clicked.connect(self.accept) + 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) # Заголовок 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) - # Общие очки - points_label = QLabel(f"Всего очков: {points} ❤️") - points_label.setAlignment(Qt.AlignCenter) - layout.addWidget(points_label) + # Общие очки и KPS в одной строке + stats_row = QHBoxLayout() + points_label = QLabel(f"Всего: {points} ❤️") - # Макс скорость печати max_kps = self.db.get_stat("max_kps") - kps_label = QLabel(f"Макс. скорость: {max_kps} кл/сек ⚡") - kps_label.setAlignment(Qt.AlignCenter) - kps_label.setStyleSheet("color: #555; font-size: 11px;") - layout.addWidget(kps_label) + kps_label = QLabel(f"Макс: {max_kps} кл/сек ⚡") + kps_label.setStyleSheet("color: #555;") + + stats_row.addWidget(points_label) + stats_row.addStretch() + stats_row.addWidget(kps_label) + layout.addLayout(stats_row) # Прогресс бар if points_for_next_level > 0: @@ -43,7 +66,7 @@ def __init__(self, db, parent=None): else: layout.addWidget(QLabel("Максимальный уровень достигнут! 🎉")) - layout.addSpacing(20) + layout.addSpacing(10) # Раздел достижений layout.addWidget(QLabel("Достижения:")) @@ -72,7 +95,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 +124,26 @@ def __init__(self, db, parent=None): scroll_layout.addWidget(ach_widget) scroll.setWidget(scroll_content) - scroll.setFixedHeight(250) + scroll.setFixedHeight(200) 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) + history_list = QListWidget() + activities = self.db.get_recent_activity(20) + + for activity in activities: + # activity format: (id, timestamp, event_type, description) + _, ts, event, desc = activity + # Убираем секунды для компактности + time_str = ts.split(" ")[1][:5] if " " in ts else ts + + item = QListWidgetItem(f"[{time_str}] {desc}") + item.setToolTip(f"{ts}\nТип: {event}") + history_list.addItem(item) + + if not activities: + history_list.addItem("История пока пуста...") + + layout.addWidget(history_list) diff --git a/src/ui/tray_menu.py b/src/ui/tray_menu.py index b4f5979..80ef58a 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_break", "Начат перерыв") 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("feed", "Котик покормлен") self.window.input_manager.check_for_achievements() def toggle_laser(self, checked): diff --git a/src/utils/data_store.py b/src/utils/data_store.py index b7021e4..6599ef6 100644 --- a/src/utils/data_store.py +++ b/src/utils/data_store.py @@ -116,6 +116,29 @@ def set_stat(self, key, value): cursor.execute('INSERT OR REPLACE INTO stats (key, value) VALUES (?, ?)', (key, value)) self.conn.commit() + def update_stats_batch(self, points, stats_dict, max_kps): + """Обновляет очки привязанности, статистику и макс. KPS в одной транзакции.""" + if not self.conn: + self.init_db() + + try: + cursor = self.conn.cursor() + cursor.execute('BEGIN TRANSACTION') + + if points > 0: + cursor.execute('UPDATE stats SET value = value + ? WHERE key = "bonding_points"', (points,)) + + for key, value in stats_dict.items(): + if value > 0: + cursor.execute('UPDATE stats SET value = value + ? WHERE key = ?', (value, key)) + + cursor.execute('INSERT OR REPLACE INTO stats (key, value) VALUES ("max_kps", ?)', (max_kps,)) + + self.conn.commit() + except Exception as e: + self.conn.rollback() + print(f"Error in update_stats_batch: {e}") + def add_achievement(self, ach_id): if not self.conn: self.init_db() diff --git a/verification/screenshots/stats_dialog.png b/verification/screenshots/stats_dialog.png new file mode 100644 index 0000000..6a10c4c Binary files /dev/null and b/verification/screenshots/stats_dialog.png differ diff --git a/verification/screenshots/stats_dialog_history.png b/verification/screenshots/stats_dialog_history.png new file mode 100644 index 0000000..b80426a Binary files /dev/null and b/verification/screenshots/stats_dialog_history.png differ diff --git a/verification/screenshots/stats_dialog_progress.png b/verification/screenshots/stats_dialog_progress.png new file mode 100644 index 0000000..6a10c4c Binary files /dev/null and b/verification/screenshots/stats_dialog_progress.png differ diff --git a/verify_ui.py b/verify_ui.py new file mode 100644 index 0000000..28d8b9c --- /dev/null +++ b/verify_ui.py @@ -0,0 +1,48 @@ + +import sys +import os + +# Set offscreen platform for headless environment +os.environ["QT_QPA_PLATFORM"] = "offscreen" + +from PySide6.QtWidgets import QApplication +from src.ui.stats_dialog import StatsDialog +from unittest.mock import MagicMock + +def verify(): + app = QApplication(sys.argv) + + # Mock DataStore + db = MagicMock() + db.get_affection_points.return_value = 150 + db.get_stat.return_value = 25 + db.get_unlocked_achievements.return_value = ["first_friend"] + db.get_recent_activity.return_value = [ + (1, "2023-10-27 10:00:00", "app_start", "Приложение запущено"), + (2, "2023-10-27 10:05:00", "feed", "Котик покормлен"), + (3, "2023-10-27 10:10:00", "achievement", "Открыто достижение: Первый друг") + ] + + dialog = StatsDialog(db) + dialog.show() + + # Process events to ensure layout is done + app.processEvents() + + # Take screenshot of the first tab + pixmap = dialog.grab() + os.makedirs("verification/screenshots", exist_ok=True) + pixmap.save("verification/screenshots/stats_dialog_progress.png") + print("Screenshot saved to verification/screenshots/stats_dialog_progress.png") + + # Switch to the second tab (History) + dialog.tabs.setCurrentIndex(1) + app.processEvents() + + # Take screenshot of the second tab + pixmap = dialog.grab() + pixmap.save("verification/screenshots/stats_dialog_history.png") + print("Screenshot saved to verification/screenshots/stats_dialog_history.png") + +if __name__ == "__main__": + verify()