Files
2025-04-02 21:01:25 +08:00

496 lines
19 KiB
Python

import customtkinter as ctk
import logging
import traceback
import os
from douyin_imbot.browser.browser_manager import BrowserManager
from douyin_imbot.ui.profile_manager_panel import ProfileManagerPanel
logger = logging.getLogger(__name__)
class MainWindow(ctk.CTk):
"""抖音 IM 机器人主窗口."""
def __init__(self, settings=None):
"""
初始化主窗口.
Args:
settings: 应用程序配置
"""
try:
super().__init__()
self.settings = settings or {}
self.title("抖音自动回复机器人")
self.geometry("1100x700")
# 设置主题和外观
ctk.set_appearance_mode("System") # 系统主题
ctk.set_default_color_theme("blue") # 蓝色主题
# 创建浏览器管理器
self.browser_manager = BrowserManager(settings)
# 初始化变量
self.current_profile = None
self.profile_dialog = None
# 创建UI
self._create_widgets()
logger.info("主窗口初始化完成")
except Exception as e:
logger.error(f"主窗口初始化失败: {str(e)}")
logger.error(traceback.format_exc())
raise
def _create_widgets(self):
"""创建UI组件"""
try:
# 配置主网格
self.grid_columnconfigure(1, weight=1)
self.grid_rowconfigure(0, weight=1)
# 创建左侧导航栏
self.sidebar_frame = ctk.CTkFrame(self, width=200, corner_radius=0)
self.sidebar_frame.grid(row=0, column=0, sticky="nsew")
self.sidebar_frame.grid_rowconfigure(5, weight=1) # 增加行数以适应新的按钮
# 标题
self.app_title = ctk.CTkLabel(
self.sidebar_frame,
text="抖音 IM 机器人",
font=ctk.CTkFont(size=20, weight="bold"),
)
self.app_title.grid(row=0, column=0, padx=20, pady=(20, 20))
# 状态指示器
self.status_frame = ctk.CTkFrame(self.sidebar_frame)
self.status_frame.grid(row=1, column=0, padx=20, pady=(0, 20), sticky="ew")
self.status_label = ctk.CTkLabel(self.status_frame, text="状态:")
self.status_label.grid(row=0, column=0, padx=10, pady=5, sticky="w")
self.status_value = ctk.CTkLabel(
self.status_frame, text="未启动", text_color="gray"
)
self.status_value.grid(row=0, column=1, padx=10, pady=5, sticky="e")
# 导航按钮 - 修改为只有控制面板和设置按钮
self.dashboard_button = ctk.CTkButton(
self.sidebar_frame,
text="控制面板",
height=40,
command=self.show_dashboard,
)
self.dashboard_button.grid(row=2, column=0, padx=20, pady=10, sticky="ew")
self.settings_button = ctk.CTkButton(
self.sidebar_frame, text="设置", height=40, command=self.show_settings
)
self.settings_button.grid(row=3, column=0, padx=20, pady=10, sticky="ew")
self.about_button = ctk.CTkButton(
self.sidebar_frame, text="关于", height=40, command=self.show_about
)
self.about_button.grid(row=4, column=0, padx=20, pady=10, sticky="ew")
# 主内容区域 - 使用帧切换而不是选项卡
self.main_content = ctk.CTkFrame(self)
self.main_content.grid(row=0, column=1, padx=20, pady=20, sticky="nsew")
self.main_content.grid_columnconfigure(0, weight=1)
self.main_content.grid_rowconfigure(0, weight=1)
# 创建各个内容区域的帧,但暂时不显示
self.dashboard_frame = ctk.CTkFrame(self.main_content)
self.settings_frame = ctk.CTkFrame(self.main_content)
self.about_frame = ctk.CTkFrame(self.main_content)
# 初始化各个内容区域
self.setup_dashboard()
self.setup_settings_panel()
self.setup_about_panel()
# 默认显示控制面板
self.show_dashboard()
# 确保配置文件列表在UI加载后刷新
self.after(500, self.refresh_profile_list)
logger.info("UI 组件创建完成")
except Exception as e:
logger.error(f"创建UI组件失败: {str(e)}")
logger.error(traceback.format_exc())
self.update_log(f"UI初始化错误: {str(e)}")
def setup_dashboard(self):
"""设置控制面板."""
try:
# 配置控制面板帧
self.dashboard_frame.grid_columnconfigure(0, weight=1)
self.dashboard_frame.grid_rowconfigure(
2, weight=1
) # 减少行数,因为移除了控制按钮区域
# 集成浏览器配置管理面板
self.profile_manager_frame = ctk.CTkFrame(self.dashboard_frame)
self.profile_manager_frame.grid(
row=1, column=0, padx=10, pady=10, sticky="ew"
)
# 创建配置管理器
self.profile_manager = ProfileManagerPanel(
self.profile_manager_frame,
self.browser_manager,
on_profile_change=self.on_profile_change,
)
self.profile_manager.pack(fill="both", expand=True)
# 移除控制按钮区域
# 日志区域
log_frame = ctk.CTkFrame(self.dashboard_frame)
log_frame.grid(row=2, column=0, padx=10, pady=10, sticky="nsew")
log_frame.grid_columnconfigure(0, weight=1)
log_frame.grid_rowconfigure(1, weight=1)
log_label = ctk.CTkLabel(
log_frame, text="运行日志:", anchor="w", font=ctk.CTkFont(size=16)
)
log_label.grid(row=0, column=0, padx=10, pady=(10, 5), sticky="w")
self.log_textbox = ctk.CTkTextbox(log_frame, height=200)
self.log_textbox.grid(row=1, column=0, padx=10, pady=(0, 10), sticky="nsew")
self.log_textbox.insert("0.0", "准备就绪...\n")
self.log_textbox.configure(state="disabled")
# 清除日志按钮
self.clear_log_button = ctk.CTkButton(
log_frame, text="清除日志", command=self.clear_log, width=100
)
self.clear_log_button.grid(
row=2, column=0, padx=10, pady=(0, 10), sticky="e"
)
logger.info("控制面板设置完成")
except Exception as e:
logger.error(f"设置控制面板失败: {str(e)}")
logger.error(traceback.format_exc())
self.update_log(f"控制面板初始化错误: {str(e)}")
def setup_settings_panel(self):
"""设置设置面板."""
try:
self.settings_frame.grid_columnconfigure(0, weight=1)
# 标题
title = ctk.CTkLabel(
self.settings_frame,
text="设置",
font=ctk.CTkFont(size=24, weight="bold"),
)
title.grid(row=0, column=0, padx=20, pady=20)
# 设置选项
options_frame = ctk.CTkFrame(self.settings_frame)
options_frame.grid(row=1, column=0, padx=20, pady=10, sticky="ew")
options_frame.grid_columnconfigure(1, weight=1)
# API 密钥设置
api_label = ctk.CTkLabel(options_frame, text="API 密钥:", width=150)
api_label.grid(row=0, column=0, padx=10, pady=15, sticky="w")
self.entry_api_key = ctk.CTkEntry(options_frame, width=400)
self.entry_api_key.grid(row=0, column=1, padx=10, pady=15, sticky="ew")
self.entry_api_key.insert(0, self.settings.get("api_key", ""))
# 消息间隔设置
interval_label = ctk.CTkLabel(
options_frame, text="消息间隔 (秒):", width=150
)
interval_label.grid(row=1, column=0, padx=10, pady=15, sticky="w")
self.entry_message_interval = ctk.CTkEntry(options_frame, width=400)
self.entry_message_interval.grid(
row=1, column=1, padx=10, pady=15, sticky="ew"
)
self.entry_message_interval.insert(
0, self.settings.get("message_interval", "5")
)
# 高级设置区域
advanced_frame = ctk.CTkFrame(self.settings_frame)
advanced_frame.grid(row=2, column=0, padx=20, pady=(20, 10), sticky="ew")
advanced_label = ctk.CTkLabel(
advanced_frame,
text="高级设置",
font=ctk.CTkFont(size=18, weight="bold"),
)
advanced_label.pack(pady=10, padx=10, anchor="w")
# 保存按钮
save_button = ctk.CTkButton(
self.settings_frame,
text="保存设置",
command=self.save_settings,
width=200,
height=40,
font=ctk.CTkFont(size=16),
)
save_button.grid(row=3, column=0, pady=30)
logger.info("设置面板设置完成")
except Exception as e:
logger.error(f"设置设置面板失败: {str(e)}")
logger.error(traceback.format_exc())
self.update_log(f"设置面板初始化错误: {str(e)}")
def setup_about_panel(self):
"""设置关于面板."""
try:
self.about_frame.grid_columnconfigure(0, weight=1)
# 标题
title = ctk.CTkLabel(
self.about_frame, text="关于", font=ctk.CTkFont(size=24, weight="bold")
)
title.grid(row=0, column=0, padx=20, pady=20)
# 信息卡片
info_frame = ctk.CTkFrame(self.about_frame)
info_frame.grid(row=1, column=0, padx=40, pady=20, sticky="ew")
version_label = ctk.CTkLabel(
info_frame, text="抖音 IM 机器人 v1.0.0", font=ctk.CTkFont(size=18)
)
version_label.pack(pady=(20, 10))
description = ctk.CTkLabel(
info_frame,
text="抖音 IM 机器人是一个自动回复工具,可以帮助你自动处理抖音私信。",
wraplength=500,
font=ctk.CTkFont(size=14),
)
description.pack(pady=10, padx=30)
# 添加更多关于信息
features_label = ctk.CTkLabel(
info_frame,
text="主要功能:",
font=ctk.CTkFont(size=16, weight="bold"),
anchor="w",
)
features_label.pack(pady=(20, 10), padx=30, anchor="w")
features_text = """• 自动回复抖音私信
• 支持多种浏览器配置
• 自定义回复内容
• 日志记录与监控"""
features = ctk.CTkLabel(
info_frame,
text=features_text,
font=ctk.CTkFont(size=14),
justify="left",
anchor="w",
)
features.pack(pady=5, padx=30, anchor="w")
copyright_label = ctk.CTkLabel(
info_frame, text="© 2023 版权所有", font=ctk.CTkFont(size=12)
)
copyright_label.pack(pady=20)
logger.info("关于面板设置完成")
except Exception as e:
logger.error(f"设置关于面板失败: {str(e)}")
logger.error(traceback.format_exc())
self.update_log(f"关于面板初始化错误: {str(e)}")
def show_dashboard(self):
"""显示控制面板."""
try:
self._hide_all_frames()
self.dashboard_frame.grid(row=0, column=0, sticky="nsew")
self.highlight_button(self.dashboard_button)
logger.debug("显示控制面板")
except Exception as e:
logger.error(f"显示控制面板失败: {str(e)}")
logger.error(traceback.format_exc())
def show_settings(self):
"""显示设置界面."""
try:
self._hide_all_frames()
self.settings_frame.grid(row=0, column=0, sticky="nsew")
self.highlight_button(self.settings_button)
logger.debug("显示设置面板")
except Exception as e:
logger.error(f"显示设置面板失败: {str(e)}")
logger.error(traceback.format_exc())
def show_about(self):
"""显示关于界面."""
try:
self._hide_all_frames()
self.about_frame.grid(row=0, column=0, sticky="nsew")
self.highlight_button(self.about_button)
logger.debug("显示关于面板")
except Exception as e:
logger.error(f"显示关于面板失败: {str(e)}")
logger.error(traceback.format_exc())
def _hide_all_frames(self):
"""隐藏所有内容帧."""
try:
for frame in [
self.dashboard_frame,
self.settings_frame,
self.about_frame,
]:
frame.grid_forget()
except Exception as e:
logger.error(f"隐藏内容帧失败: {str(e)}")
logger.error(traceback.format_exc())
def highlight_button(self, active_button):
"""高亮当前活动按钮."""
try:
for button in [
self.dashboard_button,
self.settings_button,
self.about_button,
]:
if button == active_button:
button.configure(
fg_color=["#3a7ebf", "#1f538d"]
) # 使用主题较深的颜色
else:
button.configure(fg_color=["#3B8ED0", "#1E6FBA"]) # 恢复默认颜色
except Exception as e:
logger.error(f"高亮按钮失败: {str(e)}")
logger.error(traceback.format_exc())
def update_log(self, message):
"""更新日志文本框."""
try:
if hasattr(self, "log_textbox") and self.log_textbox:
self.log_textbox.configure(state="normal")
self.log_textbox.insert("end", f"{message}\n")
self.log_textbox.see("end")
self.log_textbox.configure(state="disabled")
else:
logger.warning(f"日志无法更新,日志组件未初始化: {message}")
except Exception as e:
logger.error(f"更新日志失败: {str(e)}")
logger.error(traceback.format_exc())
def clear_log(self):
"""清除日志内容."""
try:
self.log_textbox.configure(state="normal")
self.log_textbox.delete("0.0", "end")
self.log_textbox.insert("0.0", "日志已清除...\n")
self.log_textbox.configure(state="disabled")
logger.info("日志已清除")
except Exception as e:
logger.error(f"清除日志失败: {str(e)}")
logger.error(traceback.format_exc())
def save_settings(self):
"""保存设置."""
try:
# 收集所有设置项
for key in ["api_key", "message_interval"]:
entry = getattr(self, f"entry_{key}", None)
if entry:
self.settings[key] = entry.get()
# 保存设置到文件
from douyin_imbot.config.settings import save_settings
if save_settings(self.settings):
logger.info("设置已保存")
self.update_log("设置已保存")
else:
logger.error("保存设置失败")
self.update_log("保存设置失败")
except Exception as e:
logger.error(f"保存设置失败: {str(e)}")
logger.error(traceback.format_exc())
self.update_log(f"保存设置失败: {str(e)}")
def on_profile_selected(self, profile_name):
"""当用户选择不同的配置文件时触发"""
try:
if not profile_name:
logger.warning("配置文件名为空")
self.update_log("选择了无效的配置文件")
return
if profile_name == "无可用配置":
logger.warning("选择了无效的配置文件")
self.update_log("选择了无效的配置文件")
return
self.current_profile = profile_name
logger.info(f"用户选择配置文件: {profile_name}")
self.update_log(f"已选择配置文件: {profile_name}")
# 更新配置文件详情显示
try:
profile_details = (
self.browser_manager.get_profile_details(profile_name) or {}
)
details_text = (
f"用户目录: {profile_details.get('user_data_dir', '无')}\n"
)
details_text += f"上次使用: {profile_details.get('last_used', '从未')}"
except Exception as detail_error:
logger.error(f"获取配置详情失败: {str(detail_error)}")
logger.error(traceback.format_exc())
details_text = "获取配置详情失败"
# 检查组件是否已创建
if hasattr(self, "profile_details_label") and self.profile_details_label:
self.profile_details_label.configure(text=details_text)
else:
logger.warning("配置详情标签未创建")
except Exception as e:
logger.error(f"选择配置文件时出错: {str(e)}")
logger.error(traceback.format_exc())
self.update_log(f"选择配置文件时出错: {str(e)}")
def refresh_profile_list(self):
"""刷新配置文件列表"""
try:
# 使用配置管理器刷新配置
if hasattr(self, "profile_manager") and self.profile_manager:
self.profile_manager.refresh_profiles()
self.update_log("配置文件列表已刷新")
return True
return False
except Exception as e:
logger.error(f"刷新配置文件列表失败: {str(e)}")
logger.error(traceback.format_exc())
self.update_log(f"刷新配置文件列表失败: {str(e)}")
return False
def on_profile_change(self):
"""
当浏览器配置文件发生变化时调用
可以在这里更新UI或执行其他操作
"""
try:
if hasattr(self, "refresh_profile_list"):
# 使用after方法避免在对话框销毁期间刷新UI
self.after(100, self.refresh_profile_list)
logger.info("配置文件已更改,列表已刷新")
except Exception as e:
logger.error(f"响应配置文件变化失败: {str(e)}")
logger.error(traceback.format_exc())
self.update_log(f"更新配置列表失败: {str(e)}")