import flet as ft from typing import Optional, TYPE_CHECKING import psutil import os # Import components and state from .flet_interest_monitor import InterestMonitorDisplay if TYPE_CHECKING: from .state import AppState def create_main_view(page: ft.Page, app_state: "AppState") -> ft.View: """Creates the main view ('/') of the application.""" # Get the main button from state (should be created in launcher.py main) start_button = app_state.start_bot_button if not start_button: print("[Main View] Error: start_bot_button not initialized in state! Creating placeholder.") start_button = ft.FilledButton("Error - Reload App") app_state.start_bot_button = start_button # Store placeholder back just in case from .utils import run_script # Dynamic import to avoid cycles # --- Card Styling --- # card_shadow = ft.BoxShadow( spread_radius=1, blur_radius=10, # Slightly more blur for frosted effect color=ft.colors.with_opacity(0.2, ft.colors.BLACK87), offset=ft.Offset(1, 2), ) # card_border = ft.border.all(1, ft.colors.with_opacity(0.5, ft.colors.SECONDARY)) # Optional: Remove border for cleaner glass look card_radius = ft.border_radius.all(4) # Slightly softer edges for glass # card_bgcolor = ft.colors.with_opacity(0.05, ft.colors.BLUE_GREY_50) # Subtle background # Use a semi-transparent primary color for the frosted glass effect card_bgcolor = ft.colors.with_opacity(0.65, ft.colors.PRIMARY_CONTAINER) # Example: using theme container color # --- Card Creation Function --- # def create_action_card(icon: str, text: str, on_click_handler, tooltip: str = None): # Removed icon parameter usage return ft.Container( content=ft.Row( [ # ft.Icon(name=icon, color=ft.colors.PRIMARY, size=20), # Icon Removed ft.Text( text, weight=ft.FontWeight.BOLD, # Bolder text size=20, # Even larger font size expand=True, # Allow text to take available space text_align=ft.TextAlign.CENTER, # Center text within the row ), ], # alignment=ft.MainAxisAlignment.START, # Row alignment doesn't matter much with only text vertical_alignment=ft.CrossAxisAlignment.CENTER, # spacing=15, # Spacing removed as icon is gone ), width=300, # *** Explicitly set a fixed width for the card *** # min_height=80, # Increase minimum height (Container doesn't have min_height) border_radius=card_radius, # border=card_border, # Border removed bgcolor=card_bgcolor, padding=ft.padding.symmetric(vertical=25, horizontal=20), # Further increase vertical padding for height margin=ft.margin.only(bottom=20), # Increased bottom margin for more spacing shadow=card_shadow, on_click=on_click_handler, tooltip=tooltip, ink=True, # Add ripple effect on click ) # --- Main Button Action --- # # Need process_manager for the main button action start_bot_card = create_action_card( icon=ft.icons.SMART_TOY_OUTLINED, text="启动麦麦Core", on_click_handler=lambda _: page.go("/console"), tooltip="打开 Bot 控制台视图 (在此启动 Bot)", ) # Note: We are not using app_state.start_bot_button directly here anymore # The button state update logic in process_manager might need adjustment # if we want this card's appearance to change (e.g., text to "返回控制台"). # For now, it will always show "启动". # --- Define Popup Menu Items --- # menu_items = [ # ft.PopupMenuItem( # text="麦麦学习", # on_click=lambda _: run_script("start_lpmm.bat", page, app_state), # ), ft.PopupMenuItem( text="人格生成(测试版)", on_click=lambda _: run_script("start_personality.bat", page, app_state), ), # Add more items here if needed in the future ] # --- Create "More..." Card Separately for Stack --- # more_options_card_stack = ft.Container( content=ft.Row( [ ft.Text( "更多...", # Renamed text weight=ft.FontWeight.BOLD, size=14, # Smaller size for less emphasis # expand=True, text_align=ft.TextAlign.LEFT, ), ft.PopupMenuButton(items=menu_items, icon=ft.icons.MORE_VERT, tooltip="选择要运行的脚本"), ], vertical_alignment=ft.CrossAxisAlignment.CENTER, spacing=5, # Reduced spacing alignment=ft.MainAxisAlignment.END, # Align content to the end (right) of the row ), # width=150, # Reduced width border_radius=card_radius, bgcolor=card_bgcolor, padding=ft.padding.symmetric(vertical=10, horizontal=15), # Reduced padding # margin=ft.margin.only(bottom=20), # Margin handled by Stack positioning shadow=card_shadow, ) # --- Main Column of Cards --- # main_cards_column = ft.Column( controls=[ ft.Container(height=15), # Top spacing # start_button, # Removed direct button start_bot_card, # Use the card # --- Move Adapters Card Up --- # create_action_card( icon=ft.icons.EXTENSION_OUTLINED, # Example icon text="启动适配器...", on_click_handler=lambda _: page.go("/adapters"), tooltip="管理和运行适配器脚本", ), # Re-add the LPMM script card create_action_card( icon=ft.icons.MODEL_TRAINING_OUTLINED, # Icon is not used visually but kept for consistency maybe text="麦麦学习", on_click_handler=lambda _: run_script("start_lpmm.bat", page, app_state), tooltip="运行学习脚本 (start_lpmm.bat)", ), # more_options_card, # Add the new card with the popup menu (Moved to Stack) # --- Add Adapters and Settings Cards --- # create_action_card( icon=ft.icons.SETTINGS_OUTLINED, # Example icon text="设置", on_click_handler=lambda _: page.go("/settings"), tooltip="配置启动器选项", ), ], # alignment=ft.MainAxisAlignment.START, # Default vertical alignment is START horizontal_alignment=ft.CrossAxisAlignment.START, # Align cards to the START (left) spacing=0, # Let card margin handle spacing # expand=True, # Remove expand from the inner column if using Stack ) return ft.View( "/", # Main view route [ ft.AppBar( # title=ft.Text("MaiBot 工具箱", size=18, weight=ft.FontWeight.W_600), # Larger, bolder title # Use leading for custom title layout with a line leading=ft.Row( [ ft.Container(width=4, height=28, bgcolor=ft.colors.PRIMARY, border_radius=2), # Vertical line ft.Container(width=5), # Use a Container for simple horizontal spacing ft.Text("MaiBot 工具箱", size=22, weight=ft.FontWeight.BOLD), # Larger title ], spacing=5, # Spacing within the row vertical_alignment=ft.CrossAxisAlignment.CENTER, ), leading_width=300, # Adjust width to fit the custom leading widget center_title=False, # Left-align title ), # --- Use Stack for Layout --- # ft.Stack( [ # Main column of cards (aligned top-left implicitly) main_cards_column, # "More..." card aligned bottom-right ft.Container( content=more_options_card_stack, # Use Stack positioning properties instead of alignment right=10, # Distance from right edge bottom=10, # Distance from bottom edge ), ], expand=True, # Make Stack fill the available space ), # ft.Column( # [ # ft.Container(height=15), # Top spacing # # start_button, # Removed direct button # start_bot_card, # Use the card # # Re-add the LPMM script card # create_action_card( # icon=ft.icons.MODEL_TRAINING_OUTLINED, # Icon is not used visually but kept for consistency maybe # text="麦麦学习", # on_click_handler=lambda _: run_script("start_lpmm.bat", page, app_state), # tooltip="运行学习脚本 (start_lpmm.bat)" # ), # # more_options_card, # Add the new card with the popup menu (Moved to Stack) # # --- Add Adapters and Settings Cards --- # # create_action_card( # icon=ft.icons.EXTENSION_OUTLINED, # Example icon # text="适配器...", # on_click_handler=lambda _: page.go("/adapters"), # tooltip="管理和运行适配器脚本" # ), # create_action_card( # icon=ft.icons.SETTINGS_OUTLINED, # Example icon # text="设置", # on_click_handler=lambda _: page.go("/settings"), # tooltip="配置启动器选项" # ), # ], # # alignment=ft.MainAxisAlignment.START, # Default vertical alignment is START # horizontal_alignment=ft.CrossAxisAlignment.START, # Align cards to the START (left) # spacing=0, # Let card margin handle spacing # expand=True, # ) ], padding=ft.padding.symmetric(horizontal=15), # Add horizontal padding to the view scroll=ft.ScrollMode.ADAPTIVE, # Allow scrolling if content overflows ) def create_console_view(page: ft.Page, app_state: "AppState") -> ft.View: """Creates the console output view ('/console'), including the interest monitor.""" # Get UI elements from state output_list_view = app_state.output_list_view # start_button = app_state.start_bot_button # Variable is assigned but never used from .process_manager import update_buttons_state # Dynamic import # Create ListView if it doesn't exist (as a fallback, should be created by start_bot) if not output_list_view: output_list_view = ft.ListView(expand=True, spacing=2, auto_scroll=app_state.is_auto_scroll_enabled, padding=5) app_state.output_list_view = output_list_view # Store back to state print("[Create Console View] Fallback: Created ListView.") # --- Create or get InterestMonitorDisplay instance --- # # Ensure the same instance is used if the view is recreated if app_state.interest_monitor_control is None: print("[Create Console View] Creating InterestMonitorDisplay instance") app_state.interest_monitor_control = InterestMonitorDisplay() # Store in state else: print("[Create Console View] Using existing InterestMonitorDisplay instance from state") # Optional: Trigger reactivation if needed # asyncio.create_task(app_state.interest_monitor_control.start_updates_if_needed()) interest_monitor = app_state.interest_monitor_control # --- Process Manager Functions (Import for button actions) --- # --- Auto-scroll toggle button callback (remains separate) --- # def toggle_auto_scroll(e): app_state.is_auto_scroll_enabled = not app_state.is_auto_scroll_enabled lv = app_state.output_list_view # Get potentially updated list view if lv: lv.auto_scroll = app_state.is_auto_scroll_enabled # Update button appearance (assuming button reference is available) # e.control is the Container now # We need to update the Text control stored in its data attribute text_control = e.control.data if isinstance(e.control.data, ft.Text) else None if text_control: text_control.value = "自动滚动 开" if app_state.is_auto_scroll_enabled else "自动滚动 关" else: print("[toggle_auto_scroll] Warning: Could not find Text control in button data.") # The icon and tooltip are on the Container itself (though tooltip might be better on Text?) # e.control.icon = ft.icons.PLAY_ARROW if app_state.is_auto_scroll_enabled else ft.icons.PAUSE # Icon removed e.control.tooltip = "切换控制台自动滚动" # Tooltip remains useful print(f"Auto-scroll {'enabled' if app_state.is_auto_scroll_enabled else 'disabled'}.", flush=True) # Update the container to reflect text changes # page.run_task(update_page_safe, page) # This updates the whole page e.control.update() # Try updating only the container first # --- Card Styling (Copied from create_main_view for reuse) --- # card_shadow = ft.BoxShadow( spread_radius=1, blur_radius=10, color=ft.colors.with_opacity(0.2, ft.colors.BLACK87), offset=ft.Offset(1, 2), ) card_radius = ft.border_radius.all(4) card_bgcolor = ft.colors.with_opacity(0.65, ft.colors.PRIMARY_CONTAINER) card_padding = ft.padding.symmetric(vertical=8, horizontal=12) # Smaller padding for console buttons # --- Create Buttons --- # # Create the main action button (Start/Stop) as a styled Container console_action_button_text = ft.Text("...") # Placeholder text, updated by update_buttons_state console_action_button = ft.Container( content=console_action_button_text, bgcolor=card_bgcolor, # Apply style border_radius=card_radius, shadow=card_shadow, padding=card_padding, ink=True, # on_click is set by update_buttons_state ) app_state.console_action_button = console_action_button # Store container ref # Create the auto-scroll toggle button as a styled Container with Text auto_scroll_text_content = "自动滚动 开" if app_state.is_auto_scroll_enabled else "自动滚动 关" auto_scroll_text = ft.Text(auto_scroll_text_content, size=12) toggle_button = ft.Container( content=auto_scroll_text, tooltip="切换控制台自动滚动", on_click=toggle_auto_scroll, # Attach click handler here bgcolor=card_bgcolor, # Apply style border_radius=card_radius, shadow=card_shadow, padding=card_padding, ink=True, # Remove left margin margin=ft.margin.only(right=10), ) # Store the text control inside the toggle button container for updating toggle_button.data = auto_scroll_text # Store Text reference in data attribute # --- 附加信息区 Column (在 View 级别创建) --- info_top_section = ft.Column( controls=[ ft.Text("附加信息 - 上", weight=ft.FontWeight.BOLD), ft.Divider(), ft.Text("..."), # 上半部分占位符 ], expand=True, # 让上半部分填充可用垂直空间 scroll=ft.ScrollMode.ADAPTIVE, ) info_bottom_section = ft.Column( controls=[ ft.Text("附加信息 - 下", weight=ft.FontWeight.BOLD), ft.Divider(), ft.Text("..."), # 下半部分占位符 # 将按钮放在底部 # Wrap the Row in a Container to apply padding ft.Container( content=ft.Row( [console_action_button, toggle_button], # alignment=ft.MainAxisAlignment.SPACE_AROUND, alignment=ft.MainAxisAlignment.START, # Align buttons to the start ), # Apply padding to the container holding the row padding=ft.padding.only(bottom=10), ), ], # height=100, # 可以给下半部分固定高度,或者让它自适应 spacing=5, # Remove padding from the Column itself # padding=ft.padding.only(bottom=10) ) info_column = ft.Column( controls=[ # ft.Text("附加信息区", weight=ft.FontWeight.BOLD), # ft.Divider(), info_top_section, info_bottom_section, ], width=250, # 增加宽度 # scroll=ft.ScrollMode.ADAPTIVE, # 内部分区滚动,外部不需要 spacing=10, # 分区之间的间距 ) # --- Set Initial Button State --- # # Call the helper AFTER the button is created and stored in state is_initially_running = app_state.bot_pid is not None and psutil.pid_exists(app_state.bot_pid) update_buttons_state(page, app_state, is_running=is_initially_running) # --- 视图布局 --- # return ft.View( "/console", # View route [ ft.AppBar(title=ft.Text("Mai控制台")), # --- 主要内容区域改为 Row --- # ft.Row( controls=[ # --- 左侧 Column (可扩展) --- # ft.Column( controls=[ # 1. Console Output Area ft.Container( content=output_list_view, # From state expand=5, # 在左侧 Column 内部分配比例 border=ft.border.only(bottom=ft.border.BorderSide(1, ft.colors.OUTLINE)), ), # 2. Interest Monitor Area ft.Container( content=interest_monitor, # From state expand=4, # 在左侧 Column 内部分配比例 # border=ft.border.all(1, ft.colors.OUTLINE), # 可以去掉这里的边框 # border_radius=ft.border_radius.all(5), # padding=10, # 可以调整或去掉 # margin=ft.margin.only(top=10), ), ], expand=True, # 让左侧 Column 占据 Row 的大部分空间 ), # --- 右侧 Column (固定宽度) --- # info_column, ], expand=True, # 让 Row 填满 AppBar 下方的空间 ), ], padding=0, # View padding set to 0 # Flet automatically handles calling will_unmount on UserControls like InterestMonitorDisplay # when the view is removed or the app closes. # on_disappear=lambda _: asyncio.create_task(interest_monitor.will_unmount_async()) if interest_monitor else None ) # --- Adapters View --- # def create_adapters_view(page: ft.Page, app_state: "AppState") -> ft.View: """Creates the view for managing adapters (/adapters).""" # Import necessary functions from .config_manager import save_config from .utils import show_snackbar # Removed run_script import # Import process management functions from .process_manager import start_managed_process, stop_managed_process import psutil # To check if PID exists for status adapters_list_view = ft.ListView(expand=True, spacing=5) def update_adapters_list(): """Refreshes the list view with current adapter paths and status-dependent buttons.""" adapters_list_view.controls.clear() for index, path in enumerate(app_state.adapter_paths): process_id = path # Use path as the unique ID for now process_state = app_state.managed_processes.get(process_id) is_running = False if ( process_state and process_state.status == "running" and process_state.pid and psutil.pid_exists(process_state.pid) ): is_running = True action_buttons = [] if is_running: # If running: View Output Button and Stop Button action_buttons.append( ft.IconButton( ft.icons.VISIBILITY_OUTLINED, tooltip="查看输出", data=process_id, on_click=lambda e: page.go(f"/adapters/{e.control.data}"), icon_color=ft.colors.BLUE_GREY, # Neutral color ) ) action_buttons.append( ft.IconButton( ft.icons.STOP_CIRCLE_OUTLINED, tooltip="停止此适配器", data=process_id, # Call stop and then refresh the list view on_click=lambda e: ( stop_managed_process(e.control.data, page, app_state), update_adapters_list(), ), icon_color=ft.colors.RED_ACCENT, ) ) else: # If stopped: Start Button action_buttons.append( ft.IconButton( ft.icons.PLAY_ARROW_OUTLINED, tooltip="启动此适配器脚本", data=path, on_click=lambda e: start_adapter_process(e, page, app_state), icon_color=ft.colors.GREEN, ) ) adapters_list_view.controls.append( ft.Row( [ ft.Text(path, expand=True, overflow=ft.TextOverflow.ELLIPSIS), # Add action buttons based on state *action_buttons, # Keep the remove button ft.IconButton( ft.icons.DELETE_OUTLINE, tooltip="移除此适配器", data=index, # Store index to know which one to remove on_click=remove_adapter, icon_color=ft.colors.ERROR, ), ], alignment=ft.MainAxisAlignment.SPACE_BETWEEN, ) ) # Trigger update if the list view is part of the page if adapters_list_view.page: adapters_list_view.update() def remove_adapter(e): """Removes an adapter path based on the button's data (index).""" index_to_remove = e.control.data if 0 <= index_to_remove < len(app_state.adapter_paths): removed_path = app_state.adapter_paths.pop(index_to_remove) app_state.gui_config["adapters"] = app_state.adapter_paths if save_config(app_state.gui_config): update_adapters_list() show_snackbar(page, f"已移除: {removed_path}") else: show_snackbar(page, "保存配置失败,未能移除", error=True) # Revert state app_state.adapter_paths.insert(index_to_remove, removed_path) app_state.gui_config["adapters"] = app_state.adapter_paths else: show_snackbar(page, "移除时发生错误:无效索引", error=True) # --- Start Adapter Process Handler --- # def start_adapter_process(e, page: ft.Page, app_state: "AppState"): """Handles the click event for the start adapter button.""" path_to_run = e.control.data if not path_to_run or not isinstance(path_to_run, str): show_snackbar(page, "运行错误:无效的适配器路径", error=True) return display_name = os.path.basename(path_to_run) # Use filename as display name process_id = path_to_run # Use path as ID print(f"[Adapters View] Requesting start for: {display_name} (ID: {process_id})") # Call the generic start function from process_manager # It will create the specific ListView in the state success, message = start_managed_process( script_path=path_to_run, display_name=display_name, page=page, app_state=app_state, # No target_list_view needed here, it creates its own ) if success: show_snackbar(page, f"正在启动: {display_name}") update_adapters_list() # Refresh button states # Navigate to the specific output view for this process page.go(f"/adapters/{process_id}") else: # Error message already shown by start_managed_process via snackbar update_adapters_list() # Refresh button states even on failure # --- Initial population of the list --- # update_adapters_list() new_adapter_path_field = ft.TextField(label="新适配器路径 (.py 文件)", expand=True) # --- File Picker Logic --- # def pick_adapter_file_result(e: ft.FilePickerResultEvent): """Callback when the file picker dialog closes.""" if e.files: selected_file = e.files[0] # Get the first selected file new_adapter_path_field.value = selected_file.path new_adapter_path_field.update() show_snackbar(page, f"已选择文件: {os.path.basename(selected_file.path)}") else: show_snackbar(page, "未选择文件") def open_file_picker(e): """Opens the file picker dialog.""" if app_state.file_picker: app_state.file_picker.on_result = pick_adapter_file_result app_state.file_picker.pick_files( allow_multiple=False, allowed_extensions=["py"], # Only allow Python files dialog_title="选择适配器 Python 文件", ) else: show_snackbar(page, "错误:无法打开文件选择器", error=True) # Ensure the file picker's on_result is connected when the view is created if app_state.file_picker: app_state.file_picker.on_result = pick_adapter_file_result else: # This case shouldn't happen if launcher.py runs correctly print("[create_adapters_view] Warning: FilePicker not available during view creation.") def add_adapter(e): """Adds a new adapter path to the list and config.""" new_path = new_adapter_path_field.value.strip() if not new_path: show_snackbar(page, "请输入适配器路径", error=True) return # Basic validation (you might want more robust checks) if not new_path.lower().endswith(".py"): show_snackbar(page, "路径应指向一个 Python (.py) 文件", error=True) return # Optional: Check if the file actually exists? Might be too strict. # if not os.path.exists(new_path): # show_snackbar(page, f"文件未找到: {new_path}", error=True) # return if new_path in app_state.adapter_paths: show_snackbar(page, "此适配器路径已存在") return app_state.adapter_paths.append(new_path) app_state.gui_config["adapters"] = app_state.adapter_paths save_successful = save_config(app_state.gui_config) if save_successful: new_adapter_path_field.value = "" # Clear input field update_adapters_list() # Update the list view new_adapter_path_field.update() # Update the input field visually show_snackbar(page, "适配器已添加") else: show_snackbar(page, "保存配置失败", error=True) # Revert state if save failed try: # Add try-except just in case pop fails unexpectedly app_state.adapter_paths.pop() app_state.gui_config["adapters"] = app_state.adapter_paths except IndexError: pass # Silently ignore if list was empty during failed save return ft.View( "/adapters", [ ft.AppBar(title=ft.Text("适配器管理"), bgcolor=ft.colors.SURFACE_VARIANT), # Use a Container with the padding property instead ft.Container( padding=ft.padding.all(10), # Set padding property on the Container content=ft.Column( # Place the original content inside the Container [ ft.Text("已配置的适配器:"), adapters_list_view, # ListView for adapters ft.Divider(), ft.Row( [ new_adapter_path_field, # --- Add Browse Button --- # ft.IconButton( ft.icons.FOLDER_OPEN_OUTLINED, tooltip="浏览文件...", on_click=open_file_picker, # Call the file picker opener ), ft.IconButton(ft.icons.ADD_CIRCLE_OUTLINE, tooltip="添加适配器", on_click=add_adapter), ] ), ], expand=True, ), ), ], ) # --- Settings View --- # def create_settings_view(page: ft.Page, app_state: "AppState") -> ft.View: """Creates the settings view (/settings).""" return ft.View( "/settings", [ ft.AppBar(title=ft.Text("设置"), bgcolor=ft.colors.SURFACE_VARIANT), # Pass padding value positionally, use content keyword argument # ft.Padding(ft.padding.all(20), content=ft.Text("设置选项将在此处显示...")), # Use a Container with the padding property instead ft.Container( padding=ft.padding.all(20), # Set padding property on the Container content=ft.Text("设置选项将在此处显示..."), # Place the original content inside ), # Add settings controls here later ], ) # --- Process Output View (for Adapters etc.) --- # def create_process_output_view(page: ft.Page, app_state: "AppState", process_id: str) -> Optional[ft.View]: """Creates a view to display the output of a specific managed process.""" # Import stop function from .process_manager import stop_managed_process process_state = app_state.managed_processes.get(process_id) if not process_state: print(f"[Create Output View] Error: Process state not found for ID: {process_id}") # Optionally show an error view or navigate back # For now, return None, route_change might handle this return None # Get or create the ListView for this process # It should have been created and stored by start_managed_process if process_state.output_list_view is None: print(f"[Create Output View] Warning: ListView not found in state for {process_id}. Creating fallback.") # Create a fallback, though this indicates an issue elsewhere process_state.output_list_view = ft.ListView(expand=True, spacing=2, padding=5, auto_scroll=True) process_state.output_list_view.controls.append( ft.Text( "--- Error: Output view created unexpectedly. Process might need restart. ---", italic=True, color=ft.colors.ERROR, ) ) output_lv = process_state.output_list_view # --- Stop Button --- # stop_button = ft.ElevatedButton( "停止进程", icon=ft.icons.STOP_CIRCLE_OUTLINED, on_click=lambda _: stop_managed_process(process_id, page, app_state), bgcolor=ft.colors.with_opacity(0.6, ft.colors.RED_ACCENT_100), color=ft.colors.WHITE, tooltip=f"停止 {process_state.display_name}", ) # --- Auto-scroll Toggle (Specific to this view) --- # # Create a local state for this view's scroll toggle is_this_view_auto_scroll = ft.Ref[bool]() is_this_view_auto_scroll.current = True # Default to true output_lv.auto_scroll = is_this_view_auto_scroll.current def toggle_this_view_auto_scroll(e): is_this_view_auto_scroll.current = not is_this_view_auto_scroll.current output_lv.auto_scroll = is_this_view_auto_scroll.current e.control.text = "自动滚动 开" if is_this_view_auto_scroll.current else "自动滚动 关" e.control.update() print(f"Process '{process_id}' view auto-scroll set to: {is_this_view_auto_scroll.current}") auto_scroll_button = ft.OutlinedButton( "自动滚动 开" if is_this_view_auto_scroll.current else "自动滚动 关", # icon=ft.icons.SCROLLING, icon=ft.icons.SWAP_VERT, # Use a valid icon for toggling on_click=toggle_this_view_auto_scroll, tooltip="切换此视图的自动滚动", ) return ft.View( route=f"/adapters/{process_id}", # Dynamic route appbar=ft.AppBar( title=ft.Text(f"输出: {process_state.display_name}"), bgcolor=ft.colors.SURFACE_VARIANT, actions=[ stop_button, auto_scroll_button, ft.Container(width=5), # Spacer ], ), controls=[ output_lv # Display the specific ListView for this process ], padding=0, )