[AI] MVP 1
This commit is contained in:
@@ -5,7 +5,9 @@ from .app import Application
|
|||||||
def main():
|
def main():
|
||||||
try:
|
try:
|
||||||
app = Application()
|
app = Application()
|
||||||
app.run()
|
from .ui.overlay import OverlayWindow
|
||||||
|
overlay = OverlayWindow(app)
|
||||||
|
overlay.run()
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
print(f"Ошибка: {e}", file=sys.stderr)
|
print(f"Ошибка: {e}", file=sys.stderr)
|
||||||
input("Нажмите Enter для выхода...")
|
input("Нажмите Enter для выхода...")
|
||||||
|
|||||||
@@ -1,26 +1,218 @@
|
|||||||
import json
|
import json
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from datetime import datetime
|
||||||
from .models import TrackerConfig, TrackedTask
|
from typing import Optional, List
|
||||||
|
from pynput import mouse, keyboard
|
||||||
|
|
||||||
|
from .models import TrackerConfig, TrackedTask, WorkDetail, WorkSession
|
||||||
from .utils import get_platform, ensure_dir
|
from .utils import get_platform, ensure_dir
|
||||||
from .ui.overlay import OverlayWindow
|
|
||||||
|
|
||||||
|
|
||||||
class Application:
|
class Application:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
self._lock = threading.Lock()
|
||||||
self.config_path = Path("config.json")
|
self.config_path = Path("config.json")
|
||||||
self.tasks_dir = Path("tasks")
|
self.tasks_dir = Path("tasks")
|
||||||
self.config: TrackerConfig = self._load_or_create_config()
|
self.config: TrackerConfig = self._load_or_create_config()
|
||||||
self.current_task: Optional[TrackedTask] = None
|
self.current_task: Optional[TrackedTask] = None
|
||||||
self.platform = get_platform()
|
self.platform = get_platform()
|
||||||
|
|
||||||
# Загружаем текущую задачу, если указан ID
|
# Состояние трекинга
|
||||||
|
self.tracking_enabled = self.config.tracking_enabled
|
||||||
|
self.last_activity: float = time.time() # timestamp
|
||||||
|
self.last_window_group: Optional[str] = None
|
||||||
|
self.is_idle: bool = False
|
||||||
|
self._tracking_thread: Optional[threading.Thread] = None
|
||||||
|
self._running = True
|
||||||
|
|
||||||
|
# Загружаем задачу
|
||||||
if self.config.current_task_id:
|
if self.config.current_task_id:
|
||||||
self.current_task = self._load_task(self.config.current_task_id)
|
self.current_task = self._load_task(self.config.current_task_id)
|
||||||
|
|
||||||
# Создаём директорию для задач, если нужно
|
|
||||||
ensure_dir(self.tasks_dir)
|
ensure_dir(self.tasks_dir)
|
||||||
|
|
||||||
|
# Запускаем слушатели активности (только если tracking_enabled)
|
||||||
|
self._start_activity_listeners()
|
||||||
|
# Запускаем фоновый трекер
|
||||||
|
self._start_tracking_loop()
|
||||||
|
|
||||||
|
def _start_activity_listeners(self):
|
||||||
|
def on_activity(*_):
|
||||||
|
with self._lock:
|
||||||
|
self.last_activity = time.time()
|
||||||
|
self.is_idle = False
|
||||||
|
|
||||||
|
self.mouse_listener = mouse.Listener(on_move=on_activity, on_click=on_activity)
|
||||||
|
self.keyboard_listener = keyboard.Listener(on_press=on_activity)
|
||||||
|
self.mouse_listener.start()
|
||||||
|
self.keyboard_listener.start()
|
||||||
|
|
||||||
|
def _start_tracking_loop(self):
|
||||||
|
def loop():
|
||||||
|
while self._running:
|
||||||
|
time.sleep(5)
|
||||||
|
self._check_and_update_tracking()
|
||||||
|
|
||||||
|
self._tracking_thread = threading.Thread(target=loop, daemon=True)
|
||||||
|
self._tracking_thread.start()
|
||||||
|
|
||||||
|
def _check_and_update_tracking(self):
|
||||||
|
with self._lock:
|
||||||
|
if not self.tracking_enabled or not self.current_task:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Проверка: спит ли система? (упрощённо — если активность > 60 сек назад)
|
||||||
|
current_time = time.time()
|
||||||
|
if current_time - self.last_activity > 60:
|
||||||
|
self.is_idle = True
|
||||||
|
self.last_window_group = None
|
||||||
|
return
|
||||||
|
|
||||||
|
# Получаем текущее окно
|
||||||
|
window_info = self.platform.get_active_window()
|
||||||
|
if not window_info.title and not window_info.process_name:
|
||||||
|
self.is_idle = True
|
||||||
|
return
|
||||||
|
|
||||||
|
# Определяем группу по политике
|
||||||
|
group_name = self._match_window_to_policy(window_info)
|
||||||
|
is_blocked = self._is_group_blocked(group_name)
|
||||||
|
|
||||||
|
if is_blocked:
|
||||||
|
self.is_idle = True
|
||||||
|
self.last_window_group = None
|
||||||
|
return
|
||||||
|
|
||||||
|
# Если рабочая группа — накапливаем время
|
||||||
|
self.is_idle = False
|
||||||
|
if self.last_window_group != group_name:
|
||||||
|
# Новая группа — завершаем предыдущую сессию (если была)
|
||||||
|
self.last_window_group = group_name
|
||||||
|
|
||||||
|
# Добавляем 5 секунд к текущей группе
|
||||||
|
self._add_work_detail(group_name, 5.0)
|
||||||
|
|
||||||
|
def _match_window_to_policy(self, window_info) -> str:
|
||||||
|
title = window_info.title.lower()
|
||||||
|
proc = window_info.process_name.lower()
|
||||||
|
|
||||||
|
for policy in self.config.window_policies:
|
||||||
|
for pattern in policy.window_patterns:
|
||||||
|
if pattern.lower() in title:
|
||||||
|
return policy.group_name
|
||||||
|
for pname in policy.process_names:
|
||||||
|
if pname.lower() == proc:
|
||||||
|
return policy.group_name
|
||||||
|
return "Неизвестное приложение"
|
||||||
|
|
||||||
|
def _is_group_blocked(self, group_name: str) -> bool:
|
||||||
|
for policy in self.config.window_policies:
|
||||||
|
if policy.group_name == group_name:
|
||||||
|
return policy.policy_type == "blocked"
|
||||||
|
return False # по умолчанию — не блокируем
|
||||||
|
|
||||||
|
def _add_work_detail(self, group_name: str, seconds: float):
|
||||||
|
if not self.current_task:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Создаём новую сессию или используем последнюю
|
||||||
|
if not self.current_task.sessions:
|
||||||
|
session = WorkSession(
|
||||||
|
start_time=datetime.now(),
|
||||||
|
end_time=datetime.now(),
|
||||||
|
details=[],
|
||||||
|
synchronized=False
|
||||||
|
)
|
||||||
|
self.current_task.sessions.append(session)
|
||||||
|
else:
|
||||||
|
session = self.current_task.sessions[-1]
|
||||||
|
|
||||||
|
# Ищем detail с такой же группой
|
||||||
|
for detail in session.details:
|
||||||
|
if detail.group_name == group_name:
|
||||||
|
detail.duration_seconds += seconds
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
session.details.append(WorkDetail(group_name=group_name, duration_seconds=seconds))
|
||||||
|
|
||||||
|
# Обновляем время сессии
|
||||||
|
session.end_time = datetime.now()
|
||||||
|
|
||||||
|
# === Публичные методы с локом ===
|
||||||
|
|
||||||
|
def toggle_tracking(self):
|
||||||
|
with self._lock:
|
||||||
|
if not self.current_task:
|
||||||
|
self.tracking_enabled = False
|
||||||
|
return
|
||||||
|
|
||||||
|
self.tracking_enabled = not self.tracking_enabled
|
||||||
|
self.config.tracking_enabled = self.tracking_enabled
|
||||||
|
self._save_config(self.config)
|
||||||
|
|
||||||
|
def create_task(self, task_id: str, project_id: Optional[str], estimated_minutes: Optional[float]):
|
||||||
|
with self._lock:
|
||||||
|
ensure_dir(self.tasks_dir)
|
||||||
|
new_task = TrackedTask(
|
||||||
|
task_id=task_id,
|
||||||
|
project_id=project_id,
|
||||||
|
estimated_seconds=estimated_minutes * 60 if estimated_minutes else None,
|
||||||
|
start_time=datetime.now()
|
||||||
|
)
|
||||||
|
self.save_task(new_task)
|
||||||
|
self.current_task = new_task
|
||||||
|
self.config.current_task_id = task_id
|
||||||
|
self.config.tracking_enabled = True
|
||||||
|
self.tracking_enabled = True
|
||||||
|
self._save_config(self.config)
|
||||||
|
|
||||||
|
def select_task(self, task_id: str):
|
||||||
|
with self._lock:
|
||||||
|
task = self._load_task(task_id)
|
||||||
|
if task:
|
||||||
|
self.current_task = task
|
||||||
|
self.config.current_task_id = task_id
|
||||||
|
self.config.tracking_enabled = True
|
||||||
|
self.tracking_enabled = True
|
||||||
|
self._save_config(self.config)
|
||||||
|
|
||||||
|
def add_manual_time(self, minutes: float):
|
||||||
|
with self._lock:
|
||||||
|
if not self.current_task:
|
||||||
|
return
|
||||||
|
seconds = abs(minutes) * 60
|
||||||
|
sign = 1 if minutes >= 0 else -1
|
||||||
|
self._add_work_detail("Manual", sign * seconds)
|
||||||
|
|
||||||
|
def get_display_status(self) -> str:
|
||||||
|
with self._lock:
|
||||||
|
if not self.tracking_enabled:
|
||||||
|
return "Пауза"
|
||||||
|
if not self.current_task:
|
||||||
|
return "Пауза"
|
||||||
|
if self.is_idle:
|
||||||
|
return "Бездействие"
|
||||||
|
return "Активная работа"
|
||||||
|
|
||||||
|
def get_current_window_info(self) -> dict:
|
||||||
|
with self._lock:
|
||||||
|
if not self.tracking_enabled or not self.current_task:
|
||||||
|
return {"group": "Пауза", "detail": "Трекинг остановлен. Нажмите «Пауза» для запуска."}
|
||||||
|
|
||||||
|
if self.is_idle:
|
||||||
|
return {"group": "Бездействие", "detail": "Нет активности более 60 сек или отвлекающее окно"}
|
||||||
|
|
||||||
|
window_info = self.platform.get_active_window()
|
||||||
|
group = self._match_window_to_policy(window_info)
|
||||||
|
return {
|
||||||
|
"group": group,
|
||||||
|
"detail": f"{window_info.title} | {window_info.process_name}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# === Остальные методы (без изменений, но с локом где нужно) ===
|
||||||
|
|
||||||
def _load_or_create_config(self) -> TrackerConfig:
|
def _load_or_create_config(self) -> TrackerConfig:
|
||||||
if self.config_path.exists():
|
if self.config_path.exists():
|
||||||
with open(self.config_path, "r", encoding="utf-8") as f:
|
with open(self.config_path, "r", encoding="utf-8") as f:
|
||||||
@@ -51,8 +243,9 @@ class Application:
|
|||||||
with open(task_file, "w", encoding="utf-8") as f:
|
with open(task_file, "w", encoding="utf-8") as f:
|
||||||
f.write(task.model_dump_json(indent=2))
|
f.write(task.model_dump_json(indent=2))
|
||||||
|
|
||||||
def run(self):
|
def shutdown(self):
|
||||||
# Пока просто запускаем UI
|
self._running = False
|
||||||
# Позже сюда добавим трекинг в фоне
|
if hasattr(self, 'mouse_listener'):
|
||||||
overlay = OverlayWindow()
|
self.mouse_listener.stop()
|
||||||
overlay.run()
|
if hasattr(self, 'keyboard_listener'):
|
||||||
|
self.keyboard_listener.stop()
|
||||||
@@ -1,9 +1,18 @@
|
|||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
|
from datetime import datetime
|
||||||
from tkinter import Toplevel, Label, Entry, Button
|
from tkinter import Toplevel, Label, Entry, Button
|
||||||
|
from typing import Callable, TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..app import Application
|
||||||
|
|
||||||
|
|
||||||
class AdjustTimeWindow:
|
class AdjustTimeWindow:
|
||||||
def __init__(self, parent):
|
def __init__(self, parent, app: "Application", on_close_callback: Callable):
|
||||||
|
self.parent = parent
|
||||||
|
self.app = app
|
||||||
|
self.on_close_callback = on_close_callback
|
||||||
|
|
||||||
self.window = Toplevel(parent)
|
self.window = Toplevel(parent)
|
||||||
self.window.title("Изменить время")
|
self.window.title("Изменить время")
|
||||||
self.window.geometry("250x120")
|
self.window.geometry("250x120")
|
||||||
@@ -20,5 +29,39 @@ class AdjustTimeWindow:
|
|||||||
Button(self.window, text="Применить", command=self.on_apply).pack(pady=5)
|
Button(self.window, text="Применить", command=self.on_apply).pack(pady=5)
|
||||||
|
|
||||||
def on_apply(self):
|
def on_apply(self):
|
||||||
# Позже: применить изменение
|
if not self.app.current_task:
|
||||||
|
self.window.destroy()
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
minutes = float(self.minutes_entry.get())
|
||||||
|
except ValueError:
|
||||||
|
return
|
||||||
|
|
||||||
|
if minutes == 0:
|
||||||
|
self.window.destroy()
|
||||||
|
return
|
||||||
|
|
||||||
|
seconds = abs(minutes * 60)
|
||||||
|
# Создаём новую сессию или добавляем в последнюю?
|
||||||
|
# Для простоты — добавим новую сессию с одним WorkDetail
|
||||||
|
|
||||||
|
from ..models import WorkDetail, WorkSession
|
||||||
|
|
||||||
|
detail = WorkDetail(
|
||||||
|
group_name="Manual",
|
||||||
|
duration_seconds=seconds if minutes > 0 else -seconds # можно хранить отрицательное
|
||||||
|
)
|
||||||
|
|
||||||
|
session = WorkSession(
|
||||||
|
start_time=datetime.now(),
|
||||||
|
end_time=datetime.now(),
|
||||||
|
details=[detail],
|
||||||
|
synchronized=False
|
||||||
|
)
|
||||||
|
|
||||||
|
self.app.current_task.sessions.append(session)
|
||||||
|
self.app.save_task(self.app.current_task)
|
||||||
|
|
||||||
|
self.on_close_callback()
|
||||||
self.window.destroy()
|
self.window.destroy()
|
||||||
@@ -1,14 +1,23 @@
|
|||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import Toplevel, Label, Entry, Button, Frame
|
from datetime import datetime
|
||||||
|
from tkinter import Toplevel, Label, Entry, Button
|
||||||
|
from typing import Callable, TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..app import Application
|
||||||
|
|
||||||
|
|
||||||
class NewTaskWindow:
|
class NewTaskWindow:
|
||||||
def __init__(self, parent):
|
def __init__(self, parent, app: "Application", on_close_callback: Callable):
|
||||||
|
self.parent = parent
|
||||||
|
self.app = app
|
||||||
|
self.on_close_callback = on_close_callback
|
||||||
|
|
||||||
self.window = Toplevel(parent)
|
self.window = Toplevel(parent)
|
||||||
self.window.title("Новая задача")
|
self.window.title("Новая задача")
|
||||||
self.window.geometry("300x200")
|
self.window.geometry("300x200")
|
||||||
self.window.transient(parent)
|
self.window.transient(parent)
|
||||||
self.window.grab_set() # модальное
|
self.window.grab_set()
|
||||||
|
|
||||||
Label(self.window, text="ID задачи:").pack(pady=(10, 0))
|
Label(self.window, text="ID задачи:").pack(pady=(10, 0))
|
||||||
self.task_id_entry = Entry(self.window)
|
self.task_id_entry = Entry(self.window)
|
||||||
@@ -25,5 +34,36 @@ class NewTaskWindow:
|
|||||||
Button(self.window, text="Создать", command=self.on_create).pack(pady=10)
|
Button(self.window, text="Создать", command=self.on_create).pack(pady=10)
|
||||||
|
|
||||||
def on_create(self):
|
def on_create(self):
|
||||||
# Позже: сохранить задачу
|
task_id = self.task_id_entry.get().strip()
|
||||||
|
if not task_id:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
estimate_str = self.estimate_entry.get().strip()
|
||||||
|
estimate_seconds = float(estimate_str) * 60 if estimate_str else None
|
||||||
|
except ValueError:
|
||||||
|
return
|
||||||
|
|
||||||
|
project_id = self.project_id_entry.get().strip() or None
|
||||||
|
|
||||||
|
# Создаём новую задачу
|
||||||
|
from ..models import TrackedTask
|
||||||
|
new_task = TrackedTask(
|
||||||
|
task_id=task_id,
|
||||||
|
project_id=project_id,
|
||||||
|
estimated_seconds=estimate_seconds,
|
||||||
|
start_time=datetime.now()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Сохраняем
|
||||||
|
self.app.save_task(new_task)
|
||||||
|
|
||||||
|
# Обновляем конфиг
|
||||||
|
self.app.config.current_task_id = task_id
|
||||||
|
self.app._save_config(self.app.config)
|
||||||
|
|
||||||
|
# Обновляем текущую задачу в памяти
|
||||||
|
self.app.current_task = new_task
|
||||||
|
|
||||||
|
self.on_close_callback()
|
||||||
self.window.destroy()
|
self.window.destroy()
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import Menu, Label, Button, Frame
|
from tkinter import Menu, Label, Button, Frame
|
||||||
from typing import Optional
|
from typing import TYPE_CHECKING
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..app import Application
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class OverlayWindow:
|
class OverlayWindow:
|
||||||
def __init__(self):
|
def __init__(self, app: "Application"):
|
||||||
|
self.app = app
|
||||||
self.root = tk.Tk()
|
self.root = tk.Tk()
|
||||||
self.root.title("Citrus Time Tracker")
|
self.root.title("Citrus Time Tracker")
|
||||||
self.root.geometry("320x180")
|
self.root.geometry("320x180")
|
||||||
@@ -30,6 +34,8 @@ class OverlayWindow:
|
|||||||
# Содержимое
|
# Содержимое
|
||||||
self._build_ui()
|
self._build_ui()
|
||||||
|
|
||||||
|
self._auto_update()
|
||||||
|
|
||||||
def center_window(self):
|
def center_window(self):
|
||||||
self.root.update_idletasks()
|
self.root.update_idletasks()
|
||||||
screen_width = self.root.winfo_screenwidth()
|
screen_width = self.root.winfo_screenwidth()
|
||||||
@@ -66,27 +72,72 @@ class OverlayWindow:
|
|||||||
btn_frame = Frame(main_frame)
|
btn_frame = Frame(main_frame)
|
||||||
btn_frame.pack(side="bottom", fill="x", pady=(10, 0))
|
btn_frame.pack(side="bottom", fill="x", pady=(10, 0))
|
||||||
|
|
||||||
Button(btn_frame, text="⏸ Пауза", command=self.on_pause).pack(side="left", fill="x", expand=True, padx=(0, 2))
|
self.pause_button = Button(btn_frame, text="⏸ Пауза", command=self.on_pause)
|
||||||
Button(btn_frame, text="➕ Новая", command=self.on_new_task).pack(side="left", fill="x", expand=True, padx=2)
|
self.pause_button.pack(side="left", fill="x", expand=True, padx=(0, 2))
|
||||||
Button(btn_frame, text="🔍 Выбрать", command=self.on_select_task).pack(side="left", fill="x", expand=True, padx=2)
|
self.new_task_btn = Button(btn_frame, text="➕ Новая", command=self.on_new_task)
|
||||||
Button(btn_frame, text="✏️ Время", command=self.on_adjust_time).pack(side="left", fill="x", expand=True, padx=(2, 0))
|
self.new_task_btn.pack(side="left", fill="x", expand=True, padx=2)
|
||||||
|
self.select_task_btn = Button(btn_frame, text="🔍 Выбрать", command=self.on_select_task)
|
||||||
|
self.select_task_btn.pack(side="left", fill="x", expand=True, padx=2)
|
||||||
|
self.adjust_time_btn = Button(btn_frame, text="✏️ Время", command=self.on_adjust_time)
|
||||||
|
self.adjust_time_btn.pack(side="left", fill="x", expand=True, padx=(2, 0))
|
||||||
|
|
||||||
|
self._update_display()
|
||||||
|
|
||||||
|
def _update_display(self):
|
||||||
|
status = self.app.get_display_status()
|
||||||
|
self.status_label.config(text=f"Citrus Time Tracker: {status}")
|
||||||
|
|
||||||
|
task_id = self.app.config.current_task_id or "<Не выбрано>"
|
||||||
|
self.task_label.config(text=f"Задача: {task_id}")
|
||||||
|
|
||||||
|
# Время
|
||||||
|
if self.app.current_task:
|
||||||
|
worked = self.app.current_task.total_worked_seconds / 60
|
||||||
|
est = self.app.current_task.estimated_seconds
|
||||||
|
if est is not None:
|
||||||
|
est_min = est / 60
|
||||||
|
rem = self.app.current_task.remaining_seconds / 60
|
||||||
|
time_info = f"Затрачено: {worked:.1f} мин | Оценка: {est_min:.1f} мин"
|
||||||
|
else:
|
||||||
|
time_info = f"Затрачено: {worked:.1f} мин | Оценка: —"
|
||||||
|
else:
|
||||||
|
time_info = "Задача не выбрана"
|
||||||
|
self.time_info_label.config(text=time_info)
|
||||||
|
|
||||||
|
# Окно
|
||||||
|
win_info = self.app.get_current_window_info()
|
||||||
|
self.window_group_label.config(text=win_info["group"])
|
||||||
|
self.window_detail_label.config(text=win_info["detail"])
|
||||||
|
|
||||||
|
# Активность кнопок
|
||||||
|
has_task = self.app.current_task is not None
|
||||||
|
self.pause_button.config(state="normal" if has_task else "disabled")
|
||||||
|
for btn in [self.new_task_btn, self.select_task_btn, self.adjust_time_btn]:
|
||||||
|
btn.config(state="normal")
|
||||||
|
|
||||||
|
# Но "Добавить время" — только если есть задача
|
||||||
|
self.adjust_time_btn.config(state="normal" if has_task else "disabled")
|
||||||
|
|
||||||
|
def _auto_update(self):
|
||||||
|
self._update_display()
|
||||||
|
self.root.after(2000, self._auto_update) # обновляем каждые 2 сек
|
||||||
|
|
||||||
# === Обработчики событий (пока без логики, только открытие окон) ===
|
# === Обработчики событий (пока без логики, только открытие окон) ===
|
||||||
|
|
||||||
def on_pause(self):
|
def on_pause(self):
|
||||||
pass # будет реализовано позже
|
self.app.toggle_tracking()
|
||||||
|
|
||||||
def on_new_task(self):
|
def on_new_task(self):
|
||||||
from .new_task_window import NewTaskWindow
|
from .new_task_window import NewTaskWindow
|
||||||
NewTaskWindow(self.root)
|
NewTaskWindow(self.root, self.app, self._update_display)
|
||||||
|
|
||||||
def on_select_task(self):
|
def on_select_task(self):
|
||||||
from .select_task_window import SelectTaskWindow
|
from .select_task_window import SelectTaskWindow
|
||||||
SelectTaskWindow(self.root)
|
SelectTaskWindow(self.root, self.app, self._update_display)
|
||||||
|
|
||||||
def on_adjust_time(self):
|
def on_adjust_time(self):
|
||||||
from .adjust_time_window import AdjustTimeWindow
|
from .adjust_time_window import AdjustTimeWindow
|
||||||
AdjustTimeWindow(self.root)
|
AdjustTimeWindow(self.root, self.app, self._update_display)
|
||||||
|
|
||||||
# === UI-управление ===
|
# === UI-управление ===
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +1,55 @@
|
|||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import Toplevel, Listbox, Scrollbar, Button, Frame
|
from pathlib import Path
|
||||||
|
from tkinter import Toplevel, Listbox, Scrollbar, Button
|
||||||
|
from typing import Callable, TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..app import Application
|
||||||
|
|
||||||
|
|
||||||
class SelectTaskWindow:
|
class SelectTaskWindow:
|
||||||
def __init__(self, parent):
|
def __init__(self, parent, app: "Application", on_close_callback: Callable):
|
||||||
|
self.parent = parent
|
||||||
|
self.app = app
|
||||||
|
self.on_close_callback = on_close_callback
|
||||||
|
|
||||||
self.window = Toplevel(parent)
|
self.window = Toplevel(parent)
|
||||||
self.window.title("Выбрать задачу")
|
self.window.title("Выбрать задачу")
|
||||||
self.window.geometry("300x250")
|
self.window.geometry("300x250")
|
||||||
self.window.transient(parent)
|
self.window.transient(parent)
|
||||||
self.window.grab_set()
|
self.window.grab_set()
|
||||||
|
|
||||||
list_frame = Frame(self.window)
|
# Сканируем папку tasks/
|
||||||
|
task_files = list(Path("tasks").glob("*.json")) if Path("tasks").exists() else []
|
||||||
|
self.task_ids = [f.stem for f in task_files]
|
||||||
|
|
||||||
|
list_frame = tk.Frame(self.window)
|
||||||
list_frame.pack(fill="both", expand=True, padx=10, pady=10)
|
list_frame.pack(fill="both", expand=True, padx=10, pady=10)
|
||||||
|
|
||||||
scrollbar = Scrollbar(list_frame)
|
scrollbar = Scrollbar(list_frame)
|
||||||
scrollbar.pack(side="right", fill="y")
|
scrollbar.pack(side="right", fill="y")
|
||||||
|
|
||||||
self.listbox = Listbox(list_frame, yscrollcommand=scrollbar.set)
|
self.listbox = Listbox(list_frame, yscrollcommand=scrollbar.set)
|
||||||
|
for tid in self.task_ids:
|
||||||
|
self.listbox.insert("end", tid)
|
||||||
self.listbox.pack(side="left", fill="both", expand=True)
|
self.listbox.pack(side="left", fill="both", expand=True)
|
||||||
|
|
||||||
# Заглушка: список задач
|
|
||||||
for i in range(5):
|
|
||||||
self.listbox.insert("end", f"TASK-{i+1} (1 ч 20 мин)")
|
|
||||||
|
|
||||||
scrollbar.config(command=self.listbox.yview)
|
scrollbar.config(command=self.listbox.yview)
|
||||||
|
|
||||||
Button(self.window, text="Выбрать", command=self.on_select).pack(pady=5)
|
Button(self.window, text="Выбрать", command=self.on_select).pack(pady=5)
|
||||||
|
|
||||||
def on_select(self):
|
def on_select(self):
|
||||||
# Позже: выбрать задачу
|
selection = self.listbox.curselection()
|
||||||
|
if not selection:
|
||||||
|
return
|
||||||
|
task_id = self.task_ids[selection[0]]
|
||||||
|
|
||||||
|
# Загружаем задачу
|
||||||
|
task = self.app._load_task(task_id)
|
||||||
|
if task:
|
||||||
|
self.app.current_task = task
|
||||||
|
self.app.config.current_task_id = task_id
|
||||||
|
self.app._save_config(self.app.config)
|
||||||
|
self.on_close_callback()
|
||||||
|
|
||||||
self.window.destroy()
|
self.window.destroy()
|
||||||
Reference in New Issue
Block a user