Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -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, чтобы не перегружать основной экран статистики.
31 changes: 20 additions & 11 deletions src/core/input_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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):
"""Регистрирует встряхивание котика."""
Expand Down Expand Up @@ -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")

Expand Down
78 changes: 57 additions & 21 deletions src/ui/stats_dialog.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,59 @@
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

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:
Expand All @@ -43,7 +66,7 @@ def __init__(self, db, parent=None):
else:
layout.addWidget(QLabel("Максимальный уровень достигнут! 🎉"))

layout.addSpacing(20)
layout.addSpacing(10)

# Раздел достижений
layout.addWidget(QLabel("<b>Достижения:</b>"))
Expand Down Expand Up @@ -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':
Expand Down Expand Up @@ -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)
6 changes: 6 additions & 0 deletions src/ui/tray_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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):
Expand Down
23 changes: 23 additions & 0 deletions src/utils/data_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Binary file added verification/screenshots/stats_dialog.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added verification/screenshots/stats_dialog_history.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
48 changes: 48 additions & 0 deletions verify_ui.py
Original file line number Diff line number Diff line change
@@ -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()