""" TimeLine Designer - PyWebview 主程式 本程式整合 FastAPI 後端與 HTML 前端,提供桌面應用介面。 Author: AI Agent Version: 1.0.0 DocID: SDD-APP-001 Rationale: 實現 SDD.md 定義的 PyWebview Host 架構 """ import webview import threading import uvicorn import logging import sys from pathlib import Path # 設定日誌 logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) class TimelineDesignerApp: """ TimeLine Designer 應用程式類別 負責啟動 FastAPI 後端與 PyWebview 前端。 """ def __init__(self): self.api_port = 8000 self.api_host = "127.0.0.1" self.frontend_path = self._get_frontend_path() def _get_frontend_path(self) -> str: """ 取得前端 HTML 檔案路徑 Returns: 前端 index.html 的絕對路徑 """ # 開發模式:從專案目錄載入 dev_path = Path(__file__).parent / "frontend" / "static" / "index.html" if dev_path.exists(): return str(dev_path.absolute()) # 打包模式:從執行檔旁邊載入 bundle_path = Path(sys.executable).parent / "frontend" / "static" / "index.html" if bundle_path.exists(): return str(bundle_path.absolute()) # 找不到前端檔案 logger.error("找不到前端 HTML 檔案") raise FileNotFoundError("Frontend index.html not found") def start_api_server(self): """ 啟動 FastAPI 後端伺服器 在獨立執行緒中運行,避免阻塞主程式。 """ try: from backend.main import app logger.info(f"正在啟動 API 伺服器於 http://{self.api_host}:{self.api_port}") # 設定 uvicorn config = uvicorn.Config( app, host=self.api_host, port=self.api_port, log_level="info" ) server = uvicorn.Server(config) server.run() except Exception as e: logger.error(f"API 伺服器啟動失敗: {str(e)}") raise def start_gui(self): """ 啟動 PyWebview GUI 在主執行緒中運行。 """ try: logger.info("正在啟動 GUI 視窗") # 建立視窗 window = webview.create_window( title='TimeLine Designer', url=self.frontend_path, width=1400, height=900, resizable=True, fullscreen=False, min_size=(1024, 768), ) logger.info("GUI 視窗已建立") # 啟動 webview(這會阻塞直到視窗關閉) webview.start(debug=True) logger.info("GUI 視窗已關閉") except Exception as e: logger.error(f"GUI 啟動失敗: {str(e)}") raise def run(self): """ 執行應用程式 啟動順序: 1. 在背景執行緒啟動 FastAPI 伺服器 2. 在主執行緒啟動 PyWebview GUI """ logger.info("=== TimeLine Designer 啟動中 ===") # 在背景執行緒啟動 API 伺服器 api_thread = threading.Thread(target=self.start_api_server, daemon=True) api_thread.start() # 等待 API 伺服器啟動 import time logger.info("等待 API 伺服器啟動...") time.sleep(2) # 在主執行緒啟動 GUI self.start_gui() logger.info("=== TimeLine Designer 已關閉 ===") def main(): """ 應用程式入口點 """ try: app = TimelineDesignerApp() app.run() except KeyboardInterrupt: logger.info("使用者中斷程式") sys.exit(0) except Exception as e: logger.error(f"應用程式錯誤: {str(e)}") sys.exit(1) if __name__ == "__main__": main()