before refactor

This commit is contained in:
Mikan
2026-03-10 00:44:46 +03:00
parent 320f20b974
commit 346ea77fd2
3 changed files with 212 additions and 335 deletions

View File

@@ -11,54 +11,38 @@ try:
from .core.session_manager import SessionManager from .core.session_manager import SessionManager
from .ui.overlay import OverlayWindow from .ui.overlay import OverlayWindow
# Try to import platform-specific modules
import sys import sys
if sys.platform.startswith('win'): if sys.platform.startswith('win'):
from .platform.windows import get_active_window, register_session_listener, get_idle_time from .platform.windows import get_active_window, register_session_listener, get_idle_time
else: else:
# Fallback for other platforms
def get_active_window(): def get_active_window():
return "Unsupported platform" return "Unsupported platform"
def register_session_listener(callback): def register_session_listener(callback):
print("Session monitoring not supported on this platform") print("Session monitoring not supported")
def get_idle_time(): def get_idle_time():
return 0 return 0
except ImportError: except ImportError:
# For running as a single file
from core.config import load_config, save_config from core.config import load_config, save_config
from core.session_manager import SessionManager from core.session_manager import SessionManager
from ui.overlay import OverlayWindow from ui.overlay import OverlayWindow
import sys import sys
if sys.platform.startswith('win'): if sys.platform.startswith('win'):
from platform.windows import get_active_window, register_session_listener, get_idle_time from platform.windows import get_active_window, register_session_listener, get_idle_time
else: else:
def get_active_window(): def get_active_window():
return "Unsupported platform" return "Unsupported platform"
def register_session_listener(callback): def register_session_listener(callback):
print("Session monitoring not supported on this platform") print("Session monitoring not supported")
def get_idle_time(): def get_idle_time():
return 0 return 0
class ConfigWrapper: class ConfigWrapper:
"""Wrapper to allow attribute access to config dict"""
def __init__(self, config_dict: Dict): def __init__(self, config_dict: Dict):
self.__dict__.update(config_dict) self.__dict__.update(config_dict)
def update(self, new_config: Dict): def update(self, new_config: Dict):
"""Update the config and underlying dict"""
self.__dict__.update(new_config) self.__dict__.update(new_config)
@@ -74,261 +58,212 @@ class WorkTracker:
self.is_tracking = self.config.tracking_enabled self.is_tracking = self.config.tracking_enabled
self.current_task_id = self.config.current_task_id self.current_task_id = self.config.current_task_id
# Start monitoring in background # Thread safety
self._state_lock = threading.Lock()
self.start_monitoring() self.start_monitoring()
def start_monitoring(self): def start_monitoring(self):
"""Start background monitoring threads"""
# Thread for window tracking
window_thread = threading.Thread(target=self.monitor_windows, daemon=True) window_thread = threading.Thread(target=self.monitor_windows, daemon=True)
window_thread.start() window_thread.start()
# Thread for input activity tracking
input_thread = threading.Thread(target=self.monitor_input_activity, daemon=True) input_thread = threading.Thread(target=self.monitor_input_activity, daemon=True)
input_thread.start() input_thread.start()
# Register session listener
register_session_listener(self.on_session_change) register_session_listener(self.on_session_change)
def monitor_windows(self): def monitor_windows(self):
"""Monitor active windows and update classifications"""
while True: while True:
try: try:
current_window = get_active_window() current_window = get_active_window()
# Only update if window changed
if current_window != self.active_window: if current_window != self.active_window:
with self._state_lock:
self.active_window = current_window self.active_window = current_window
self.window_classification = self.classify_window(current_window) self.window_classification = self.classify_window(current_window)
# Show notification if it's a distraction or neutral app # Notifications will be shown via callbacks set in main()
if self.window_classification == "distraction":
self.show_distraction_notification(current_window)
elif self.window_classification == "neutral":
self.show_neutral_notification(current_window)
time.sleep(1) time.sleep(1)
except Exception as e: except Exception as e:
print(f"Error in window monitoring: {e}") print(f"Error in window monitoring: {e}")
time.sleep(5) time.sleep(5)
def monitor_input_activity(self): def monitor_input_activity(self):
"""Monitor keyboard and mouse activity"""
try: try:
from pynput import mouse, keyboard from pynput import mouse, keyboard
def on_activity(*args): def on_activity(*args):
self.last_activity = time.time() self.last_activity = time.time()
# Start listeners
mouse.Listener(on_move=on_activity, on_click=on_activity).start() mouse.Listener(on_move=on_activity, on_click=on_activity).start()
keyboard.Listener(on_press=on_activity).start() keyboard.Listener(on_press=on_activity).start()
except ImportError: except ImportError:
print("pynput not available, using fallback idle detection") print("pynput not available, using fallback idle detection")
# Fallback: just use system idle time # Fallback: periodically check system idle time
def idle_fallback():
while True:
try:
idle_sec = get_idle_time()
if idle_sec < 5: # Consider active if idle < 5 sec
self.last_activity = time.time()
except:
pass pass
time.sleep(2)
threading.Thread(target=idle_fallback, daemon=True).start()
def classify_window(self, title: str) -> str: def classify_window(self, title: str) -> str:
"""Classify window as work, distraction, or neutral"""
title_lower = title.lower() title_lower = title.lower()
rules = self.config.window_rules # Access through wrapper rules = self.config.window_rules
# Check distractions first
for keyword in rules.get("distraction", []): for keyword in rules.get("distraction", []):
if keyword.lower() in title_lower: if keyword.lower() in title_lower:
return "distraction" return "distraction"
# Then check work apps
for keyword in rules.get("work", []): for keyword in rules.get("work", []):
if keyword.lower() in title_lower: if keyword.lower() in title_lower:
return "work" return "work"
# Otherwise neutral
return "neutral" return "neutral"
def on_session_change(self, event_type: str): def on_session_change(self, event_type: str):
"""Handle session lock/unlock events"""
if event_type == "locked": if event_type == "locked":
self.session_locked = True self.session_locked = True
# End current session if active
if self.session_manager.current_session: if self.session_manager.current_session:
self.session_manager.end_current_session() self.session_manager.end_current_session()
elif event_type == "unlocked": elif event_type == "unlocked":
self.session_locked = False self.session_locked = False
self.last_activity = time.time() # Reset idle time on unlock self.last_activity = time.time()
def should_count_as_work(self) -> bool: def should_count_as_work(self) -> bool:
"""Check if current conditions should count as work time""" with self._state_lock:
# Check if tracking is enabled
if not self.is_tracking: if not self.is_tracking:
return False return False
# Check if session is locked
if self.session_locked: if self.session_locked:
return False return False
# Check idle time (more than 60 seconds of inactivity)
idle_time = time.time() - self.last_activity idle_time = time.time() - self.last_activity
if idle_time > 60: if idle_time > 60:
return False return False
return self.window_classification == "work"
# Check window classification
if self.window_classification != "work":
return False
return True
def run_main_loop(self): def run_main_loop(self):
"""Main tracking loop"""
while True: while True:
try: try:
if self.should_count_as_work(): if self.should_count_as_work():
# Increment current session duration
if self.session_manager.current_session: if self.session_manager.current_session:
self.session_manager.increment_duration(1) self.session_manager.increment_duration(1)
# Record time for current window
self.session_manager.record_window_time(self.active_window, 1) self.session_manager.record_window_time(self.active_window, 1)
time.sleep(1) time.sleep(1)
except Exception as e: except Exception as e:
print(f"Error in main loop: {e}") print(f"Error in main loop: {e}")
time.sleep(5) time.sleep(5)
def toggle_tracking(self): def toggle_tracking(self):
"""Toggle tracking on/off""" with self._state_lock:
self.is_tracking = not self.is_tracking self.is_tracking = not self.is_tracking
self.config.tracking_enabled = self.is_tracking self.config.tracking_enabled = self.is_tracking
# Update raw config too raw_config = {k: v for k, v in vars(self.config).items() if not k.startswith('_')}
raw_config = {key: value for key, value in vars(self.config).items() if not key.startswith('_')}
save_config(raw_config) save_config(raw_config)
def create_task(self, task_id: str): def create_task(self, task_id: str):
"""Create a new task and start tracking it"""
project_id = task_id.split('-')[0] if '-' in task_id else task_id project_id = task_id.split('-')[0] if '-' in task_id else task_id
self.session_manager.create_session(task_id, project_id) self.session_manager.create_session(task_id, project_id)
with self._state_lock:
self.current_task_id = task_id self.current_task_id = task_id
self.config.current_task_id = task_id self.config.current_task_id = task_id
raw_config = {key: value for key, value in vars(self.config).items() if not key.startswith('_')} raw_config = {k: v for k, v in vars(self.config).items() if not k.startswith('_')}
save_config(raw_config) save_config(raw_config)
def switch_task(self, task_id: str): def switch_task(self, task_id: str):
"""Switch to an existing task"""
# End current session if exists
if self.session_manager.current_session: if self.session_manager.current_session:
self.session_manager.end_current_session() self.session_manager.end_current_session()
# Start new session for the task
project_id = task_id.split('-')[0] if '-' in task_id else task_id project_id = task_id.split('-')[0] if '-' in task_id else task_id
self.session_manager.create_session(task_id, project_id) self.session_manager.create_session(task_id, project_id)
with self._state_lock:
self.current_task_id = task_id self.current_task_id = task_id
self.config.current_task_id = task_id self.config.current_task_id = task_id
raw_config = {key: value for key, value in vars(self.config).items() if not key.startswith('_')} raw_config = {k: v for k, v in vars(self.config).items() if not k.startswith('_')}
save_config(raw_config) save_config(raw_config)
def get_available_tasks(self) -> List[str]: def get_available_tasks(self) -> List[str]:
"""Get list of all available tasks"""
tasks_dir = "tasks" tasks_dir = "tasks"
if not os.path.exists(tasks_dir): if not os.path.exists(tasks_dir):
return [] return []
return [item for item in os.listdir(tasks_dir) if os.path.isdir(os.path.join(tasks_dir, item))]
tasks = []
for item in os.listdir(tasks_dir):
item_path = os.path.join(tasks_dir, item)
if os.path.isdir(item_path):
tasks.append(item)
return tasks
def has_active_session(self) -> bool: def has_active_session(self) -> bool:
"""Check if there's an active session"""
return self.session_manager.current_session is not None return self.session_manager.current_session is not None
def add_manual_adjustment(self, seconds: int, reason: str): def add_manual_adjustment(self, seconds: int, reason: str):
"""Add a manual time adjustment"""
if self.session_manager.current_session: if self.session_manager.current_session:
self.session_manager.add_manual_adjustment(seconds, reason) self.session_manager.add_manual_adjustment(seconds, reason)
def get_current_session_duration(self) -> int: def get_current_session_duration(self) -> int:
"""Get duration of current session"""
return self.session_manager.get_current_session_duration() return self.session_manager.get_current_session_duration()
def get_total_work_today(self) -> int: def get_total_work_today(self) -> int:
"""Get total work time for today"""
return self.session_manager.get_total_work_today() return self.session_manager.get_total_work_today()
def get_planned_duration(self, task_id: str) -> Optional[int]: def get_planned_duration(self, task_id: str) -> Optional[int]:
"""Get planned duration for a task (placeholder implementation)"""
# In a full implementation, this would look up the planned time for the task
# For now, return None to indicate no planned time
return None return None
def show_distraction_notification(self, window_title: str): # Notification methods will be replaced by callbacks in main()
"""Show notification about distraction window"""
# In a full implementation, this would show a GUI notification
print(f"DISTRACTION: {window_title}")
def show_neutral_notification(self, window_title: str):
"""Show notification about neutral window"""
# In a full implementation, this would show a GUI notification
print(f"NEUTRAL APP: {window_title}")
def main(): def setup_hotkeys(tracker, overlay):
# Create main Tkinter root
root = tk.Tk()
root.title("Work Tracker") # Give it a title so it appears in taskbar
root.geometry("1x1+2000+2000") # Minimal size off-screen to avoid flicker
# Don't call withdraw() so the window manager recognizes it
# Initialize tracker
tracker = WorkTracker()
# Create overlay window - this will be the visible always-on-top window
overlay = OverlayWindow(root, tracker, tracker.config)
# Start main tracking loop in background
main_loop_thread = threading.Thread(target=tracker.run_main_loop, daemon=True)
main_loop_thread.start()
# Set up hotkeys
try: try:
from pynput import keyboard from pynput import keyboard
def on_hotkey(key): def on_toggle():
try:
if key == keyboard.Key.alt_l: # Left Alt pressed
return # Wait for combination
elif hasattr(key, 'char') and key.char == 't':
# Ctrl+Alt+T: Toggle tracking
tracker.toggle_tracking() tracker.toggle_tracking()
overlay.update_display() overlay.update_display()
elif hasattr(key, 'char') and key.char == 'n':
# Ctrl+Alt+N: New task def on_new_task():
overlay.new_task() overlay.new_task()
elif hasattr(key, 'char') and key.char == 's':
# Ctrl+Alt+S: End session def on_end_session():
if tracker.session_manager.current_session: if tracker.session_manager.current_session:
tracker.session_manager.end_current_session() tracker.session_manager.end_current_session()
overlay.update_display() overlay.update_display()
elif hasattr(key, 'char') and key.char == 'm':
# Ctrl+Alt+M: Adjust time
overlay.adjust_time()
except AttributeError:
# Special keys like Ctrl, Alt don't have char attribute
pass
# Start keyboard listener def on_adjust():
keyboard.Listener(on_press=on_hotkey).start() overlay.adjust_time()
hotkey_map = {
'<ctrl>+<alt>+t': on_toggle,
'<ctrl>+<alt>+n': on_new_task,
'<ctrl>+<alt>+s': on_end_session,
'<ctrl>+<alt>+m': on_adjust,
}
listener = keyboard.GlobalHotKeys(hotkey_map)
listener.start()
return listener
except ImportError: except ImportError:
print("pynput not available, hotkeys disabled") print("pynput not available, hotkeys disabled")
return None
def main():
root = tk.Tk()
root.title("Work Tracker")
root.geometry("1x1+2000+2000")
tracker = WorkTracker()
overlay = OverlayWindow(root, tracker, tracker.config)
# Inject notification callbacks
tracker.show_distraction_notification = lambda title: overlay.show_notification("Отвлечение", f"Вы используете: {title}")
tracker.show_neutral_notification = lambda title: overlay.show_notification("Нейтральное приложение", f"Окно: {title}")
main_loop_thread = threading.Thread(target=tracker.run_main_loop, daemon=True)
main_loop_thread.start()
hotkey_listener = setup_hotkeys(tracker, overlay)
# Run the GUI
try: try:
root.mainloop() root.mainloop()
except KeyboardInterrupt: except KeyboardInterrupt:
print("\nShutting down...") print("\nShutting down...")
# End any current session before exiting
if tracker.session_manager.current_session: if tracker.session_manager.current_session:
tracker.session_manager.end_current_session() tracker.session_manager.end_current_session()

View File

@@ -1,33 +1,36 @@
import win32gui import win32gui
import win32process
import win32con import win32con
import win32api import win32api
import threading import threading
from typing import Callable from typing import Callable
import psutil
def get_active_window() -> str: def get_active_window() -> str:
"""Get the currently active window title on Windows""" """Returns a string like 'YouTube - Chrome (chrome.exe)'"""
try: try:
hwnd = win32gui.GetForegroundWindow() hwnd = win32gui.GetForegroundWindow()
return win32gui.GetWindowText(hwnd) or "Unknown Window" title = win32gui.GetWindowText(hwnd) or "Unknown Window"
except:
return "Error getting window title" # Get process name
_, pid = win32process.GetWindowThreadProcessId(hwnd)
try:
proc = psutil.Process(pid)
exe_name = proc.name()
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
exe_name = "unknown.exe"
return f"{title} ({exe_name})"
except Exception:
return "Error getting window info"
def register_session_listener(callback: Callable[[str], None]) -> None: def register_session_listener(callback: Callable[[str], None]) -> None:
"""Register a callback for Windows session lock/unlock events""" """Register a callback for Windows session lock/unlock events"""
def session_event_handler(hwnd, msg, wparam, lparam): def session_event_handler(hwnd, msg, wparam, lparam):
# Check if the constant exists before using it # Fallback numeric constants
try:
if msg == win32con.WM_WTSSESSION_CHANGE:
if wparam == getattr(win32con, 'WTS_SESSION_LOCK', 0x1):
callback("locked")
elif wparam == getattr(win32con, 'WTS_SESSION_UNLOCK', 0x2):
callback("unlocked")
except AttributeError:
# Some versions of pywin32 might not have these constants
# Use numeric values as fallback
WM_WTSSESSION_CHANGE = 0x02B1 WM_WTSSESSION_CHANGE = 0x02B1
WTS_SESSION_LOCK = 0x1 WTS_SESSION_LOCK = 0x1
WTS_SESSION_UNLOCK = 0x2 WTS_SESSION_UNLOCK = 0x2
@@ -52,33 +55,22 @@ def register_session_listener(callback: Callable[[str], None]) -> None:
except: except:
pass # Already registered pass # Already registered
# Corrected CreateWindow call with all required parameters
hwnd = win32gui.CreateWindow( hwnd = win32gui.CreateWindow(
wndclass.lpszClassName, # lpClassName wndclass.lpszClassName,
"Session Watcher", # lpWindowName "Session Watcher",
0, # dwStyle 0, 0, 0, 0, 0,
0, # x 0, 0, hinst, None
0, # y
0, # nWidth
0, # nHeight
0, # hWndParent
0, # hMenu
hinst, # hInstance
None # lParam
) )
# Register for session notifications # Register for session notifications
try: try:
from win32ts import WTSRegisterSessionNotification from win32ts import WTSRegisterSessionNotification
WTSRegisterSessionNotification(hwnd, 1) # NOTIFY_FOR_THIS_SESSION WTSRegisterSessionNotification(hwnd, 1)
except ImportError: except ImportError:
# pywin32 might not have win32ts on all systems
pass pass
# Start message loop
win32gui.PumpMessages() win32gui.PumpMessages()
# Run in a separate thread
thread = threading.Thread(target=run_message_loop, daemon=True) thread = threading.Thread(target=run_message_loop, daemon=True)
thread.start() thread.start()
@@ -95,4 +87,4 @@ def get_idle_time() -> int:
windll.user32.GetLastInputInfo(byref(lastInputInfo)) windll.user32.GetLastInputInfo(byref(lastInputInfo))
millis = windll.kernel32.GetTickCount() - lastInputInfo.dwTime millis = windll.kernel32.GetTickCount() - lastInputInfo.dwTime
return millis // 1000 # Convert milliseconds to seconds return millis // 1000

View File

@@ -2,7 +2,7 @@ import tkinter as tk
from tkinter import simpledialog, messagebox from tkinter import simpledialog, messagebox
import threading import threading
import time import time
from typing import Any, Callable from typing import Any
class OverlayWindow: class OverlayWindow:
@@ -11,108 +11,87 @@ class OverlayWindow:
self.tracker = tracker self.tracker = tracker
self.config = config self.config = config
# Configure window to be always on top # Always on top, no decorations
self.root.attributes("-topmost", True) self.root.attributes("-topmost", True)
self.root.overrideredirect(True) # Remove window decorations self.root.overrideredirect(True)
self.root.geometry("320x180+100+100")
# Force the geometry to be set correctly # Semi-transparent by default
self.root.geometry("300x150+100+100") self.root.wm_attributes("-alpha", 0.7)
# Hover effects
self.root.bind("<Enter>", self.on_hover)
self.root.bind("<Leave>", self.on_leave)
# Create UI elements
self.create_widgets() self.create_widgets()
self.setup_drag_full()
# Bind drag functionality
self.setup_drag()
# Bind right-click to show context menu for closing
self.root.bind("<Button-3>", self.show_context_menu) self.root.bind("<Button-3>", self.show_context_menu)
# Update display periodically # Start periodic updates
self.update_display_periodically() self.update_display_periodically()
def on_hover(self, event=None):
self.root.wm_attributes("-alpha", 1.0)
def on_leave(self, event=None):
self.root.wm_attributes("-alpha", 0.7)
def setup_drag_full(self):
"""Allow dragging the entire window from any point."""
def start_move(event):
self._drag_x = event.x
self._drag_y = event.y
def do_move(event):
x = self.root.winfo_x() + (event.x - self._drag_x)
y = self.root.winfo_y() + (event.y - self._drag_y)
self.root.geometry(f"+{x}+{y}")
self.root.bind("<Button-1>", start_move)
self.root.bind("<B1-Motion>", do_move)
def create_widgets(self): def create_widgets(self):
"""Create all UI widgets"""
# Main frame with visible background
main_frame = tk.Frame(self.root, bg='#f0f0f0', bd=2, relief='solid') main_frame = tk.Frame(self.root, bg='#f0f0f0', bd=2, relief='solid')
main_frame.pack(fill='both', expand=True, padx=2, pady=2) main_frame.pack(fill='both', expand=True, padx=2, pady=2)
# Status line (tracking enabled/disabled) self.status_label = tk.Label(main_frame, text="", bg='#f0f0f0', font=('Arial', 9))
self.status_label = tk.Label(main_frame, text="", bg='#f0f0f0', font=('Arial', 10))
self.status_label.pack(anchor='w', padx=5, pady=2) self.status_label.pack(anchor='w', padx=5, pady=2)
# Task info
self.task_label = tk.Label(main_frame, text="", bg='#f0f0f0', font=('Arial', 10, 'bold')) self.task_label = tk.Label(main_frame, text="", bg='#f0f0f0', font=('Arial', 10, 'bold'))
self.task_label.pack(anchor='w', padx=5, pady=2) self.task_label.pack(anchor='w', padx=5, pady=2)
# Window info
self.window_label = tk.Label(main_frame, text="", bg='#f0f0f0', font=('Arial', 9)) self.window_label = tk.Label(main_frame, text="", bg='#f0f0f0', font=('Arial', 9))
self.window_label.pack(anchor='w', padx=5, pady=2) self.window_label.pack(anchor='w', padx=5, pady=2)
# Total time info
self.total_label = tk.Label(main_frame, text="", bg='#f0f0f0', font=('Arial', 9)) self.total_label = tk.Label(main_frame, text="", bg='#f0f0f0', font=('Arial', 9))
self.total_label.pack(anchor='w', padx=5, pady=2) self.total_label.pack(anchor='w', padx=5, pady=2)
# Buttons frame
buttons_frame = tk.Frame(main_frame, bg='#f0f0f0') buttons_frame = tk.Frame(main_frame, bg='#f0f0f0')
buttons_frame.pack(fill='x', padx=5, pady=5) buttons_frame.pack(fill='x', padx=5, pady=5)
# Tracking toggle button self.track_btn = tk.Button(buttons_frame, text="", command=self.toggle_tracking, width=8)
self.track_btn = tk.Button(buttons_frame, text="", command=self.toggle_tracking, width=3)
self.track_btn.pack(side='left', padx=2) self.track_btn.pack(side='left', padx=2)
# New task button tk.Button(buttons_frame, text="+Задача", command=self.new_task, width=8).pack(side='left', padx=2)
new_task_btn = tk.Button(buttons_frame, text="N", command=self.new_task, width=3) tk.Button(buttons_frame, text="↔Задача", command=self.switch_task, width=8).pack(side='left', padx=2)
new_task_btn.pack(side='left', padx=2) tk.Button(buttons_frame, text="±Время", command=self.adjust_time, width=8).pack(side='left', padx=2)
# Switch task button
switch_task_btn = tk.Button(buttons_frame, text="S", command=self.switch_task, width=3)
switch_task_btn.pack(side='left', padx=2)
# Adjust time button
adjust_btn = tk.Button(buttons_frame, text="±", command=self.adjust_time, width=3)
adjust_btn.pack(side='left', padx=2)
def setup_drag(self):
"""Setup window dragging functionality"""
def start_move(event):
self.root.x = event.x
self.root.y = event.y
def do_move(event):
x = self.root.winfo_x() + (event.x - self.root.x)
y = self.root.winfo_y() + (event.y - self.root.y)
self.root.geometry(f"+{x}+{y}")
# Bind drag events to the main label (covers entire window)
self.status_label.bind("<Button-1>", start_move)
self.status_label.bind("<B1-Motion>", do_move)
# Also bind to other labels for full coverage
for widget in [self.task_label, self.window_label, self.total_label]:
widget.bind("<Button-1>", start_move)
widget.bind("<B1-Motion>", do_move)
def show_context_menu(self, event): def show_context_menu(self, event):
"""Show context menu with close option"""
context_menu = tk.Menu(self.root, tearoff=0) context_menu = tk.Menu(self.root, tearoff=0)
context_menu.add_command(label="Закрыть", command=self.root.quit) context_menu.add_command(label="Закрыть", command=self.root.quit)
context_menu.post(event.x_root, event.y_root) context_menu.post(event.x_root, event.y_root)
def toggle_tracking(self): def toggle_tracking(self):
"""Toggle tracking state"""
self.tracker.toggle_tracking() self.tracker.toggle_tracking()
self.update_display() self.update_display()
def new_task(self): def new_task(self):
"""Create a new task"""
task_id = simpledialog.askstring("Новая задача", "Введите ID задачи (например, PRJ-123):") task_id = simpledialog.askstring("Новая задача", "Введите ID задачи (например, PRJ-123):")
if task_id: if task_id and task_id.strip():
self.tracker.create_task(task_id) self.tracker.create_task(task_id.strip())
self.update_display() self.update_display()
def switch_task(self): def switch_task(self):
"""Switch to an existing task"""
available_tasks = self.tracker.get_available_tasks() available_tasks = self.tracker.get_available_tasks()
if not available_tasks: if not available_tasks:
messagebox.showinfo("Переключить задачу", "Нет доступных задач") messagebox.showinfo("Переключить задачу", "Нет доступных задач")
@@ -124,146 +103,117 @@ class OverlayWindow:
f"Доступные задачи:\n{task_list}\n\nВведите ID задачи:" f"Доступные задачи:\n{task_list}\n\nВведите ID задачи:"
) )
if selected_task and selected_task in available_tasks: if selected_task and selected_task.strip() in available_tasks:
self.tracker.switch_task(selected_task) self.tracker.switch_task(selected_task.strip())
self.update_display() self.update_display()
def adjust_time(self): def adjust_time(self):
"""Show dialog to adjust time manually"""
self.show_adjust_time_dialog() self.show_adjust_time_dialog()
def show_adjust_time_dialog(self): def show_adjust_time_dialog(self):
"""Show dialog for manual time adjustment"""
adjustment_window = tk.Toplevel(self.root) adjustment_window = tk.Toplevel(self.root)
adjustment_window.title("Ручная корректировка времени") adjustment_window.title("Ручная корректировка времени")
adjustment_window.geometry("300x150") adjustment_window.geometry("350x200")
adjustment_window.transient(self.root) adjustment_window.transient(self.root)
adjustment_window.grab_set() # Modal window adjustment_window.grab_set()
# Operation selection
operation_var = tk.StringVar(value="+") operation_var = tk.StringVar(value="+")
tk.Radiobutton(adjustment_window, text="Добавить время", variable=operation_var, value="+").pack(anchor='w', tk.Radiobutton(adjustment_window, text="Добавить время", variable=operation_var, value="+").pack(anchor='w', padx=10, pady=2)
padx=10, tk.Radiobutton(adjustment_window, text="Вычесть время", variable=operation_var, value="-").pack(anchor='w', padx=10, pady=2)
pady=5)
tk.Radiobutton(adjustment_window, text="Вычесть время", variable=operation_var, value="-").pack(anchor='w',
padx=10, pady=5)
# Minutes entry tk.Label(adjustment_window, text="Минуты:").pack(anchor='w', padx=10, pady=(10,0))
tk.Label(adjustment_window, text="Минут:").pack(anchor='w', padx=10)
minutes_entry = tk.Entry(adjustment_window) minutes_entry = tk.Entry(adjustment_window)
minutes_entry.pack(padx=10, pady=5) minutes_entry.pack(padx=10, pady=2)
minutes_entry.insert(0, "15") # Default value minutes_entry.insert(0, "15")
# Reason entry tk.Label(adjustment_window, text="Причина:").pack(anchor='w', padx=10, pady=(10,0))
tk.Label(adjustment_window, text="Причина:").pack(anchor='w', padx=10)
reason_entry = tk.Entry(adjustment_window) reason_entry = tk.Entry(adjustment_window)
reason_entry.pack(padx=10, pady=5) reason_entry.pack(padx=10, pady=2)
reason_entry.insert(0, "Ручная корректировка") # Default value reason_entry.insert(0, "Перерыв / встреча")
def apply_adjustment(): def apply_adjustment():
try: try:
minutes = int(minutes_entry.get()) minutes = int(minutes_entry.get())
if minutes <= 0:
raise ValueError("Minutes must be positive")
operation = operation_var.get() operation = operation_var.get()
seconds = minutes * 60 seconds = minutes * 60 * (1 if operation == "+" else -1)
if operation == "-": reason = reason_entry.get().strip()
seconds = -seconds if not reason:
reason = "Без причины"
reason = reason_entry.get()
if self.tracker.has_active_session(): if self.tracker.has_active_session():
self.tracker.add_manual_adjustment(seconds, reason) self.tracker.add_manual_adjustment(seconds, reason)
messagebox.showinfo("Успешно", f"Время скорректировано: {operation}{minutes} мин") messagebox.showinfo("Успех", f"Изменено: {operation}{minutes} мин\nПричина: {reason}")
adjustment_window.destroy() adjustment_window.destroy()
self.update_display() self.update_display()
else: else:
messagebox.showwarning("Внимание", "Нет активной сессии для корректировки") messagebox.showwarning("Ошибка", "Нет активной сессии!")
except ValueError: except ValueError:
messagebox.showerror("Ошибка", "Введите корректное число минут") messagebox.showerror("Ошибка", "Введите положительное число минут")
# Apply button tk.Button(adjustment_window, text="Применить", command=apply_adjustment, bg='lightblue').pack(pady=10)
apply_btn = tk.Button(adjustment_window, text="Применить", command=apply_adjustment)
apply_btn.pack(pady=10)
def update_display(self): def update_display(self):
"""Update the display with current information""" # Use lock to avoid race conditions
# Update tracking status with self.tracker._state_lock:
track_status = "" if self.config.tracking_enabled else "" track_status = "" if self.config.tracking_enabled else ""
status_text = f"[{track_status}] Трекинг: {'ВКЛ' if self.config.tracking_enabled else 'ВЫКЛ'}" status_text = f"[{track_status}] Трекинг: {'ВКЛ' if self.config.tracking_enabled else 'ВЫКЛ'}"
self.status_label.config(text=status_text) self.status_label.config(text=status_text)
# Update task information
if self.config.current_task_id: if self.config.current_task_id:
# Get current session duration
current_duration = self.tracker.get_current_session_duration() current_duration = self.tracker.get_current_session_duration()
current_duration_str = self.format_duration(current_duration) current_str = self.format_duration(current_duration)
task_text = f"Задача: {self.config.current_task_id} | {current_str}"
# Get planned duration if available
planned_duration = self.tracker.get_planned_duration(self.config.current_task_id)
if planned_duration:
planned_str = self.format_duration(planned_duration * 60) # Convert minutes to seconds
task_text = f"Задача: {self.config.current_task_id} | {current_duration_str} / {planned_str}"
else:
task_text = f"Задача: {self.config.current_task_id} | {current_duration_str}"
else: else:
task_text = "Нет активной задачи" task_text = "Нет активной задачи"
self.task_label.config(text=task_text) self.task_label.config(text=task_text)
# Update window information
if hasattr(self.tracker, 'active_window'):
window_title = getattr(self.tracker, 'active_window', 'Неизвестно') window_title = getattr(self.tracker, 'active_window', 'Неизвестно')
classification = getattr(self.tracker, 'window_classification', 'neutral') classification = getattr(self.tracker, 'window_classification', 'neutral')
class_symbol = {"work": "", "distraction": "", "neutral": ""}[classification] class_symbol = {"work": "", "distraction": "", "neutral": ""}[classification]
window_text = f"Окно: {window_title[:30]}{'...' if len(window_title) > 30 else ''} {class_symbol}" display_title = window_title[:40] + '...' if len(window_title) > 40 else window_title
window_text = f"Окно: {display_title} {class_symbol}"
self.window_label.config(text=window_text) self.window_label.config(text=window_text)
else:
self.window_label.config(text="Окно: Неизвестно")
# Update total time today
total_today = self.tracker.get_total_work_today() total_today = self.tracker.get_total_work_today()
total_str = self.format_duration(total_today) total_str = self.format_duration(total_today)
self.total_label.config(text=f"Всего сегодня: {total_str}") self.total_label.config(text=f"Сегодня: {total_str}")
# Update button colors based on tracking status # Update button appearance
if self.config.tracking_enabled: self.track_btn.config(
self.track_btn.config(bg='lightgreen', text="") text="" if self.config.tracking_enabled else "",
else: bg='lightgreen' if self.config.tracking_enabled else 'lightcoral'
self.track_btn.config(bg='lightcoral', text="") )
def format_duration(self, seconds: int) -> str: def format_duration(self, seconds: int) -> str:
"""Format seconds into human-readable string (h:mm)""" """Format as H:MM:SS or M:SS"""
if seconds < 0: if seconds < 0:
seconds = 0 seconds = 0
hours, remainder = divmod(seconds, 3600)
hours = seconds // 3600 minutes, secs = divmod(remainder, 60)
minutes = (seconds % 3600) // 60
if hours > 0: if hours > 0:
return f"{hours}ч{minutes:02d}м" return f"{hours}:{minutes:02d}:{secs:02d}"
else: else:
return f"{minutes}м" return f"{minutes}:{secs:02d}"
def update_display_periodically(self): def update_display_periodically(self):
"""Update display every second"""
self.update_display() self.update_display()
self.root.after(1000, self.update_display_periodically) self.root.after(1000, self.update_display_periodically)
def show_notification(self, title: str, message: str, duration: int = 5000): def show_notification(self, title: str, message: str, duration: int = 5000):
"""Show a temporary notification""" """Show a temporary notification window"""
# Create notification window
notif = tk.Toplevel(self.root) notif = tk.Toplevel(self.root)
notif.title(title) notif.title(title)
notif.geometry("250x80+200+200") notif.geometry("280x90+200+200")
notif.overrideredirect(True) notif.overrideredirect(True)
notif.attributes("-topmost", True) notif.attributes("-topmost", True)
notif.wm_attributes("-alpha", 0.9)
# Style similar to main window
frame = tk.Frame(notif, bg='lightyellow', bd=2, relief='solid') frame = tk.Frame(notif, bg='lightyellow', bd=2, relief='solid')
frame.pack(fill='both', expand=True) frame.pack(fill='both', expand=True)
tk.Label(frame, text=title, bg='lightyellow', font=('Arial', 10, 'bold')).pack(pady=5) tk.Label(frame, text=title, bg='lightyellow', font=('Arial', 10, 'bold')).pack(pady=5)
tk.Label(frame, text=message, bg='lightyellow', font=('Arial', 9)).pack(pady=5) tk.Label(frame, text=message[:80], bg='lightyellow', font=('Arial', 9), wraplength=260).pack(pady=5)
# Close after specified duration
notif.after(duration, notif.destroy) notif.after(duration, notif.destroy)