import flet as ft import os import atexit import psutil # Keep for initial PID checks maybe, though state should handle it # import asyncio # <--- 如果不再需要其他异步任务,可以考虑移除 # --- Import refactored modules --- # from src.MaiGoi.state import AppState from src.MaiGoi.process_manager import ( start_bot_and_show_console, output_processor_loop, # Needed for restarting on navigate cleanup_on_exit, handle_disconnect, ) from src.MaiGoi.ui_views import ( create_main_view, create_console_view, create_adapters_view, create_process_output_view, ) from src.MaiGoi.config_manager import load_config # --- Import the new settings view --- # from src.MaiGoi.ui_settings_view import create_settings_view # --- Global AppState instance --- # # This holds all the state previously scattered as globals app_state = AppState() # --- File Picker Result Handler Placeholder --- # We need a placeholder function or logic to handle the result here if needed # For now, the result will be handled within the adapters view itself. # def handle_file_picker_result(e: ft.FilePickerResultEvent): # print("File picker result in launcher (should be handled in view):", e.files) # --- atexit Cleanup Registration --- # # Register the cleanup function from the process manager module # It needs access to the app_state atexit.register(cleanup_on_exit, app_state) print("[Main Script] atexit cleanup handler from process_manager registered.", flush=True) # --- Routing Logic --- # def route_change(route: ft.RouteChangeEvent): """Handles Flet route changes, creating and appending views.""" page = route.page target_route = route.route # --- 移除异步显示弹窗的辅助函数 --- # async def show_python_path_dialog(): # ... # Clear existing views before adding new ones page.views.clear() # Always add the main view main_view = create_main_view(page, app_state) page.views.append(main_view) # --- Handle Specific Routes --- # if target_route == "/console": # 清理:移除之前添加的 is_python_dialog_opening 标志(如果愿意) # app_state.is_python_dialog_opening = False # 可选清理 console_view = create_console_view(page, app_state) page.views.append(console_view) # --- 仅设置标志 --- print(f"[Route Change /console] Checking python_path: '{app_state.python_path}'") if not app_state.python_path: print("[Route Change /console] python_path is empty, setting flag.") app_state.needs_python_path_dialog = True # *** 不再在这里打开弹窗 *** # Check process status and potentially restart processor loop if needed is_running = app_state.bot_pid is not None and psutil.pid_exists(app_state.bot_pid) print( f"[Route Change /console] Checking status: PID={app_state.bot_pid}, is_running={is_running}, stop_event={app_state.stop_event.is_set()}", flush=True, ) if is_running: print("[Route Change /console] Process is running.", flush=True) # If the processor loop was stopped (e.g., by navigating away or stop button), # but the process is still running, restart the loop. if app_state.stop_event.is_set(): print("[Route Change /console] Stop event was set, clearing and restarting processor loop.", flush=True) app_state.stop_event.clear() # Make sure output_list_view is available before starting loop if not app_state.output_list_view: print("[Route Change /console] Warning: output_list_view is None when restarting loop. Creating.") app_state.output_list_view = ft.ListView( expand=True, spacing=2, auto_scroll=app_state.is_auto_scroll_enabled, padding=5 ) console_view.controls[1].controls[0].content = app_state.output_list_view # Update content in view page.run_task(output_processor_loop, page, app_state) else: print("[Route Change /console] Process is not running.", flush=True) # Ensure console view shows the 'not running' state if needed if app_state.output_list_view: # Check if already has the message? Might add duplicates. # Simple approach: just add it if the list is empty or last msg isn't it. add_not_running_msg = True if app_state.output_list_view.controls: last_control = app_state.output_list_view.controls[-1] # Check if it's Text and value is not None before checking content if ( isinstance(last_control, ft.Text) and last_control.value is not None and "Bot 进程未运行" in last_control.value ): add_not_running_msg = False if add_not_running_msg: app_state.output_list_view.controls.append(ft.Text("--- Bot 进程未运行 ---", italic=True)) else: # If list view doesn't exist here, create it and add the message print("[Route Change /console] Creating ListView to show 'not running' message.") app_state.output_list_view = ft.ListView( expand=True, spacing=2, auto_scroll=app_state.is_auto_scroll_enabled, padding=5 ) app_state.output_list_view.controls.append(ft.Text("--- Bot 进程未运行 ---", italic=True)) # Update the console view container's content console_view.controls[1].controls[0].content = app_state.output_list_view elif target_route == "/adapters": adapters_view = create_adapters_view(page, app_state) page.views.append(adapters_view) elif target_route == "/settings": # Call the new settings view function settings_view = create_settings_view(page, app_state) page.views.append(settings_view) # --- Handle Dynamic Adapter Output Route --- # # Check if the route matches the pattern /adapters/ elif target_route.startswith("/adapters/") and len(target_route.split("/")) == 3: parts = target_route.split("/") process_id = parts[2] # Extract the process ID (which is the script path for now) print(f"[Route Change] Detected adapter output route for ID: {process_id}") adapter_output_view = create_process_output_view(page, app_state, process_id) if adapter_output_view: page.views.append(adapter_output_view) else: # If view creation failed (e.g., process state not found), show error and stay on previous view? # Or redirect back to /adapters? Let's go back to adapters list. print(f"[Route Change] Failed to create output view for {process_id}. Redirecting to /adapters.") # Avoid infinite loop if /adapters also fails if len(page.views) > 1: # Ensure we don't pop the main view page.views.pop() # Pop the failed view attempt # Find the adapters view if it exists, otherwise just update adapters_view_index = -1 for i, view in enumerate(page.views): if view.route == "/adapters": adapters_view_index = i break if adapters_view_index == -1: # Adapters view wasn't in stack? Add it. adapters_view = create_adapters_view(page, app_state) page.views.append(adapters_view) # Go back to the adapters list route to rebuild the view stack correctly page.go("/adapters") return # Prevent page.update() below # Update the page to show the correct view(s) page.update() def view_pop(e: ft.ViewPopEvent): """Handles view popping (e.g., back navigation).""" page = e.page # Remove the top view page.views.pop() if page.views: top_view = page.views[-1] # Go to the route of the view now at the top of the stack # This will trigger route_change again to rebuild the view stack correctly page.go(top_view.route) # else: print("Warning: Popped the last view.") # --- Main Application Setup --- # def main(page: ft.Page): # Load initial config and store in state if os.path.exists("logs/interest/interest_history.log"): os.remove("logs/interest/interest_history.log") loaded_config = load_config() app_state.gui_config = loaded_config app_state.adapter_paths = loaded_config.get("adapters", []).copy() app_state.bot_script_path = loaded_config.get("bot_script_path", "bot.py") # Load bot script path # 加载用户自定义的 Python 路径 if "python_path" in loaded_config and os.path.exists(loaded_config["python_path"]): app_state.python_path = loaded_config["python_path"] print(f"[Main] 从配置加载 Python 路径: {app_state.python_path}") print(f"[Main] Initial adapters loaded: {app_state.adapter_paths}") # Set script_dir in AppState early app_state.script_dir = os.path.dirname(os.path.abspath(__file__)) print(f"[Main] Script directory set in state: {app_state.script_dir}", flush=True) # --- Setup File Picker --- # # Create the FilePicker instance # The on_result handler will be set dynamically in the view that uses it app_state.file_picker = ft.FilePicker() # Add the FilePicker to the page's overlay controls page.overlay.append(app_state.file_picker) print("[Main] FilePicker created and added to page overlay.") page.title = "MaiBot 启动器" page.window.width = 1400 page.window.height = 1000 # Increased height slightly for monitor page.vertical_alignment = ft.MainAxisAlignment.START page.horizontal_alignment = ft.CrossAxisAlignment.CENTER # --- Apply Theme from Config --- # saved_theme = app_state.gui_config.get("theme", "System").upper() try: page.theme_mode = ft.ThemeMode[saved_theme] print(f"[Main] Applied theme from config: {page.theme_mode}") except KeyError: print(f"[Main] Warning: Invalid theme '{saved_theme}' in config. Falling back to System.") page.theme_mode = ft.ThemeMode.SYSTEM # --- 自定义主题颜色 --- # # 创建深色主题,使橙色变得更暗 dark_theme = ft.Theme( color_scheme_seed=ft.colors.ORANGE, primary_color=ft.colors.ORANGE_700, # 使用更暗的橙色 color_scheme=ft.ColorScheme( primary=ft.colors.ORANGE_700, primary_container=ft.colors.ORANGE_800, ) ) # 创建亮色主题 light_theme = ft.Theme( color_scheme_seed=ft.colors.ORANGE, ) # 设置自定义主题 page.theme = light_theme page.dark_theme = dark_theme page.padding = 0 # <-- 将页面 padding 设置为 0 # --- Create the main 'Start Bot' button and store in state --- # # This button needs to exist before the first route_change call app_state.start_bot_button = ft.FilledButton( "启动 MaiBot 主程序 (bot.py)", icon=ft.icons.SMART_TOY_OUTLINED, # The click handler now calls the function from process_manager on_click=lambda _: start_bot_and_show_console(page, app_state), expand=True, tooltip="启动主程序并在新视图中显示控制台输出", ) print("[Main] Start Bot Button created and stored in state.", flush=True) # --- Routing Setup --- # page.on_route_change = route_change page.on_view_pop = view_pop # --- Disconnect Handler --- # # Pass app_state to the disconnect handler page.on_disconnect = lambda e: handle_disconnect(page, app_state, e) print("[Main] Registered page.on_disconnect handler.", flush=True) # Prevent immediate close to allow cleanup page.window_prevent_close = True # --- Hide Native Title Bar --- # # page.window_title_bar_hidden = True # page.window.frameless = True # --- Initial Navigation --- # # Trigger the initial route change to build the first view page.go(page.route if page.route else "/") # --- Run Flet App --- # if __name__ == "__main__": # No need to initialize globals here anymore, AppState handles it. ft.app(target=main) # This print will appear *after* the Flet window closes, # but *before* the atexit handler runs. print("[Main Script] Flet app exited. atexit handler should run next.", flush=True) # --- Removed Code Sections (Previously Globals and Functions) --- # (Keep this comment block or similar for reference if desired) # Removed: bot_process, bot_pid, output_queue, stop_event, interest_monitor_control, # output_list_view, start_bot_button (now in AppState), # is_auto_scroll_enabled (now in AppState) # Removed: ansi_converter # Removed: cleanup_on_exit (moved to process_manager) # Removed: update_page_safe (moved to utils) # Removed: show_snackbar (moved to utils) # Removed: run_script (moved to utils) # Removed: handle_disconnect (moved to process_manager) # Removed: stop_bot_process (moved to process_manager) # Removed: read_process_output (moved to process_manager) # Removed: output_processor_loop (moved to process_manager) # Removed: start_bot_and_show_console (moved to process_manager) # Removed: create_console_view (moved to ui_views) # (Main view creation logic also moved to ui_views within create_main_view)