mirror of https://github.com/Mai-with-u/MaiBot.git
746 lines
34 KiB
Python
746 lines
34 KiB
Python
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,
|
|
)
|