before refactor
This commit is contained in:
@@ -11,54 +11,38 @@ try:
|
||||
from .core.session_manager import SessionManager
|
||||
from .ui.overlay import OverlayWindow
|
||||
|
||||
# Try to import platform-specific modules
|
||||
import sys
|
||||
|
||||
if sys.platform.startswith('win'):
|
||||
from .platform.windows import get_active_window, register_session_listener, get_idle_time
|
||||
else:
|
||||
# Fallback for other platforms
|
||||
def get_active_window():
|
||||
return "Unsupported platform"
|
||||
|
||||
|
||||
def register_session_listener(callback):
|
||||
print("Session monitoring not supported on this platform")
|
||||
|
||||
|
||||
print("Session monitoring not supported")
|
||||
def get_idle_time():
|
||||
return 0
|
||||
except ImportError:
|
||||
# For running as a single file
|
||||
from core.config import load_config, save_config
|
||||
from core.session_manager import SessionManager
|
||||
from ui.overlay import OverlayWindow
|
||||
|
||||
import sys
|
||||
|
||||
if sys.platform.startswith('win'):
|
||||
from platform.windows import get_active_window, register_session_listener, get_idle_time
|
||||
else:
|
||||
def get_active_window():
|
||||
return "Unsupported platform"
|
||||
|
||||
|
||||
def register_session_listener(callback):
|
||||
print("Session monitoring not supported on this platform")
|
||||
|
||||
|
||||
print("Session monitoring not supported")
|
||||
def get_idle_time():
|
||||
return 0
|
||||
|
||||
|
||||
class ConfigWrapper:
|
||||
"""Wrapper to allow attribute access to config dict"""
|
||||
|
||||
def __init__(self, config_dict: Dict):
|
||||
self.__dict__.update(config_dict)
|
||||
|
||||
def update(self, new_config: Dict):
|
||||
"""Update the config and underlying dict"""
|
||||
self.__dict__.update(new_config)
|
||||
|
||||
|
||||
@@ -74,261 +58,212 @@ class WorkTracker:
|
||||
self.is_tracking = self.config.tracking_enabled
|
||||
self.current_task_id = self.config.current_task_id
|
||||
|
||||
# Start monitoring in background
|
||||
# Thread safety
|
||||
self._state_lock = threading.Lock()
|
||||
|
||||
self.start_monitoring()
|
||||
|
||||
def start_monitoring(self):
|
||||
"""Start background monitoring threads"""
|
||||
# Thread for window tracking
|
||||
window_thread = threading.Thread(target=self.monitor_windows, daemon=True)
|
||||
window_thread.start()
|
||||
|
||||
# Thread for input activity tracking
|
||||
input_thread = threading.Thread(target=self.monitor_input_activity, daemon=True)
|
||||
input_thread.start()
|
||||
|
||||
# Register session listener
|
||||
register_session_listener(self.on_session_change)
|
||||
|
||||
def monitor_windows(self):
|
||||
"""Monitor active windows and update classifications"""
|
||||
while True:
|
||||
try:
|
||||
current_window = get_active_window()
|
||||
|
||||
# Only update if window changed
|
||||
if current_window != self.active_window:
|
||||
self.active_window = current_window
|
||||
self.window_classification = self.classify_window(current_window)
|
||||
|
||||
# Show notification if it's a distraction or neutral app
|
||||
if self.window_classification == "distraction":
|
||||
self.show_distraction_notification(current_window)
|
||||
elif self.window_classification == "neutral":
|
||||
self.show_neutral_notification(current_window)
|
||||
with self._state_lock:
|
||||
self.active_window = current_window
|
||||
self.window_classification = self.classify_window(current_window)
|
||||
|
||||
# Notifications will be shown via callbacks set in main()
|
||||
time.sleep(1)
|
||||
except Exception as e:
|
||||
print(f"Error in window monitoring: {e}")
|
||||
time.sleep(5)
|
||||
|
||||
def monitor_input_activity(self):
|
||||
"""Monitor keyboard and mouse activity"""
|
||||
try:
|
||||
from pynput import mouse, keyboard
|
||||
|
||||
def on_activity(*args):
|
||||
self.last_activity = time.time()
|
||||
|
||||
# Start listeners
|
||||
mouse.Listener(on_move=on_activity, on_click=on_activity).start()
|
||||
keyboard.Listener(on_press=on_activity).start()
|
||||
except ImportError:
|
||||
print("pynput not available, using fallback idle detection")
|
||||
# Fallback: just use system idle time
|
||||
pass
|
||||
# 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
|
||||
time.sleep(2)
|
||||
threading.Thread(target=idle_fallback, daemon=True).start()
|
||||
|
||||
def classify_window(self, title: str) -> str:
|
||||
"""Classify window as work, distraction, or neutral"""
|
||||
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", []):
|
||||
if keyword.lower() in title_lower:
|
||||
return "distraction"
|
||||
|
||||
# Then check work apps
|
||||
for keyword in rules.get("work", []):
|
||||
if keyword.lower() in title_lower:
|
||||
return "work"
|
||||
|
||||
# Otherwise neutral
|
||||
return "neutral"
|
||||
|
||||
def on_session_change(self, event_type: str):
|
||||
"""Handle session lock/unlock events"""
|
||||
if event_type == "locked":
|
||||
self.session_locked = True
|
||||
# End current session if active
|
||||
if self.session_manager.current_session:
|
||||
self.session_manager.end_current_session()
|
||||
elif event_type == "unlocked":
|
||||
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:
|
||||
"""Check if current conditions should count as work time"""
|
||||
# Check if tracking is enabled
|
||||
if not self.is_tracking:
|
||||
return False
|
||||
|
||||
# Check if session is locked
|
||||
if self.session_locked:
|
||||
return False
|
||||
|
||||
# Check idle time (more than 60 seconds of inactivity)
|
||||
idle_time = time.time() - self.last_activity
|
||||
if idle_time > 60:
|
||||
return False
|
||||
|
||||
# Check window classification
|
||||
if self.window_classification != "work":
|
||||
return False
|
||||
|
||||
return True
|
||||
with self._state_lock:
|
||||
if not self.is_tracking:
|
||||
return False
|
||||
if self.session_locked:
|
||||
return False
|
||||
idle_time = time.time() - self.last_activity
|
||||
if idle_time > 60:
|
||||
return False
|
||||
return self.window_classification == "work"
|
||||
|
||||
def run_main_loop(self):
|
||||
"""Main tracking loop"""
|
||||
while True:
|
||||
try:
|
||||
if self.should_count_as_work():
|
||||
# Increment current session duration
|
||||
if self.session_manager.current_session:
|
||||
self.session_manager.increment_duration(1)
|
||||
|
||||
# Record time for current window
|
||||
self.session_manager.record_window_time(self.active_window, 1)
|
||||
|
||||
time.sleep(1)
|
||||
except Exception as e:
|
||||
print(f"Error in main loop: {e}")
|
||||
time.sleep(5)
|
||||
|
||||
def toggle_tracking(self):
|
||||
"""Toggle tracking on/off"""
|
||||
self.is_tracking = not self.is_tracking
|
||||
self.config.tracking_enabled = self.is_tracking
|
||||
# Update raw config too
|
||||
raw_config = {key: value for key, value in vars(self.config).items() if not key.startswith('_')}
|
||||
save_config(raw_config)
|
||||
with self._state_lock:
|
||||
self.is_tracking = not self.is_tracking
|
||||
self.config.tracking_enabled = self.is_tracking
|
||||
raw_config = {k: v for k, v in vars(self.config).items() if not k.startswith('_')}
|
||||
save_config(raw_config)
|
||||
|
||||
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
|
||||
self.session_manager.create_session(task_id, project_id)
|
||||
self.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('_')}
|
||||
with self._state_lock:
|
||||
self.current_task_id = task_id
|
||||
self.config.current_task_id = task_id
|
||||
raw_config = {k: v for k, v in vars(self.config).items() if not k.startswith('_')}
|
||||
save_config(raw_config)
|
||||
|
||||
def switch_task(self, task_id: str):
|
||||
"""Switch to an existing task"""
|
||||
# End current session if exists
|
||||
if self.session_manager.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
|
||||
self.session_manager.create_session(task_id, project_id)
|
||||
self.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('_')}
|
||||
with self._state_lock:
|
||||
self.current_task_id = task_id
|
||||
self.config.current_task_id = task_id
|
||||
raw_config = {k: v for k, v in vars(self.config).items() if not k.startswith('_')}
|
||||
save_config(raw_config)
|
||||
|
||||
def get_available_tasks(self) -> List[str]:
|
||||
"""Get list of all available tasks"""
|
||||
tasks_dir = "tasks"
|
||||
if not os.path.exists(tasks_dir):
|
||||
return []
|
||||
|
||||
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
|
||||
return [item for item in os.listdir(tasks_dir) if os.path.isdir(os.path.join(tasks_dir, item))]
|
||||
|
||||
def has_active_session(self) -> bool:
|
||||
"""Check if there's an active session"""
|
||||
return self.session_manager.current_session is not None
|
||||
|
||||
def add_manual_adjustment(self, seconds: int, reason: str):
|
||||
"""Add a manual time adjustment"""
|
||||
if self.session_manager.current_session:
|
||||
self.session_manager.add_manual_adjustment(seconds, reason)
|
||||
|
||||
def get_current_session_duration(self) -> int:
|
||||
"""Get duration of current session"""
|
||||
return self.session_manager.get_current_session_duration()
|
||||
|
||||
def get_total_work_today(self) -> int:
|
||||
"""Get total work time for today"""
|
||||
return self.session_manager.get_total_work_today()
|
||||
|
||||
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
|
||||
|
||||
def show_distraction_notification(self, window_title: str):
|
||||
"""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}")
|
||||
# Notification methods will be replaced by callbacks in main()
|
||||
|
||||
|
||||
def main():
|
||||
# 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
|
||||
def setup_hotkeys(tracker, overlay):
|
||||
try:
|
||||
from pynput import keyboard
|
||||
|
||||
def on_hotkey(key):
|
||||
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()
|
||||
overlay.update_display()
|
||||
elif hasattr(key, 'char') and key.char == 'n':
|
||||
# Ctrl+Alt+N: New task
|
||||
overlay.new_task()
|
||||
elif hasattr(key, 'char') and key.char == 's':
|
||||
# Ctrl+Alt+S: End session
|
||||
if tracker.session_manager.current_session:
|
||||
tracker.session_manager.end_current_session()
|
||||
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
|
||||
def on_toggle():
|
||||
tracker.toggle_tracking()
|
||||
overlay.update_display()
|
||||
|
||||
# Start keyboard listener
|
||||
keyboard.Listener(on_press=on_hotkey).start()
|
||||
def on_new_task():
|
||||
overlay.new_task()
|
||||
|
||||
def on_end_session():
|
||||
if tracker.session_manager.current_session:
|
||||
tracker.session_manager.end_current_session()
|
||||
overlay.update_display()
|
||||
|
||||
def on_adjust():
|
||||
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:
|
||||
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:
|
||||
root.mainloop()
|
||||
except KeyboardInterrupt:
|
||||
print("\nShutting down...")
|
||||
# End any current session before exiting
|
||||
if tracker.session_manager.current_session:
|
||||
tracker.session_manager.end_current_session()
|
||||
|
||||
|
||||
@@ -1,42 +1,45 @@
|
||||
import win32gui
|
||||
import win32process
|
||||
import win32con
|
||||
import win32api
|
||||
import threading
|
||||
from typing import Callable
|
||||
import psutil
|
||||
|
||||
|
||||
def get_active_window() -> str:
|
||||
"""Get the currently active window title on Windows"""
|
||||
"""Returns a string like 'YouTube - Chrome (chrome.exe)'"""
|
||||
try:
|
||||
hwnd = win32gui.GetForegroundWindow()
|
||||
return win32gui.GetWindowText(hwnd) or "Unknown Window"
|
||||
except:
|
||||
return "Error getting window title"
|
||||
title = win32gui.GetWindowText(hwnd) or "Unknown Window"
|
||||
|
||||
# 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:
|
||||
"""Register a callback for Windows session lock/unlock events"""
|
||||
|
||||
def session_event_handler(hwnd, msg, wparam, lparam):
|
||||
# Check if the constant exists before using it
|
||||
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
|
||||
WTS_SESSION_LOCK = 0x1
|
||||
WTS_SESSION_UNLOCK = 0x2
|
||||
# Fallback numeric constants
|
||||
WM_WTSSESSION_CHANGE = 0x02B1
|
||||
WTS_SESSION_LOCK = 0x1
|
||||
WTS_SESSION_UNLOCK = 0x2
|
||||
|
||||
if msg == WM_WTSSESSION_CHANGE:
|
||||
if wparam == WTS_SESSION_LOCK:
|
||||
callback("locked")
|
||||
elif wparam == WTS_SESSION_UNLOCK:
|
||||
callback("unlocked")
|
||||
if msg == WM_WTSSESSION_CHANGE:
|
||||
if wparam == WTS_SESSION_LOCK:
|
||||
callback("locked")
|
||||
elif wparam == WTS_SESSION_UNLOCK:
|
||||
callback("unlocked")
|
||||
|
||||
return win32gui.DefWindowProc(hwnd, msg, wparam, lparam)
|
||||
|
||||
@@ -52,33 +55,22 @@ def register_session_listener(callback: Callable[[str], None]) -> None:
|
||||
except:
|
||||
pass # Already registered
|
||||
|
||||
# Corrected CreateWindow call with all required parameters
|
||||
hwnd = win32gui.CreateWindow(
|
||||
wndclass.lpszClassName, # lpClassName
|
||||
"Session Watcher", # lpWindowName
|
||||
0, # dwStyle
|
||||
0, # x
|
||||
0, # y
|
||||
0, # nWidth
|
||||
0, # nHeight
|
||||
0, # hWndParent
|
||||
0, # hMenu
|
||||
hinst, # hInstance
|
||||
None # lParam
|
||||
wndclass.lpszClassName,
|
||||
"Session Watcher",
|
||||
0, 0, 0, 0, 0,
|
||||
0, 0, hinst, None
|
||||
)
|
||||
|
||||
# Register for session notifications
|
||||
try:
|
||||
from win32ts import WTSRegisterSessionNotification
|
||||
WTSRegisterSessionNotification(hwnd, 1) # NOTIFY_FOR_THIS_SESSION
|
||||
WTSRegisterSessionNotification(hwnd, 1)
|
||||
except ImportError:
|
||||
# pywin32 might not have win32ts on all systems
|
||||
pass
|
||||
|
||||
# Start message loop
|
||||
win32gui.PumpMessages()
|
||||
|
||||
# Run in a separate thread
|
||||
thread = threading.Thread(target=run_message_loop, daemon=True)
|
||||
thread.start()
|
||||
|
||||
@@ -95,4 +87,4 @@ def get_idle_time() -> int:
|
||||
windll.user32.GetLastInputInfo(byref(lastInputInfo))
|
||||
|
||||
millis = windll.kernel32.GetTickCount() - lastInputInfo.dwTime
|
||||
return millis // 1000 # Convert milliseconds to seconds
|
||||
return millis // 1000
|
||||
@@ -2,7 +2,7 @@ import tkinter as tk
|
||||
from tkinter import simpledialog, messagebox
|
||||
import threading
|
||||
import time
|
||||
from typing import Any, Callable
|
||||
from typing import Any
|
||||
|
||||
|
||||
class OverlayWindow:
|
||||
@@ -11,108 +11,87 @@ class OverlayWindow:
|
||||
self.tracker = tracker
|
||||
self.config = config
|
||||
|
||||
# Configure window to be always on top
|
||||
# Always on top, no decorations
|
||||
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
|
||||
self.root.geometry("300x150+100+100")
|
||||
# Semi-transparent by default
|
||||
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()
|
||||
|
||||
# Bind drag functionality
|
||||
self.setup_drag()
|
||||
|
||||
# Bind right-click to show context menu for closing
|
||||
self.setup_drag_full()
|
||||
self.root.bind("<Button-3>", self.show_context_menu)
|
||||
|
||||
# Update display periodically
|
||||
# Start periodic updates
|
||||
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):
|
||||
"""Create all UI widgets"""
|
||||
# Main frame with visible background
|
||||
main_frame = tk.Frame(self.root, bg='#f0f0f0', bd=2, relief='solid')
|
||||
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', 10))
|
||||
self.status_label = tk.Label(main_frame, text="", bg='#f0f0f0', font=('Arial', 9))
|
||||
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.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.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.pack(anchor='w', padx=5, pady=2)
|
||||
|
||||
# Buttons frame
|
||||
buttons_frame = tk.Frame(main_frame, bg='#f0f0f0')
|
||||
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=3)
|
||||
self.track_btn = tk.Button(buttons_frame, text="▶", command=self.toggle_tracking, width=8)
|
||||
self.track_btn.pack(side='left', padx=2)
|
||||
|
||||
# New task button
|
||||
new_task_btn = tk.Button(buttons_frame, text="N", command=self.new_task, width=3)
|
||||
new_task_btn.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)
|
||||
tk.Button(buttons_frame, text="+Задача", command=self.new_task, width=8).pack(side='left', padx=2)
|
||||
tk.Button(buttons_frame, text="↔Задача", command=self.switch_task, width=8).pack(side='left', padx=2)
|
||||
tk.Button(buttons_frame, text="±Время", command=self.adjust_time, width=8).pack(side='left', padx=2)
|
||||
|
||||
def show_context_menu(self, event):
|
||||
"""Show context menu with close option"""
|
||||
context_menu = tk.Menu(self.root, tearoff=0)
|
||||
context_menu.add_command(label="Закрыть", command=self.root.quit)
|
||||
context_menu.post(event.x_root, event.y_root)
|
||||
|
||||
def toggle_tracking(self):
|
||||
"""Toggle tracking state"""
|
||||
self.tracker.toggle_tracking()
|
||||
self.update_display()
|
||||
|
||||
def new_task(self):
|
||||
"""Create a new task"""
|
||||
task_id = simpledialog.askstring("Новая задача", "Введите ID задачи (например, PRJ-123):")
|
||||
if task_id:
|
||||
self.tracker.create_task(task_id)
|
||||
if task_id and task_id.strip():
|
||||
self.tracker.create_task(task_id.strip())
|
||||
self.update_display()
|
||||
|
||||
def switch_task(self):
|
||||
"""Switch to an existing task"""
|
||||
available_tasks = self.tracker.get_available_tasks()
|
||||
if not available_tasks:
|
||||
messagebox.showinfo("Переключить задачу", "Нет доступных задач")
|
||||
@@ -124,146 +103,117 @@ class OverlayWindow:
|
||||
f"Доступные задачи:\n{task_list}\n\nВведите ID задачи:"
|
||||
)
|
||||
|
||||
if selected_task and selected_task in available_tasks:
|
||||
self.tracker.switch_task(selected_task)
|
||||
if selected_task and selected_task.strip() in available_tasks:
|
||||
self.tracker.switch_task(selected_task.strip())
|
||||
self.update_display()
|
||||
|
||||
def adjust_time(self):
|
||||
"""Show dialog to adjust time manually"""
|
||||
self.show_adjust_time_dialog()
|
||||
|
||||
def show_adjust_time_dialog(self):
|
||||
"""Show dialog for manual time adjustment"""
|
||||
adjustment_window = tk.Toplevel(self.root)
|
||||
adjustment_window.title("Ручная корректировка времени")
|
||||
adjustment_window.geometry("300x150")
|
||||
adjustment_window.geometry("350x200")
|
||||
adjustment_window.transient(self.root)
|
||||
adjustment_window.grab_set() # Modal window
|
||||
adjustment_window.grab_set()
|
||||
|
||||
# Operation selection
|
||||
operation_var = tk.StringVar(value="+")
|
||||
tk.Radiobutton(adjustment_window, text="Добавить время", variable=operation_var, value="+").pack(anchor='w',
|
||||
padx=10,
|
||||
pady=5)
|
||||
tk.Radiobutton(adjustment_window, text="Вычесть время", variable=operation_var, value="-").pack(anchor='w',
|
||||
padx=10, pady=5)
|
||||
tk.Radiobutton(adjustment_window, text="Добавить время", variable=operation_var, value="+").pack(anchor='w', padx=10, pady=2)
|
||||
tk.Radiobutton(adjustment_window, text="Вычесть время", variable=operation_var, value="-").pack(anchor='w', padx=10, pady=2)
|
||||
|
||||
# Minutes entry
|
||||
tk.Label(adjustment_window, text="Минут:").pack(anchor='w', padx=10)
|
||||
tk.Label(adjustment_window, text="Минуты:").pack(anchor='w', padx=10, pady=(10,0))
|
||||
minutes_entry = tk.Entry(adjustment_window)
|
||||
minutes_entry.pack(padx=10, pady=5)
|
||||
minutes_entry.insert(0, "15") # Default value
|
||||
minutes_entry.pack(padx=10, pady=2)
|
||||
minutes_entry.insert(0, "15")
|
||||
|
||||
# Reason entry
|
||||
tk.Label(adjustment_window, text="Причина:").pack(anchor='w', padx=10)
|
||||
tk.Label(adjustment_window, text="Причина:").pack(anchor='w', padx=10, pady=(10,0))
|
||||
reason_entry = tk.Entry(adjustment_window)
|
||||
reason_entry.pack(padx=10, pady=5)
|
||||
reason_entry.insert(0, "Ручная корректировка") # Default value
|
||||
reason_entry.pack(padx=10, pady=2)
|
||||
reason_entry.insert(0, "Перерыв / встреча")
|
||||
|
||||
def apply_adjustment():
|
||||
try:
|
||||
minutes = int(minutes_entry.get())
|
||||
if minutes <= 0:
|
||||
raise ValueError("Minutes must be positive")
|
||||
operation = operation_var.get()
|
||||
seconds = minutes * 60
|
||||
if operation == "-":
|
||||
seconds = -seconds
|
||||
|
||||
reason = reason_entry.get()
|
||||
seconds = minutes * 60 * (1 if operation == "+" else -1)
|
||||
reason = reason_entry.get().strip()
|
||||
if not reason:
|
||||
reason = "Без причины"
|
||||
|
||||
if self.tracker.has_active_session():
|
||||
self.tracker.add_manual_adjustment(seconds, reason)
|
||||
messagebox.showinfo("Успешно", f"Время скорректировано: {operation}{minutes} мин")
|
||||
messagebox.showinfo("Успех", f"Изменено: {operation}{minutes} мин\nПричина: {reason}")
|
||||
adjustment_window.destroy()
|
||||
self.update_display()
|
||||
else:
|
||||
messagebox.showwarning("Внимание", "Нет активной сессии для корректировки")
|
||||
messagebox.showwarning("Ошибка", "Нет активной сессии!")
|
||||
except ValueError:
|
||||
messagebox.showerror("Ошибка", "Введите корректное число минут")
|
||||
messagebox.showerror("Ошибка", "Введите положительное число минут")
|
||||
|
||||
# Apply button
|
||||
apply_btn = tk.Button(adjustment_window, text="Применить", command=apply_adjustment)
|
||||
apply_btn.pack(pady=10)
|
||||
tk.Button(adjustment_window, text="Применить", command=apply_adjustment, bg='lightblue').pack(pady=10)
|
||||
|
||||
def update_display(self):
|
||||
"""Update the display with current information"""
|
||||
# Update tracking status
|
||||
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)
|
||||
# Use lock to avoid race conditions
|
||||
with self.tracker._state_lock:
|
||||
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)
|
||||
|
||||
# Update task information
|
||||
if self.config.current_task_id:
|
||||
# Get current session duration
|
||||
current_duration = self.tracker.get_current_session_duration()
|
||||
current_duration_str = self.format_duration(current_duration)
|
||||
|
||||
# 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}"
|
||||
if self.config.current_task_id:
|
||||
current_duration = self.tracker.get_current_session_duration()
|
||||
current_str = self.format_duration(current_duration)
|
||||
task_text = f"Задача: {self.config.current_task_id} | {current_str}"
|
||||
else:
|
||||
task_text = f"Задача: {self.config.current_task_id} | {current_duration_str}"
|
||||
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', 'Неизвестно')
|
||||
classification = getattr(self.tracker, 'window_classification', 'neutral')
|
||||
|
||||
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)
|
||||
else:
|
||||
self.window_label.config(text="Окно: Неизвестно")
|
||||
|
||||
# Update total time today
|
||||
total_today = self.tracker.get_total_work_today()
|
||||
total_str = self.format_duration(total_today)
|
||||
self.total_label.config(text=f"Всего сегодня: {total_str}")
|
||||
total_today = self.tracker.get_total_work_today()
|
||||
total_str = self.format_duration(total_today)
|
||||
self.total_label.config(text=f"Сегодня: {total_str}")
|
||||
|
||||
# Update button colors based on tracking status
|
||||
if self.config.tracking_enabled:
|
||||
self.track_btn.config(bg='lightgreen', text="●")
|
||||
else:
|
||||
self.track_btn.config(bg='lightcoral', text="○")
|
||||
# Update button appearance
|
||||
self.track_btn.config(
|
||||
text="⏸" if self.config.tracking_enabled else "▶",
|
||||
bg='lightgreen' if self.config.tracking_enabled else 'lightcoral'
|
||||
)
|
||||
|
||||
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:
|
||||
seconds = 0
|
||||
|
||||
hours = seconds // 3600
|
||||
minutes = (seconds % 3600) // 60
|
||||
|
||||
hours, remainder = divmod(seconds, 3600)
|
||||
minutes, secs = divmod(remainder, 60)
|
||||
if hours > 0:
|
||||
return f"{hours}ч{minutes:02d}м"
|
||||
return f"{hours}:{minutes:02d}:{secs:02d}"
|
||||
else:
|
||||
return f"{minutes}м"
|
||||
return f"{minutes}:{secs:02d}"
|
||||
|
||||
def update_display_periodically(self):
|
||||
"""Update display every second"""
|
||||
self.update_display()
|
||||
self.root.after(1000, self.update_display_periodically)
|
||||
|
||||
def show_notification(self, title: str, message: str, duration: int = 5000):
|
||||
"""Show a temporary notification"""
|
||||
# Create notification window
|
||||
"""Show a temporary notification window"""
|
||||
notif = tk.Toplevel(self.root)
|
||||
notif.title(title)
|
||||
notif.geometry("250x80+200+200")
|
||||
notif.geometry("280x90+200+200")
|
||||
notif.overrideredirect(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.pack(fill='both', expand=True)
|
||||
|
||||
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)
|
||||
Reference in New Issue
Block a user