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 .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:
with self._state_lock:
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)
# 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
# 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
with self._state_lock:
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
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"""
with self._state_lock:
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('_')}
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)
with self._state_lock:
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('_')}
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)
with self._state_lock:
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('_')}
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
def on_toggle():
tracker.toggle_tracking()
overlay.update_display()
elif hasattr(key, 'char') and key.char == 'n':
# Ctrl+Alt+N: New task
def on_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:
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
# Start keyboard listener
keyboard.Listener(on_press=on_hotkey).start()
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()

View File

@@ -1,33 +1,36 @@
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
# Fallback numeric constants
WM_WTSSESSION_CHANGE = 0x02B1
WTS_SESSION_LOCK = 0x1
WTS_SESSION_UNLOCK = 0x2
@@ -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

View File

@@ -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 ""
# 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}"
else:
task_text = f"Задача: {self.config.current_task_id} | {current_duration_str}"
current_str = self.format_duration(current_duration)
task_text = f"Задача: {self.config.current_task_id} | {current_str}"
else:
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}")
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)