Files
lottosight/main.py
T
2026-05-23 12:11:14 -04:00

189 lines
6.7 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 os
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 core.exporter import create_icon_png
from core.paths import bundled_asset, user_data_dir
from ui.statusbar import StatusBar
from ui.dashboard import DashboardScreen
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=bundled_asset("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("Dashboard")
self._launch_fetch()
# ── 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 == "Dashboard":
return DashboardScreen(self._content, on_fetch=self._manual_fetch)
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 = 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()
icon_path = bundled_asset("assets/icon.png")
if not os.path.exists(icon_path):
os.makedirs(os.path.dirname(icon_path), exist_ok=True)
create_icon_png(icon_path)
app = LottoSightApp()
app.protocol("WM_DELETE_WINDOW", app.on_close)
app.mainloop()
if __name__ == "__main__":
main()