diff --git a/README.md b/README.md index 06fe40a..1aaf5ea 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ chmod +x build_mac.sh ./build_mac.sh ``` -The built .app will appear in dist/ (example: `dist/mac_launcher.app`). Double-click the `.app` to launch; it will start a local web server and open your default browser to the UI. +The built .app will appear in dist/ (example: `dist/MediaEpisodesViewer.app`). Double-click the `.app` to launch; it will open a native GUI application (no local web server is required). ## Usage diff --git a/mac_native.py b/mac_native.py new file mode 100644 index 0000000..da4de0c --- /dev/null +++ b/mac_native.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python3 +""" +Native GUI using tkinter to generate One Piece progress reports without a web server. +""" + +import os +import threading +import tkinter as tk +from tkinter import ttk, messagebox, scrolledtext +from datetime import datetime + +# Reuse helper logic from app.py +from app import current_week, format_date_range, get_latest_sunday, get_previous_sunday +from clients import jellyfin + + +def generate_report(ui): + def task(): + ui['status_var'].set("Fetching...") + jellyfin.clear_debug_logs() + try: + latest = get_latest_sunday() + previous = get_previous_sunday() + latest_dt = datetime.fromisoformat(latest.replace("Z", "+00:00")) + previous_dt = datetime.fromisoformat(previous.replace("Z", "+00:00")) + user_id = os.environ.get("JELLYFIN_USER_ID") + if not user_id: + raise RuntimeError("JELLYFIN_USER_ID not set in environment") + + ep_info = jellyfin.find_last_one_piece_episode(latest_dt, user_id) + prev_ep_info = jellyfin.find_last_one_piece_before_previous_sunday(previous_dt, user_id) + previous_episode = prev_ep_info["episode_number"] + total_episodes = int(os.environ.get("TOTAL_EPISODES", 1155)) + watched_this_week = ep_info["episode_number"] - previous_episode + week_start, week_end, _ = current_week() + date_range = format_date_range(week_start, week_end) + + report_text = ( + f"ONE PIECE PROGRESS!!! ({date_range})\n" + f"Episode {ep_info['episode_number']}/{total_episodes}\n" + f"Around {watched_this_week} episodes watched this week!" + ) + + debug_output = jellyfin.get_debug_logs() + ui['root'].after(0, lambda: update_ui(ui, ep_info, previous_episode, watched_this_week, total_episodes, report_text, debug_output)) + except Exception as e: + debug_output = jellyfin.get_debug_logs() + ui['root'].after(0, lambda: ui['status_var'].set(f"Error: {e}\n{debug_output}")) + + threading.Thread(target=task, daemon=True).start() + + +def update_ui(ui, ep_info, previous_episode, watched_this_week, total_episodes, report_text, debug_output): + ui['latest_var'].set(str(ep_info.get("episode_number", "—"))) + ui['previous_var'].set(str(previous_episode)) + ui['watched_var'].set(str(watched_this_week)) + ui['total_var'].set(str(total_episodes)) + + ui['report_box'].config(state='normal') + ui['report_box'].delete('1.0', tk.END) + ui['report_box'].insert(tk.END, report_text) + ui['report_box'].config(state='disabled') + + ui['debug_box'].config(state='normal') + ui['debug_box'].delete('1.0', tk.END) + ui['debug_box'].insert(tk.END, debug_output) + ui['debug_box'].config(state='disabled') + + ui['status_var'].set("Done") + + +def copy_report(ui): + report = ui['report_box'].get('1.0', tk.END).strip() + if not report: + messagebox.showinfo("Copy", "No report to copy.") + return + ui['root'].clipboard_clear() + ui['root'].clipboard_append(report) + messagebox.showinfo("Copy", "Report copied to clipboard.") + + +def build_ui(root): + root.title("One Piece Progress Reporter") + root.geometry("900x650") + main = ttk.Frame(root, padding=12) + main.pack(fill='both', expand=True) + + week_frame = ttk.Frame(main) + week_frame.pack(fill='x', pady=(0,8)) + week_start, week_end, _ = current_week() + date_range = format_date_range(week_start, week_end) + week_label = ttk.Label(week_frame, text=f"Week: {date_range} ({week_start.isoformat()} → {week_end.isoformat()})", font=("Helvetica", 12)) + week_label.pack(anchor='w') + + info_frame = ttk.Frame(main) + info_frame.pack(fill='x', pady=(6,6)) + + ttk.Label(info_frame, text="Last episode watched:").grid(row=0, column=0, sticky='w', padx=4, pady=2) + latest_var = tk.StringVar(value="—") + ttk.Label(info_frame, textvariable=latest_var).grid(row=0, column=1, sticky='w', padx=4) + + ttk.Label(info_frame, text="Previous week ended at:").grid(row=1, column=0, sticky='w', padx=4, pady=2) + previous_var = tk.StringVar(value="—") + ttk.Label(info_frame, textvariable=previous_var).grid(row=1, column=1, sticky='w', padx=4) + + ttk.Label(info_frame, text="Watched this week:").grid(row=2, column=0, sticky='w', padx=4, pady=2) + watched_var = tk.StringVar(value="—") + ttk.Label(info_frame, textvariable=watched_var).grid(row=2, column=1, sticky='w', padx=4) + + ttk.Label(info_frame, text="Total in series:").grid(row=3, column=0, sticky='w', padx=4, pady=2) + total_var = tk.StringVar(value="—") + ttk.Label(info_frame, textvariable=total_var).grid(row=3, column=1, sticky='w', padx=4) + + btn_frame = ttk.Frame(info_frame) + btn_frame.grid(row=0, column=2, rowspan=4, padx=18) + status_var = tk.StringVar(value="") + generate_btn = ttk.Button(btn_frame, text="Generate Report") + generate_btn.pack(fill='x', pady=(0,6)) + copy_btn = ttk.Button(btn_frame, text="Copy to Clipboard") + copy_btn.pack(fill='x') + + report_label = ttk.Label(main, text="Report:") + report_label.pack(anchor='w', pady=(8,0)) + report_box = scrolledtext.ScrolledText(main, height=6, wrap='word', state='disabled') + report_box.pack(fill='both', expand=False, pady=(4,6)) + + debug_label = ttk.Label(main, text="Debug:") + debug_label.pack(anchor='w') + debug_box = scrolledtext.ScrolledText(main, height=12, wrap='word', state='disabled') + debug_box.pack(fill='both', expand=True, pady=(4,0)) + + status = ttk.Label(root, textvariable=status_var, relief='sunken', anchor='w') + status.pack(side='bottom', fill='x') + + ui = { + 'root': root, + 'latest_var': latest_var, + 'previous_var': previous_var, + 'watched_var': watched_var, + 'total_var': total_var, + 'status_var': status_var, + 'report_box': report_box, + 'debug_box': debug_box, + } + + generate_btn.config(command=lambda: generate_report(ui)) + copy_btn.config(command=lambda: copy_report(ui)) + + debug_box.config(state='normal') + debug_box.insert(tk.END, f"LATEST_SUNDAY={get_latest_sunday()}\nPREVIOUS_SUNDAY={get_previous_sunday()}\n") + debug_box.config(state='disabled') + + return ui + + +def main(): + root = tk.Tk() + ui = build_ui(root) + root.mainloop() + + +if __name__ == '__main__': + main() diff --git a/setup.py b/setup.py index 8409435..32d4241 100644 --- a/setup.py +++ b/setup.py @@ -1,11 +1,11 @@ from setuptools import setup -APP = ['mac_launcher.py'] +APP = ['mac_native.py'] DATA_FILES = [] OPTIONS = { 'argv_emulation': True, 'packages': ['clients'], - 'includes': ['jinja2', 'requests', 'dotenv'], + 'includes': ['tkinter','requests', 'dotenv'], 'resources': ['templates'], 'plist': { 'CFBundleName': 'MediaEpisodesViewer',