[AI] Tracking

This commit is contained in:
Mikan
2026-03-10 02:29:37 +03:00
parent 07ed356cf1
commit bf51fc454d
3 changed files with 60 additions and 31 deletions

View File

@@ -40,15 +40,27 @@ class Application:
def _start_activity_listeners(self): def _start_activity_listeners(self):
def on_activity(*_): def on_activity(*_):
with self._lock: # Активность зафиксирована — но обработаем её только если окно рабочее
self.last_activity = time.time() self._check_and_update_activity()
self.is_idle = False
self.mouse_listener = mouse.Listener(on_move=on_activity, on_click=on_activity) self.mouse_listener = mouse.Listener(on_move=on_activity, on_click=on_activity)
self.keyboard_listener = keyboard.Listener(on_press=on_activity) self.keyboard_listener = keyboard.Listener(on_press=on_activity)
self.mouse_listener.start() self.mouse_listener.start()
self.keyboard_listener.start() self.keyboard_listener.start()
def _check_and_update_activity(self):
"""Обновляет last_activity, только если текущее окно — рабочее"""
window_info = self.platform.get_active_window()
group_name = self._match_window_to_policy(window_info)
is_blocked = self._is_group_blocked(group_name)
is_unknown = (group_name == "Неизвестное приложение")
# Считаем "рабочим", если не blocked и не unknown
if not is_blocked and not is_unknown:
with self._lock:
self.last_activity = time.time()
self.is_idle = False
def _start_tracking_loop(self): def _start_tracking_loop(self):
def loop(): def loop():
while self._running: while self._running:
@@ -63,37 +75,26 @@ class Application:
if not self.tracking_enabled or not self.current_task: if not self.tracking_enabled or not self.current_task:
return return
# Проверка: спит ли система? (упрощённо — если активность > 60 сек назад)
current_time = time.time() current_time = time.time()
if current_time - self.last_activity > 60:
self.is_idle = True
self.last_window_group = None
return
# Получаем текущее окно
window_info = self.platform.get_active_window() window_info = self.platform.get_active_window()
if not window_info.title and not window_info.process_name:
self.is_idle = True
return
# Определяем группу по политике
group_name = self._match_window_to_policy(window_info) group_name = self._match_window_to_policy(window_info)
is_blocked = self._is_group_blocked(group_name) is_blocked = self._is_group_blocked(group_name)
if is_blocked: was_idle = self.is_idle
if current_time - self.last_activity > 60 or is_blocked:
self.is_idle = True self.is_idle = True
self.last_window_group = None self.last_window_group = None
return else:
# Если рабочая группа — накапливаем время
self.is_idle = False self.is_idle = False
if self.last_window_group != group_name: if self.last_window_group != group_name:
# Новая группа — завершаем предыдущую сессию (если была)
self.last_window_group = group_name self.last_window_group = group_name
# Добавляем 5 секунд к текущей группе
self._add_work_detail(group_name, 5.0) self._add_work_detail(group_name, 5.0)
# Сохраняем при переходе в бездействие
if not was_idle and self.is_idle:
self.maybe_save_task()
def _match_window_to_policy(self, window_info) -> str: def _match_window_to_policy(self, window_info) -> str:
title = window_info.title.lower() title = window_info.title.lower()
proc = window_info.process_name.lower() proc = window_info.process_name.lower()
@@ -142,8 +143,14 @@ class Application:
# === Публичные методы с локом === # === Публичные методы с локом ===
def maybe_save_task(self):
"""Сохраняет текущую задачу, если она есть"""
if self.current_task:
self.save_task(self.current_task)
def toggle_tracking(self): def toggle_tracking(self):
with self._lock: with self._lock:
was_enabled = self.tracking_enabled
if not self.current_task: if not self.current_task:
self.tracking_enabled = False self.tracking_enabled = False
return return
@@ -152,6 +159,10 @@ class Application:
self.config.tracking_enabled = self.tracking_enabled self.config.tracking_enabled = self.tracking_enabled
self._save_config(self.config) self._save_config(self.config)
# Сохраняем при переходе в паузу
if was_enabled and not self.tracking_enabled:
self.maybe_save_task()
def create_task(self, task_id: str, project_id: Optional[str], estimated_minutes: Optional[float]): def create_task(self, task_id: str, project_id: Optional[str], estimated_minutes: Optional[float]):
with self._lock: with self._lock:
ensure_dir(self.tasks_dir) ensure_dir(self.tasks_dir)
@@ -245,6 +256,7 @@ class Application:
def shutdown(self): def shutdown(self):
self._running = False self._running = False
self.maybe_save_task() # ← сохраняем перед выходом
if hasattr(self, 'mouse_listener'): if hasattr(self, 'mouse_listener'):
self.mouse_listener.stop() self.mouse_listener.stop()
if hasattr(self, 'keyboard_listener'): if hasattr(self, 'keyboard_listener'):

View File

@@ -1,6 +1,9 @@
import tkinter as tk import tkinter as tk
from tkinter import Menu, Label, Button, Frame from tkinter import Menu, Label, Button, Frame
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from ..utils import format_seconds
if TYPE_CHECKING: if TYPE_CHECKING:
from ..app import Application from ..app import Application
@@ -10,6 +13,7 @@ class OverlayWindow:
def __init__(self, app: "Application"): def __init__(self, app: "Application"):
self.app = app self.app = app
self.root = tk.Tk() self.root = tk.Tk()
self.root.protocol("WM_DELETE_WINDOW", self.on_close)
self.root.title("Citrus Time Tracker") self.root.title("Citrus Time Tracker")
self.root.geometry("320x180") self.root.geometry("320x180")
self.root.overrideredirect(True) # убираем заголовок self.root.overrideredirect(True) # убираем заголовок
@@ -36,6 +40,10 @@ class OverlayWindow:
self._auto_update() self._auto_update()
def on_close(self):
self.app.shutdown()
self.root.destroy()
def center_window(self): def center_window(self):
self.root.update_idletasks() self.root.update_idletasks()
screen_width = self.root.winfo_screenwidth() screen_width = self.root.winfo_screenwidth()
@@ -92,14 +100,13 @@ class OverlayWindow:
# Время # Время
if self.app.current_task: if self.app.current_task:
worked = self.app.current_task.total_worked_seconds / 60 worked_str = format_seconds(self.app.current_task.total_worked_seconds)
est = self.app.current_task.estimated_seconds est = self.app.current_task.estimated_seconds
if est is not None: if est is not None:
est_min = est / 60 est_str = format_seconds(est)
rem = self.app.current_task.remaining_seconds / 60 time_info = f"Затрачено: {worked_str} | Оценка: {est_str}"
time_info = f"Затрачено: {worked:.1f} мин | Оценка: {est_min:.1f} мин"
else: else:
time_info = f"Затрачено: {worked:.1f} мин | Оценка: —" time_info = f"Затрачено: {worked_str} | Оценка: —"
else: else:
time_info = "Задача не выбрана" time_info = "Задача не выбрана"
self.time_info_label.config(text=time_info) self.time_info_label.config(text=time_info)

View File

@@ -14,3 +14,13 @@ def get_platform() -> PlatformBase:
def ensure_dir(path: Path) -> None: def ensure_dir(path: Path) -> None:
path.mkdir(parents=True, exist_ok=True) path.mkdir(parents=True, exist_ok=True)
def format_seconds(seconds: float) -> str:
seconds = int(abs(seconds))
h = seconds // 3600
m = (seconds % 3600) // 60
s = seconds % 60
if h > 0:
return f"{h}:{m:02d}:{s:02d}"
else:
return f"{m}:{s:02d}"