[AI] Tracking
This commit is contained in:
@@ -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,36 +75,25 @@ 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
|
||||||
|
if self.last_window_group != group_name:
|
||||||
|
self.last_window_group = group_name
|
||||||
|
self._add_work_detail(group_name, 5.0)
|
||||||
|
|
||||||
# Если рабочая группа — накапливаем время
|
# Сохраняем при переходе в бездействие
|
||||||
self.is_idle = False
|
if not was_idle and self.is_idle:
|
||||||
if self.last_window_group != group_name:
|
self.maybe_save_task()
|
||||||
# Новая группа — завершаем предыдущую сессию (если была)
|
|
||||||
self.last_window_group = group_name
|
|
||||||
|
|
||||||
# Добавляем 5 секунд к текущей группе
|
|
||||||
self._add_work_detail(group_name, 5.0)
|
|
||||||
|
|
||||||
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()
|
||||||
@@ -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'):
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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}"
|
||||||
Reference in New Issue
Block a user