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
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@
**Инсайт:** Простое перечисление заблокированных достижений со статусом "заперто" демотивирует. Отображение прогресс-бара и числовых значений (7/10) внутри списка достижений значительно повышает вовлеченность пользователя.
**Действие:** В StatsDialog для каждого заблокированного достижения рассчитывать процент выполнения и отображать QProgressBar.

## 2026-07-06 - [Real-Time Pomodoro countdown with Dynamic Tooltips]
**Инсайт:** Стандартные таймеры без визуального таймера обратного отсчета снижают пользовательский контроль и ухудшают UX. Обновление обратного отсчета по секундам (1 Гц) практически не нагружает процессор, но значительно повышает удобство использования таймеров Pomodoro.
**Действие:** Переработать TimerSystem на посекундный шаг с сигналом `pomodoro_tick` и обновлять тултипы трея и основного окна динамически.
4 changes: 4 additions & 0 deletions src/core/input_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,10 @@ def periodic_check(self):
self.window.animation_manager.play_state("hunting")
self.window.start_hunting(self.last_mouse_pos[0], self.last_mouse_pos[1])

# 4. Обновление тултипа окна питомца
if hasattr(self.window, "update_tooltip"):
self.window.update_tooltip()

def add_points(self, points):
"""Добавляет очки и проверяет повышение уровня."""
if not self.db:
Expand Down
25 changes: 22 additions & 3 deletions src/core/timer_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
class TimerSystem(QObject):
stretch_reminder = Signal()
pomodoro_finished = Signal(str) # 'work' or 'break'
pomodoro_tick = Signal(int) # remaining seconds

def __init__(self, config):
super().__init__()
Expand All @@ -12,10 +13,11 @@ def __init__(self, config):
self.stretch_timer = QTimer(self)
self.stretch_timer.timeout.connect(self.on_stretch_timeout)

# Таймер Pomodoro
# Таймер Pomodoro (работает с секундным интервалом)
self.pomodoro_timer = QTimer(self)
self.pomodoro_timer.timeout.connect(self.on_pomodoro_timeout)
self.pomodoro_timer.timeout.connect(self.on_pomodoro_tick)
self.pomodoro_state = "idle" # 'work', 'break', 'idle'
self.pomodoro_remaining = 0

def start_stretch_timer(self):
interval = self.config.get("stretch_interval") * 60 * 1000 # в мс
Expand All @@ -31,10 +33,27 @@ def on_stretch_timeout(self):
def start_pomodoro(self, mode="work"):
self.pomodoro_state = mode
minutes = self.config.get(f"pomodoro_{mode}")
self.pomodoro_timer.start(minutes * 60 * 1000)
self.pomodoro_remaining = minutes * 60
self.pomodoro_timer.start(1000) # Секундный интервал
self.pomodoro_tick.emit(self.pomodoro_remaining)

def on_pomodoro_tick(self):
if self.pomodoro_remaining > 0:
self.pomodoro_remaining -= 1
self.pomodoro_tick.emit(self.pomodoro_remaining)
if self.pomodoro_remaining <= 0:
self.on_pomodoro_timeout()

def on_pomodoro_timeout(self):
last_state = self.pomodoro_state
self.pomodoro_timer.stop()
self.pomodoro_state = "idle"
self.pomodoro_remaining = 0
self.pomodoro_finished.emit(last_state)

def stop_pomodoro(self):
if self.pomodoro_state != "idle":
self.pomodoro_timer.stop()
self.pomodoro_state = "idle"
self.pomodoro_remaining = 0
self.pomodoro_tick.emit(0)
33 changes: 33 additions & 0 deletions src/ui/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,39 @@ def mouseReleaseEvent(self, event):
self.animation_manager.update_size(self.size())
self.animation_manager.play_state(self.last_state_before_drag)

def update_tooltip(self):
"""Обновляет интерактивный тултип окна питомца"""
if not self.input_manager:
return

# Получаем данные из InputManager и Config
name = self.config.get("username", "Котик") if self.config else "Котик"
points = self.input_manager.last_affection_points + self.input_manager.pending_points

# Получаем уровень и титул
from src.utils.bonding_utils import get_level_info
level, title, _, _ = get_level_info(points)

max_kps = self.input_manager.max_kps

# Статус таймера Pomodoro
pomodoro_text = "Не активен"
if self.timer_system and self.timer_system.pomodoro_state != "idle":
state_name = "Работа" if self.timer_system.pomodoro_state == "work" else "Перерыв"
rem = self.timer_system.pomodoro_remaining
mins = rem // 60
secs = rem % 60
pomodoro_text = f"{state_name} ({mins:02d}:{secs:02d})"

tooltip_content = (
f"Имя: {name}\n"
f"Уровень {level}: {title}\n"
f"Привязанность: {points} ❤️\n"
f"Рекорд: {max_kps} кл/сек ⚡\n"
f"Таймер Pomodoro: {pomodoro_text}"
)
self.setToolTip(tooltip_content)

def closeEvent(self, event):
self.closed.emit()
super().closeEvent(event)
Expand Down
45 changes: 45 additions & 0 deletions src/ui/tray_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ def __init__(self, pet_window):
self.window = pet_window

self.tray_icon = QSystemTrayIcon(self.window)
self.tray_icon.setToolTip("Десктопный Котик 🐾")
# Используем статичную PNG иконку для трея
if os.path.exists(TRAY_ICON_PATH):
self.tray_icon.setIcon(QIcon(TRAY_ICON_PATH))
Expand All @@ -25,6 +26,10 @@ def __init__(self, pet_window):
self.tray_icon.setContextMenu(self.menu)
self.tray_icon.show()

if self.window.timer_system:
self.window.timer_system.pomodoro_tick.connect(self.update_pomodoro_status)
self.window.timer_system.pomodoro_finished.connect(self.on_pomodoro_finished)

def setup_menu(self):
# Действия с питомцем
feed_action = QAction("Покормить", self)
Expand Down Expand Up @@ -61,6 +66,8 @@ def setup_menu(self):

# Pomodoro
pomodoro_menu = QMenu("Таймер Pomodoro", self.menu)
self.pomodoro_menu = pomodoro_menu

start_work = QAction("Начать работу (25 мин)", self)
start_work.triggered.connect(self.start_work_timer)
pomodoro_menu.addAction(start_work)
Expand All @@ -69,6 +76,15 @@ def setup_menu(self):
start_break.triggered.connect(self.start_break_timer)
pomodoro_menu.addAction(start_break)

self.pomodoro_status_action = QAction("Статус: Не активен", self)
self.pomodoro_status_action.setEnabled(False)
pomodoro_menu.addAction(self.pomodoro_status_action)

self.pomodoro_stop_action = QAction("Остановить таймер", self)
self.pomodoro_stop_action.triggered.connect(self.stop_pomodoro_timer)
self.pomodoro_stop_action.setVisible(False)
pomodoro_menu.addAction(self.pomodoro_stop_action)

self.menu.addMenu(pomodoro_menu)

self.menu.addSeparator()
Expand Down Expand Up @@ -136,6 +152,35 @@ def show_stats(self, checked=False):
dialog = StatsDialog(self.window.input_manager.db, self.window)
dialog.exec()

def stop_pomodoro_timer(self, checked=False):
if self.window.timer_system:
self.window.timer_system.stop_pomodoro()
self.window.show_message("Таймер остановлен ⏱️")

def update_pomodoro_status(self, remaining_seconds):
if not self.window.timer_system:
return

state = self.window.timer_system.pomodoro_state
if state == "idle" or remaining_seconds <= 0:
self.pomodoro_status_action.setText("Статус: Не активен")
self.pomodoro_stop_action.setVisible(False)
self.tray_icon.setToolTip("Десктопный Котик 🐾")
else:
mins = remaining_seconds // 60
secs = remaining_seconds % 60
state_str = "Работа" if state == "work" else "Перерыв"
time_str = f"{mins:02d}:{secs:02d}"

self.pomodoro_status_action.setText(f"Осталось ({state_str}): {time_str}")
self.pomodoro_stop_action.setVisible(True)
self.tray_icon.setToolTip(f"Десктопный Котик 🐾\n({state_str}: {time_str})")

def on_pomodoro_finished(self, mode):
self.pomodoro_status_action.setText("Статус: Не активен")
self.pomodoro_stop_action.setVisible(False)
self.tray_icon.setToolTip("Десктопный Котик 🐾")

def feed_pet(self, checked=False):
self.window.animation_manager.play_state("eating")
if self.window.input_manager:
Expand Down
82 changes: 82 additions & 0 deletions tests/test_timer_system.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import sys
import unittest
from PySide6.QtWidgets import QApplication
from src.core.timer_system import TimerSystem
from src.utils.config_manager import ConfigManager

# Ensure QApplication instance is created for QTimer
app = QApplication.instance() or QApplication(sys.argv)

class TestTimerSystem(unittest.TestCase):
def setUp(self):
# Create a mock config or ConfigManager
self.config = ConfigManager("test_timer_settings.json")
# Ensure our test settings are set
self.config.set("pomodoro_work", 25)
self.config.set("pomodoro_break", 5)
self.timer_system = TimerSystem(self.config)

def tearDown(self):
# Stop any active timers
self.timer_system.stretch_timer.stop()
self.timer_system.pomodoro_timer.stop()
import os
if os.path.exists("test_timer_settings.json"):
os.remove("test_timer_settings.json")

def test_initial_state(self):
self.assertEqual(self.timer_system.pomodoro_state, "idle")
self.assertEqual(self.timer_system.pomodoro_remaining, 0)

def test_start_pomodoro_work(self):
self.timer_system.start_pomodoro("work")
self.assertEqual(self.timer_system.pomodoro_state, "work")
self.assertEqual(self.timer_system.pomodoro_remaining, 25 * 60)
self.assertTrue(self.timer_system.pomodoro_timer.isActive())

def test_start_pomodoro_break(self):
self.timer_system.start_pomodoro("break")
self.assertEqual(self.timer_system.pomodoro_state, "break")
self.assertEqual(self.timer_system.pomodoro_remaining, 5 * 60)
self.assertTrue(self.timer_system.pomodoro_timer.isActive())

def test_stop_pomodoro(self):
self.timer_system.start_pomodoro("work")
self.timer_system.stop_pomodoro()
self.assertEqual(self.timer_system.pomodoro_state, "idle")
self.assertEqual(self.timer_system.pomodoro_remaining, 0)
self.assertFalse(self.timer_system.pomodoro_timer.isActive())

def test_pomodoro_tick(self):
ticks = []
self.timer_system.pomodoro_tick.connect(lambda val: ticks.append(val))

self.timer_system.start_pomodoro("work")
# Remaining starts at 25 * 60
self.assertEqual(self.timer_system.pomodoro_remaining, 1500)
self.assertEqual(len(ticks), 1)
self.assertEqual(ticks[0], 1500)

# Simulate a tick
self.timer_system.on_pomodoro_tick()
self.assertEqual(self.timer_system.pomodoro_remaining, 1499)
self.assertEqual(len(ticks), 2)
self.assertEqual(ticks[1], 1499)

def test_pomodoro_finish(self):
finished_modes = []
self.timer_system.pomodoro_finished.connect(lambda mode: finished_modes.append(mode))

self.timer_system.start_pomodoro("break")
self.timer_system.pomodoro_remaining = 1 # Set remaining to 1 second

# Simulate final tick to 0
self.timer_system.on_pomodoro_tick()
self.assertEqual(self.timer_system.pomodoro_remaining, 0)
self.assertEqual(self.timer_system.pomodoro_state, "idle")
self.assertEqual(len(finished_modes), 1)
self.assertEqual(finished_modes[0], "break")
self.assertFalse(self.timer_system.pomodoro_timer.isActive())

if __name__ == "__main__":
unittest.main()
Binary file modified verification/screenshots/stats_history.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.