Add PyObjC Cocoa UI (mac_cocoa.py) and update build to install pyobjc
Co-authored-by: Copilot <[email protected]>
This commit is contained in:
1 parent
672b0148a6
commit
12c168bbfe
3 files changed
+184
-3
No files matched your search
Regular → Executable
+1
-1
@@ -11,7 +11,7 @@ source .venv/bin/activate
|
|||||||
|
|
||||||
pip install --upgrade pip setuptools wheel
|
pip install --upgrade pip setuptools wheel
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
pip install py2app
|
pip install py2app pyobjc-framework-Cocoa
|
||||||
|
|
||||||
# Build the macOS .app
|
# Build the macOS .app
|
||||||
python3 setup.py py2app
|
python3 setup.py py2app
|
||||||
|
|||||||
+181
@@ -0,0 +1,181 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Cocoa-native GUI using PyObjC to generate the One Piece progress report without a web server.
|
||||||
|
This is a minimal native UI: Generate Report, Copy to Clipboard, and Debug output.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import threading
|
||||||
|
from datetime import datetime
|
||||||
|
from app import current_week, format_date_range, get_latest_sunday, get_previous_sunday
|
||||||
|
from clients import jellyfin
|
||||||
|
|
||||||
|
import objc
|
||||||
|
from AppKit import *
|
||||||
|
|
||||||
|
|
||||||
|
class AppDelegate(NSObject):
|
||||||
|
def applicationDidFinishLaunching_(self, notification):
|
||||||
|
# Create main window
|
||||||
|
rect = NSMakeRect(0.0, 0.0, 900.0, 650.0)
|
||||||
|
style = (NSWindowStyleMaskTitled | NSWindowStyleMaskClosable |
|
||||||
|
NSWindowStyleMaskResizable | NSWindowStyleMaskMiniaturizable)
|
||||||
|
self.window = NSWindow.alloc().initWithContentRect_styleMask_backing_defer_(rect, style, NSBackingStoreBuffered, False)
|
||||||
|
self.window.center()
|
||||||
|
self.window.setTitle_("One Piece Progress Reporter")
|
||||||
|
|
||||||
|
content = self.window.contentView()
|
||||||
|
|
||||||
|
# Title
|
||||||
|
title = NSTextField.alloc().initWithFrame_(NSMakeRect(20, 610, 600, 24))
|
||||||
|
title.setStringValue_("🏴☠️ One Piece Progress Reporter")
|
||||||
|
title.setBezeled_(False)
|
||||||
|
title.setDrawsBackground_(False)
|
||||||
|
title.setEditable_(False)
|
||||||
|
title.setSelectable_(False)
|
||||||
|
content.addSubview_(title)
|
||||||
|
|
||||||
|
# Generate button
|
||||||
|
gen_btn = NSButton.alloc().initWithFrame_(NSMakeRect(760, 610, 120, 30))
|
||||||
|
gen_btn.setTitle_("Generate Report")
|
||||||
|
gen_btn.setTarget_(self)
|
||||||
|
gen_btn.setAction_("generateReport:")
|
||||||
|
content.addSubview_(gen_btn)
|
||||||
|
|
||||||
|
# Report label and box
|
||||||
|
report_label = NSTextField.alloc().initWithFrame_(NSMakeRect(20, 580, 200, 18))
|
||||||
|
report_label.setStringValue_("Report:")
|
||||||
|
report_label.setBezeled_(False)
|
||||||
|
report_label.setDrawsBackground_(False)
|
||||||
|
report_label.setEditable_(False)
|
||||||
|
report_label.setSelectable_(False)
|
||||||
|
content.addSubview_(report_label)
|
||||||
|
|
||||||
|
report_scroll = NSScrollView.alloc().initWithFrame_(NSMakeRect(20, 420, 860, 150))
|
||||||
|
report_scroll.setHasVerticalScroller_(True)
|
||||||
|
report_view = NSTextView.alloc().initWithFrame_(NSMakeRect(0, 0, 860, 150))
|
||||||
|
report_view.setEditable_(False)
|
||||||
|
report_scroll.setDocumentView_(report_view)
|
||||||
|
content.addSubview_(report_scroll)
|
||||||
|
self.report_view = report_view
|
||||||
|
|
||||||
|
# Copy button
|
||||||
|
copy_btn = NSButton.alloc().initWithFrame_(NSMakeRect(760, 360, 120, 30))
|
||||||
|
copy_btn.setTitle_("Copy to Clipboard")
|
||||||
|
copy_btn.setTarget_(self)
|
||||||
|
copy_btn.setAction_("copyReport:")
|
||||||
|
content.addSubview_(copy_btn)
|
||||||
|
|
||||||
|
# Debug label and box
|
||||||
|
debug_label = NSTextField.alloc().initWithFrame_(NSMakeRect(20, 390, 200, 18))
|
||||||
|
debug_label.setStringValue_("Debug:")
|
||||||
|
debug_label.setBezeled_(False)
|
||||||
|
debug_label.setDrawsBackground_(False)
|
||||||
|
debug_label.setEditable_(False)
|
||||||
|
debug_label.setSelectable_(False)
|
||||||
|
content.addSubview_(debug_label)
|
||||||
|
|
||||||
|
debug_scroll = NSScrollView.alloc().initWithFrame_(NSMakeRect(20, 40, 860, 340))
|
||||||
|
debug_scroll.setHasVerticalScroller_(True)
|
||||||
|
debug_view = NSTextView.alloc().initWithFrame_(NSMakeRect(0, 0, 860, 340))
|
||||||
|
debug_view.setEditable_(False)
|
||||||
|
debug_scroll.setDocumentView_(debug_view)
|
||||||
|
content.addSubview_(debug_scroll)
|
||||||
|
self.debug_view = debug_view
|
||||||
|
|
||||||
|
# Initial debug text
|
||||||
|
try:
|
||||||
|
self.debug_view.setString_(f"LATEST_SUNDAY={get_latest_sunday()}\nPREVIOUS_SUNDAY={get_previous_sunday()}\n")
|
||||||
|
except Exception:
|
||||||
|
self.debug_view.setString_("(Could not compute initial debug values)\n")
|
||||||
|
|
||||||
|
self.window.makeKeyAndOrderFront_(None)
|
||||||
|
NSApp.activateIgnoringOtherApps_(True)
|
||||||
|
|
||||||
|
# Button actions
|
||||||
|
def generateReport_(self, sender):
|
||||||
|
# Run the heavy work in a background thread and marshal UI updates back to the main thread
|
||||||
|
threading.Thread(target=self._generate_worker, daemon=True).start()
|
||||||
|
|
||||||
|
def _generate_worker(self):
|
||||||
|
try:
|
||||||
|
jellyfin.clear_debug_logs()
|
||||||
|
|
||||||
|
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()
|
||||||
|
payload = {"report": report_text, "debug": debug_output}
|
||||||
|
# Update UI on main thread
|
||||||
|
self.performSelectorOnMainThread_withObject_waitUntilDone_("updateUI:", payload, False)
|
||||||
|
except Exception as e:
|
||||||
|
payload = {"error": str(e), "debug": jellyfin.get_debug_logs()}
|
||||||
|
self.performSelectorOnMainThread_withObject_waitUntilDone_("showError:", payload, False)
|
||||||
|
|
||||||
|
def updateUI_(self, payload):
|
||||||
|
report = payload.get("report", "")
|
||||||
|
debug = payload.get("debug", "")
|
||||||
|
try:
|
||||||
|
self.report_view.setString_(report)
|
||||||
|
self.debug_view.setString_(debug)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def showError_(self, payload):
|
||||||
|
error = payload.get("error", "Unknown error")
|
||||||
|
debug = payload.get("debug", "")
|
||||||
|
alert = NSAlert.alloc().init()
|
||||||
|
alert.setMessageText_("Error")
|
||||||
|
alert.setInformativeText_(error)
|
||||||
|
alert.runModal()
|
||||||
|
try:
|
||||||
|
self.debug_view.setString_(debug)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def copyReport_(self, sender):
|
||||||
|
try:
|
||||||
|
s = self.report_view.string() or ""
|
||||||
|
pb = NSPasteboard.generalPasteboard()
|
||||||
|
pb.clearContents()
|
||||||
|
# Use public.utf8-plain-text which is commonly accepted
|
||||||
|
pb.setString_forType_(s, "public.utf8-plain-text")
|
||||||
|
alert = NSAlert.alloc().init()
|
||||||
|
alert.setMessageText_("Copied")
|
||||||
|
alert.setInformativeText_("Report copied to clipboard.")
|
||||||
|
alert.runModal()
|
||||||
|
except Exception as e:
|
||||||
|
alert = NSAlert.alloc().init()
|
||||||
|
alert.setMessageText_("Copy failed")
|
||||||
|
alert.setInformativeText_(str(e))
|
||||||
|
alert.runModal()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
app = NSApplication.sharedApplication()
|
||||||
|
delegate = AppDelegate.alloc().init()
|
||||||
|
app.setDelegate_(delegate)
|
||||||
|
app.run()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
|
|
||||||
APP = ['mac_native.py']
|
APP = ['mac_cocoa.py']
|
||||||
DATA_FILES = []
|
DATA_FILES = []
|
||||||
OPTIONS = {
|
OPTIONS = {
|
||||||
'argv_emulation': True,
|
'argv_emulation': True,
|
||||||
'packages': ['clients'],
|
'packages': ['clients'],
|
||||||
'includes': ['tkinter','requests', 'dotenv'],
|
'includes': ['objc','AppKit','requests', 'dotenv'],
|
||||||
'resources': ['templates'],
|
'resources': ['templates'],
|
||||||
'plist': {
|
'plist': {
|
||||||
'CFBundleName': 'MediaEpisodesViewer',
|
'CFBundleName': 'MediaEpisodesViewer',
|
||||||
|
|||||||
Reference in new issue
Block a user