189 lines
6.7 KiB
Python
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()
|