J.A.R.V.I.S-py/gui/pages/scheduler.py

81 lines
2.9 KiB
Python
Raw Permalink Normal View History

feat: Wave 3 — plugin loader + Flet GUI (parity with Rust fork) Plugin loader (mirrors Rust #29): - plugins_store.py walks %APPDATA%\com.priler.jarvis\plugins\<name>\ for commands.yaml fragments and merges them into VA_CMD_LIST at startup. Same schema as the project root commands.yaml. Per-pack `disabled` flag file skips a pack without deletion. Built-in ids win conflicts so a malicious pack can't silently override open_browser. 9 unit tests (parity with the Rust tests). - main.py: plugin merge runs after the root yaml load; logs the number of extra commands picked up. Flet GUI (#27 — first parity with the Rust Tauri GUI): - New gui/ package: __main__.py launches via `python -m gui` (or gui.bat). app.py routes between 7 pages (Главная / Команды / Макросы / Расписание / Память / Плагины / Настройки) via NavigationRail; dark theme matching the Tauri look. - gui/services.py centralises every store-module call the pages share — single place to swap implementations later. - One file per page in gui/pages/, all expose `build(page)` and use the same card layout idiom. Pages mirror the Tauri ones feature- for-feature: home (status + counters + launcher), commands (filterable list of all VA_CMD_LIST entries), macros (list + delete + recording banner), scheduler (list + remove), memory (add/forget/search), plugins (toggle/open folder), settings (profile + LLM hot-swap). - requirements.txt: flet>=0.85 (optional; assistant runs without). - 3 smoke tests verify imports + build callables; skip cleanly when Flet isn't installed. README: documents both the GUI and the plugin layout in user-facing terms. Tests: 42 → 54 (+9 plugins +3 GUI smoke).
2026-05-16 13:27:04 +03:00
"""Scheduler page — list tasks + remove."""
import datetime
import flet as ft
from gui import services as svc
def _fmt_ts(ts) -> str:
if not ts:
return ""
try:
return datetime.datetime.fromtimestamp(int(ts)).strftime("%Y-%m-%d %H:%M:%S")
except (TypeError, ValueError, OSError):
return ""
def build(page: ft.Page) -> ft.Control:
list_view = ft.Column(spacing=8, expand=True, scroll=ft.ScrollMode.AUTO)
def render():
list_view.controls.clear()
tasks = svc.scheduler_all()
if not tasks:
list_view.controls.append(
ft.Text("Запланированных задач нет. Создавай голосом: «Каждые 30 минут напомни попить воды».",
color="#8aa0a8"))
for t in sorted(tasks, key=lambda x: x.get("created_at", 0)):
list_view.controls.append(_task_card(t, on_delete=lambda tid=t["id"]: do_remove(tid)))
page.update()
def do_remove(task_id: str):
if svc.scheduler_remove(task_id):
page.open(ft.SnackBar(content=ft.Text("Задача удалена", color="white"), bgcolor="#33d3a4"))
render()
render()
return ft.Column(
[
ft.Row([
ft.Text("Расписание", size=24, weight=ft.FontWeight.W_700),
ft.FilledButton(text="Обновить", icon=ft.Icons.REFRESH, on_click=lambda e: render()),
], alignment=ft.MainAxisAlignment.SPACE_BETWEEN),
list_view,
],
spacing=10,
expand=True,
)
def _task_card(t: dict, on_delete) -> ft.Control:
name = t.get("name", "?")
sched_h = t.get("schedule_human") or t.get("schedule_kind") or "?"
action = t.get("action") or {}
action_text = action.get("text") or action.get("speak") or action.get("phrase") or "(нет текста)"
return ft.Container(
content=ft.Column([
ft.Row([
ft.Text(name, weight=ft.FontWeight.W_600, size=14),
ft.Container(
content=ft.Text(sched_h, size=10),
bgcolor="#1e3a44",
padding=ft.padding.symmetric(horizontal=8, vertical=2),
border_radius=10,
),
], alignment=ft.MainAxisAlignment.SPACE_BETWEEN),
ft.Text(f"Действие: {action_text}", size=11, color="#a8b8c0"),
ft.Row([
ft.Text(f"создана: {_fmt_ts(t.get('created_at'))}", size=10, color="#5a6a72"),
ft.Text(f"последний запуск: {_fmt_ts(t.get('last_fired'))}", size=10, color="#5a6a72"),
]),
ft.Row([
ft.OutlinedButton(text="Удалить", icon=ft.Icons.DELETE, on_click=lambda e: on_delete()),
]),
], spacing=6),
bgcolor="#15252b",
border_radius=8,
padding=12,
)