[AI] Tracking
This commit is contained in:
@@ -26,6 +26,7 @@ class Application:
|
|||||||
self.is_idle: bool = False
|
self.is_idle: bool = False
|
||||||
self._tracking_thread: Optional[threading.Thread] = None
|
self._tracking_thread: Optional[threading.Thread] = None
|
||||||
self._running = True
|
self._running = True
|
||||||
|
self.session_start_time: Optional[float] = None
|
||||||
|
|
||||||
# Загружаем задачу
|
# Загружаем задачу
|
||||||
if self.config.current_task_id:
|
if self.config.current_task_id:
|
||||||
@@ -73,6 +74,7 @@ class Application:
|
|||||||
def _check_and_update_tracking(self):
|
def _check_and_update_tracking(self):
|
||||||
with self._lock:
|
with self._lock:
|
||||||
if not self.tracking_enabled or not self.current_task:
|
if not self.tracking_enabled or not self.current_task:
|
||||||
|
self.session_start_time = None
|
||||||
return
|
return
|
||||||
|
|
||||||
current_time = time.time()
|
current_time = time.time()
|
||||||
@@ -85,12 +87,17 @@ class Application:
|
|||||||
if current_time - self.last_activity > 60 or is_blocked:
|
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
|
||||||
|
self.session_start_time = None
|
||||||
else:
|
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
|
||||||
self._add_work_detail(group_name, 5.0)
|
self._add_work_detail(group_name, 5.0)
|
||||||
|
|
||||||
|
# Начинаем/продолжаем сессию
|
||||||
|
if self.session_start_time is None:
|
||||||
|
self.session_start_time = current_time
|
||||||
|
|
||||||
# Сохраняем при переходе в бездействие
|
# Сохраняем при переходе в бездействие
|
||||||
if not was_idle and self.is_idle:
|
if not was_idle and self.is_idle:
|
||||||
self.maybe_save_task()
|
self.maybe_save_task()
|
||||||
@@ -222,7 +229,11 @@ class Application:
|
|||||||
"detail": f"{window_info.title} | {window_info.process_name}"
|
"detail": f"{window_info.title} | {window_info.process_name}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# === Остальные методы (без изменений, но с локом где нужно) ===
|
def get_current_session_duration(self) -> float:
|
||||||
|
with self._lock:
|
||||||
|
if self.session_start_time is None:
|
||||||
|
return 0.0
|
||||||
|
return time.time() - self.session_start_time
|
||||||
|
|
||||||
def _load_or_create_config(self) -> TrackerConfig:
|
def _load_or_create_config(self) -> TrackerConfig:
|
||||||
if self.config_path.exists():
|
if self.config_path.exists():
|
||||||
|
|||||||
@@ -12,10 +12,11 @@ if TYPE_CHECKING:
|
|||||||
class OverlayWindow:
|
class OverlayWindow:
|
||||||
def __init__(self, app: "Application"):
|
def __init__(self, app: "Application"):
|
||||||
self.app = app
|
self.app = app
|
||||||
|
self.session_timer_label = None
|
||||||
self.root = tk.Tk()
|
self.root = tk.Tk()
|
||||||
self.root.protocol("WM_DELETE_WINDOW", self.on_close)
|
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("380x190")
|
||||||
self.root.overrideredirect(True) # убираем заголовок
|
self.root.overrideredirect(True) # убираем заголовок
|
||||||
self.root.attributes("-topmost", True) # всегда поверх
|
self.root.attributes("-topmost", True) # всегда поверх
|
||||||
self.root.attributes("-alpha", 0.5) # полупрозрачность по умолчанию
|
self.root.attributes("-alpha", 0.5) # полупрозрачность по умолчанию
|
||||||
@@ -48,9 +49,9 @@ class OverlayWindow:
|
|||||||
self.root.update_idletasks()
|
self.root.update_idletasks()
|
||||||
screen_width = self.root.winfo_screenwidth()
|
screen_width = self.root.winfo_screenwidth()
|
||||||
screen_height = self.root.winfo_screenheight()
|
screen_height = self.root.winfo_screenheight()
|
||||||
x = (screen_width // 2) - (320 // 2)
|
x = (screen_width // 2) - (380 // 2)
|
||||||
y = (screen_height // 2) - (180 // 2)
|
y = (screen_height // 2) - (190 // 2)
|
||||||
self.root.geometry(f"320x180+{x}+{y}")
|
self.root.geometry(f"380x190+{x}+{y}")
|
||||||
|
|
||||||
def _build_ui(self):
|
def _build_ui(self):
|
||||||
# Главный фрейм с отступами
|
# Главный фрейм с отступами
|
||||||
@@ -69,6 +70,9 @@ class OverlayWindow:
|
|||||||
self.time_info_label = Label(main_frame, text="Времени нет", anchor="w", font=("TkDefaultFont", 9))
|
self.time_info_label = Label(main_frame, text="Времени нет", anchor="w", font=("TkDefaultFont", 9))
|
||||||
self.time_info_label.pack(anchor="w")
|
self.time_info_label.pack(anchor="w")
|
||||||
|
|
||||||
|
self.session_timer_label = Label(main_frame, text="0:00", font=("Arial", 16, "bold"), fg="green")
|
||||||
|
self.session_timer_label.pack(pady=(5, 0))
|
||||||
|
|
||||||
# Текущее окно
|
# Текущее окно
|
||||||
self.window_group_label = Label(main_frame, text="Неизвестное приложение", anchor="w", fg="gray")
|
self.window_group_label = Label(main_frame, text="Неизвестное приложение", anchor="w", fg="gray")
|
||||||
self.window_group_label.pack(anchor="w")
|
self.window_group_label.pack(anchor="w")
|
||||||
@@ -89,6 +93,10 @@ class OverlayWindow:
|
|||||||
self.adjust_time_btn = Button(btn_frame, text="✏️ Время", command=self.on_adjust_time)
|
self.adjust_time_btn = Button(btn_frame, text="✏️ Время", command=self.on_adjust_time)
|
||||||
self.adjust_time_btn.pack(side="left", fill="x", expand=True, padx=(2, 0))
|
self.adjust_time_btn.pack(side="left", fill="x", expand=True, padx=(2, 0))
|
||||||
|
|
||||||
|
Button(btn_frame, text="📊 Отчёт", command=self.on_show_report).pack(side="left", fill="x", expand=True, padx=2)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
self._update_display()
|
self._update_display()
|
||||||
|
|
||||||
def _update_display(self):
|
def _update_display(self):
|
||||||
@@ -127,7 +135,13 @@ class OverlayWindow:
|
|||||||
|
|
||||||
def _auto_update(self):
|
def _auto_update(self):
|
||||||
self._update_display()
|
self._update_display()
|
||||||
self.root.after(2000, self._auto_update) # обновляем каждые 2 сек
|
|
||||||
|
# Обновляем таймер сессии каждую секунду
|
||||||
|
session_sec = self.app.get_current_session_duration()
|
||||||
|
session_str = format_seconds(session_sec)
|
||||||
|
self.session_timer_label.config(text=session_str)
|
||||||
|
|
||||||
|
self.root.after(500, self._auto_update) # теперь каждую секунду
|
||||||
|
|
||||||
# === Обработчики событий (пока без логики, только открытие окон) ===
|
# === Обработчики событий (пока без логики, только открытие окон) ===
|
||||||
|
|
||||||
@@ -146,6 +160,10 @@ class OverlayWindow:
|
|||||||
from .adjust_time_window import AdjustTimeWindow
|
from .adjust_time_window import AdjustTimeWindow
|
||||||
AdjustTimeWindow(self.root, self.app, self._update_display)
|
AdjustTimeWindow(self.root, self.app, self._update_display)
|
||||||
|
|
||||||
|
def on_show_report(self):
|
||||||
|
from .report_window import ReportWindow
|
||||||
|
ReportWindow(self.root, self.app)
|
||||||
|
|
||||||
# === UI-управление ===
|
# === UI-управление ===
|
||||||
|
|
||||||
def on_mouse_enter(self, event):
|
def on_mouse_enter(self, event):
|
||||||
|
|||||||
53
src/time_tracker/ui/report_window.py
Normal file
53
src/time_tracker/ui/report_window.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import tkinter as tk
|
||||||
|
from tkinter import Toplevel, Text, Scrollbar, Button
|
||||||
|
from ..utils import format_seconds
|
||||||
|
|
||||||
|
|
||||||
|
class ReportWindow:
|
||||||
|
def __init__(self, parent, app: "Application"):
|
||||||
|
self.window = Toplevel(parent)
|
||||||
|
self.window.title("Отчёт по задаче")
|
||||||
|
self.window.geometry("500x400")
|
||||||
|
self.window.transient(parent)
|
||||||
|
self.window.grab_set()
|
||||||
|
|
||||||
|
if not app.current_task:
|
||||||
|
tk.Label(self.window, text="Нет активной задачи", font=("Arial", 12)).pack(pady=20)
|
||||||
|
Button(self.window, text="Закрыть", command=self.window.destroy).pack()
|
||||||
|
return
|
||||||
|
|
||||||
|
task = app.current_task
|
||||||
|
text_widget = Text(self.window, wrap="word", padx=10, pady=10)
|
||||||
|
scrollbar = Scrollbar(self.window, command=text_widget.yview)
|
||||||
|
text_widget.config(yscrollcommand=scrollbar.set)
|
||||||
|
|
||||||
|
# Собираем отчёт
|
||||||
|
report = f"Задача: {task.task_id}\n"
|
||||||
|
if task.project_id:
|
||||||
|
report += f"Проект: {task.project_id}\n"
|
||||||
|
report += f"Всего затрачено: {format_seconds(task.total_worked_seconds)}\n\n"
|
||||||
|
|
||||||
|
# Группировка по группам
|
||||||
|
group_totals = {}
|
||||||
|
for session in task.sessions:
|
||||||
|
for detail in session.details:
|
||||||
|
group = detail.group_name
|
||||||
|
group_totals[group] = group_totals.get(group, 0) + detail.duration_seconds
|
||||||
|
|
||||||
|
report += "Распределение по группам:\n"
|
||||||
|
for group, total in sorted(group_totals.items(), key=lambda x: -x[1]):
|
||||||
|
report += f" • {group}: {format_seconds(total)}\n"
|
||||||
|
|
||||||
|
report += "\nПодробные сессии:\n"
|
||||||
|
for i, session in enumerate(task.sessions, 1):
|
||||||
|
report += f"\nСессия {i} ({session.start_time.strftime('%H:%M')}–{session.end_time.strftime('%H:%M')}):\n"
|
||||||
|
for detail in session.details:
|
||||||
|
report += f" - {detail.group_name}: {format_seconds(detail.duration_seconds)}\n"
|
||||||
|
|
||||||
|
text_widget.insert("1.0", report)
|
||||||
|
text_widget.config(state="disabled")
|
||||||
|
|
||||||
|
text_widget.pack(side="left", fill="both", expand=True)
|
||||||
|
scrollbar.pack(side="right", fill="y")
|
||||||
|
|
||||||
|
Button(self.window, text="Закрыть", command=self.window.destroy).pack(pady=10)
|
||||||
Reference in New Issue
Block a user