Files
lottosight/main.py
T
2026-05-23 11:37:17 -04:00

180 lines
6.3 KiB
Python

"""
main.py
-------
LottoSight entry point.
Creates the main window with toolbar, content area, and status bar.
Wires auto-fetch on launch (background thread) and 24hr APScheduler job.
"""
import logging
import threading
import tkinter as tk
from tkinter import ttk
from apscheduler.schedulers.background import BackgroundScheduler
from db.database import init_db
from core.fetcher import fetch_all
from ui.statusbar import StatusBar
from ui.history import HistoryScreen
from ui.analysis import AnalysisScreen
from ui.predictor_ui import PredictorScreen
from ui.settings import SettingsScreen
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
logger = logging.getLogger(__name__)
class LottoSightApp(tk.Tk):
def __init__(self):
super().__init__()
self.title("LottoSight")
self.minsize(900, 600)
try:
self.iconphoto(True, tk.PhotoImage(file="assets/icon.png"))
except Exception:
pass
self._fetch_lock = threading.Lock()
self._scheduler: BackgroundScheduler | None = None
self._current_screen: tk.Widget | None = None
self._screens: dict[str, tk.Widget] = {}
self._build_ui()
self._start_scheduler()
self._navigate("History") # open History as default for now
self._launch_fetch() # auto-fetch on startup
# ── UI construction ───────────────────────────────────────────────────────
def _build_ui(self):
# Toolbar
toolbar = ttk.Frame(self, relief="raised", padding=(8, 4))
toolbar.pack(side="top", fill="x")
ttk.Label(
toolbar, text="LottoSight", font=("TkDefaultFont", 13, "bold")
).pack(side="left", padx=(0, 16))
for name in ("Dashboard", "History", "Analysis", "Predictor", "Settings"):
ttk.Button(
toolbar, text=name, width=10,
command=lambda n=name: self._navigate(n),
).pack(side="left", padx=2)
self._fetch_btn = ttk.Button(
toolbar, text="Fetch Now", command=self._manual_fetch
)
self._fetch_btn.pack(side="right", padx=4)
# Content area — screens are packed/unpacked inside here
self._content = ttk.Frame(self)
self._content.pack(side="top", fill="both", expand=True)
# Status bar
self._statusbar = StatusBar(self)
self._statusbar.pack(side="bottom", fill="x")
# ── Navigation ────────────────────────────────────────────────────────────
def _navigate(self, screen_name: str):
logger.info("Navigate → %s", screen_name)
if self._current_screen is not None:
self._current_screen.pack_forget()
if screen_name not in self._screens:
self._screens[screen_name] = self._make_screen(screen_name)
self._current_screen = self._screens[screen_name]
self._current_screen.pack(fill="both", expand=True)
# Refresh data-bearing screens each time they're shown
if hasattr(self._current_screen, "refresh"):
self._current_screen.refresh()
def _make_screen(self, name: str) -> tk.Widget:
if name == "History":
return HistoryScreen(self._content)
if name == "Analysis":
return AnalysisScreen(self._content)
if name == "Predictor":
return PredictorScreen(self._content)
if name == "Settings":
return SettingsScreen(self._content, on_fetch=self._manual_fetch)
# Placeholder for screens added in later phases
placeholder = ttk.Label(
self._content, text=f"{name} — coming soon",
font=("TkDefaultFont", 14), anchor="center",
)
return placeholder
# ── Fetch wiring ──────────────────────────────────────────────────────────
def _start_scheduler(self):
self._scheduler = BackgroundScheduler()
self._scheduler.add_job(
self._launch_fetch, "interval", hours=24, id="auto_fetch_24h"
)
self._scheduler.start()
logger.info("[FETCH] 24hr scheduler started")
def _launch_fetch(self):
"""Start a background fetch thread (skips if one is already running)."""
thread = threading.Thread(
target=self._run_fetch, daemon=True, name="lottosight-fetch"
)
thread.start()
def _manual_fetch(self):
self._launch_fetch()
def _run_fetch(self):
"""Worker: fetch all sources, then update UI on the main thread."""
if not self._fetch_lock.acquire(blocking=False):
logger.info("[FETCH] Fetch already in progress — skipping")
return
try:
self.after(0, self._on_fetch_start)
results = fetch_all()
self.after(0, lambda r=results: self._on_fetch_done(r))
except Exception as e:
logger.error("[ERROR] fetch_all raised unexpectedly: %s", e, exc_info=True)
self.after(0, self._statusbar.set_ready)
finally:
self._fetch_lock.release()
def _on_fetch_start(self):
self._fetch_btn.config(state="disabled", text="Fetching…")
self._statusbar.set_fetching()
def _on_fetch_done(self, results: list):
self._fetch_btn.config(state="normal", text="Fetch Now")
self._statusbar.update_fetch_results(results)
# Refresh the current screen if it can show new data
if self._current_screen and hasattr(self._current_screen, "refresh"):
self._current_screen.refresh()
# ── Lifecycle ─────────────────────────────────────────────────────────────
def on_close(self):
if self._scheduler:
self._scheduler.shutdown(wait=False)
self.destroy()
def main():
init_db()
app = LottoSightApp()
app.protocol("WM_DELETE_WINDOW", app.on_close)
app.mainloop()
if __name__ == "__main__":
main()