import ctypes import datetime as dt import json import os import platform import re import subprocess import sys import psutil import signal import traceback from shutil import copy from typing import Optional from PyQt5 import uic from PyQt5.QtCore import Qt, QTimer, QPropertyAnimation, QRect, QEasingCurve, QSize, QPoint, QUrl, QObject, QParallelAnimationGroup from PyQt5.QtGui import QColor, QIcon, QPixmap, QPainter, QDesktopServices from PyQt5.QtGui import QFontDatabase from PyQt5.QtSvg import QSvgRenderer from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QProgressBar, QGraphicsBlurEffect, QPushButton, \ QGraphicsDropShadowEffect, QSystemTrayIcon, QFrame, QGraphicsOpacityEffect, QHBoxLayout from loguru import logger from packaging.version import Version from qfluentwidgets import Theme, setTheme, setThemeColor, SystemTrayMenu, Action, FluentIcon as fIcon, isDarkTheme, \ Dialog, ProgressRing, PlainTextEdit, ImageLabel, PushButton, InfoBarIcon, Flyout, FlyoutAnimationType, CheckBox, \ PrimaryPushButton, IconWidget import conf import list_ import tip_toast from tip_toast import active_windows import utils import weather_db as db from conf import base_directory from extra_menu import ExtraMenu, open_settings from generate_speech import generate_speech_sync, list_pyttsx3_voices from menu import open_plaza from network_thread import check_update, weatherReportThread from play_audio import play_audio from plugin import p_loader from utils import restart, stop, share, update_timer, DarkModeWatcher from file import config_center, schedule_center if os.name == 'nt': import pygetwindow # 适配高DPI缩放 if platform.system() == 'Windows' and platform.release() not in ['7', 'XP', 'Vista']: QApplication.setHighDpiScaleFactorRoundingPolicy( Qt.HighDpiScaleFactorRoundingPolicy.PassThrough) QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps) else: logger.warning('不兼容的系统,跳过高DPI标识') today = dt.date.today() # 存储窗口对象 windows = [] order = [] error_dialog = None current_lesson_name = '课程表未加载' current_state = 0 # 0:课间 1:上课 2: 休息段 current_time = dt.datetime.now().strftime('%H:%M:%S') current_week = dt.datetime.now().weekday() current_lessons = {} loaded_data = {} parts_type = [] notification = tip_toast excluded_lessons = [] last_notify_time = None notify_cooldown = 2 # 2秒内仅能触发一次通知(防止触发114514个通知导致爆炸 timeline_data = {} next_lessons = [] parts_start_time = [] temperature = '未设置' weather_icon = 0 weather_name = '' weather_data_temp = None city = 101010100 # 默认城市 theme = None time_offset = 0 # 时差偏移 first_start = True error_cooldown = dt.timedelta(seconds=2) # 冷却时间(s) ignore_errors = [] last_error_time = dt.datetime.now() - error_cooldown # 上一次错误 ex_menu = None dark_mode_watcher = None was_floating_mode = False # 浮窗状态 if config_center.read_conf('Other', 'do_not_log') != '1': logger.add(f"{base_directory}/log/ClassWidgets_main_{{time}}.log", rotation="1 MB", encoding="utf-8", retention="1 minute") logger.info('未禁用日志输出') else: logger.info('已禁用日志输出功能,若需保存日志,请在“设置”->“高级选项”中关闭禁用日志功能') def global_exceptHook(exc_type, exc_value, exc_tb): # 全局异常捕获 if config_center.read_conf('Other', 'safe_mode') == '1': # 安全模式 return error_details = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb)) # 异常详情 if error_details in ignore_errors: # 忽略重复错误 return global last_error_time, error_dialog, error_cooldown current_time = dt.datetime.now() if current_time - last_error_time > error_cooldown: # 冷却时间 last_error_time = current_time logger.error(f"全局异常捕获:{exc_type} {exc_value} {exc_tb}") logger.error(f"详细堆栈信息:\n{error_details}") if not error_dialog: w = ErrorDialog(error_details) w.exec() else: # 忽略冷却时间 pass sys.excepthook = global_exceptHook # 设置全局异常捕获 def handle_dark_mode_change(is_dark): """处理DarkModeWatcher触发的UI更新""" if config_center.read_conf('General', 'color_mode') == '2': logger.info(f"系统颜色模式更新: {'深色' if is_dark else '浅色'}") current_theme = Theme.DARK if is_dark else Theme.LIGHT setTheme(current_theme) if mgr: mgr.clear_widgets() else: logger.warning("主题更改时,mgr还未初始化") # if current_state == 1: # setThemeColor(f"#{config_center.read_conf('Color', 'attend_class')}") # else: # setThemeColor(f"#{config_center.read_conf('Color', 'finish_class')}") def setTheme_(): # 设置主题 global theme color_mode = config_center.read_conf('General', 'color_mode') if color_mode == '2': # 自动 logger.info(f'颜色模式: 自动({color_mode})') if platform.system() == 'Darwin' and Version(platform.mac_ver()[0]) < Version('10.14'): return if platform.system() == 'Windows': # Windows 7特殊处理 if sys.getwindowsversion().major == 6 and sys.getwindowsversion().minor == 1: setTheme(Theme.LIGHT) return # 检查Windows版本是否支持深色模式(Windows 10 build 14393及以上) try: win_build = sys.getwindowsversion().build if win_build < 14393: # 不支持深色模式的最低版本 return except AttributeError: # 无法获取版本信息,保守返回 return if platform.system() == 'Linux': return if dark_mode_watcher: is_dark = dark_mode_watcher.isDark() if is_dark is not None: logger.info(f"当前颜色模式: {'深色' if is_dark else '浅色'}") setTheme(Theme.DARK if is_dark else Theme.LIGHT) else: logger.warning("无法获取系统颜色模式,暂时使用浅色主题") setTheme(Theme.LIGHT) else: logger.warning("DarkModeWatcher 未被初始化,使用浅色主题") setTheme(Theme.LIGHT) elif color_mode == '1': logger.info(f'颜色模式: 深色({color_mode})') setTheme(Theme.DARK) else: logger.info(f'颜色模式: 浅色({color_mode})') setTheme(Theme.LIGHT) def get_timeline_data(): if len(loaded_data['timeline']) == 1: return loaded_data['timeline']['default'] else: if str(current_week) in loaded_data['timeline'] and loaded_data['timeline'][str(current_week)]: # 如果此周有时间线 return loaded_data['timeline'][str(current_week)] else: return loaded_data['timeline']['default'] # 获取Part开始时间 def get_start_time(): global parts_start_time, timeline_data, loaded_data, order, parts_type loaded_data = schedule_center.schedule_data timeline = get_timeline_data() part = loaded_data['part'] parts_start_time = [] timeline_data = {} order = [] for item_name, item_value in part.items(): try: h, m = item_value[:2] try: part_type = item_value[2] except IndexError: part_type = 'part' except Exception as e: logger.error(f'加载课程表文件[节点类型]出错:{e}') part_type = 'part' # 应用时差偏移到课程表时间 start_time = dt.datetime.combine(today, dt.time(h, m)) + dt.timedelta(seconds=time_offset) parts_start_time.append(start_time) order.append(item_name) parts_type.append(part_type) except Exception as e: logger.error(f'加载课程表文件[起始时间]出错:{e}') paired = zip(parts_start_time, order) paired_sorted = sorted(paired, key=lambda x: x[0]) # 按时间大小排序 if paired_sorted: parts_start_time, order = zip(*paired_sorted) def sort_timeline_key(item): item_name = item[0] prefix = item_name[0] if len(item_name) > 1: try: # 提取节点序数 part_num = int(item_name[1]) # 提取课程序数 class_num = 0 if len(item_name) > 2: class_num = int(item_name[2:]) if prefix == 'a': return part_num, class_num, 0 else: return part_num, class_num, 1 except ValueError: # 如果转换失败,返回原始字符串 return item_name return item_name # 对timeline排序后添加到timeline_data sorted_timeline = sorted(timeline.items(), key=sort_timeline_key) for item_name, item_time in sorted_timeline: try: timeline_data[item_name] = item_time except Exception as e: logger.error(f'加载课程表文件[课程数据]出错:{e}') def get_part(): if not parts_start_time: return None def return_data(): c_time = parts_start_time[i] return c_time, int(order[i]) # 返回开始时间、Part序号 current_dt = dt.datetime.now() # 当前时间 for i in range(len(parts_start_time)): # 遍历每个Part time_len = dt.timedelta(minutes=0) # Part长度 for item_name, item_time in timeline_data.items(): if item_name.startswith(f'a{str(order[i])}') or item_name.startswith(f'f{str(order[i])}'): time_len += dt.timedelta(minutes=int(item_time)) # 累计Part的时间点总长度 time_len += dt.timedelta(seconds=1) if time_len != dt.timedelta(seconds=1): # 有课程 if i == len(parts_start_time) - 1: # 最后一个Part return return_data() else: if current_dt <= parts_start_time[i] + time_len: return return_data() return parts_start_time[0] + dt.timedelta(seconds=time_offset), 0, 'part' def get_excluded_lessons(): global excluded_lessons if config_center.read_conf('General', 'excluded_lesson') == "0": excluded_lessons = [] return excluded_lessons_raw = config_center.read_conf('General', 'excluded_lessons') excluded_lessons = excluded_lessons_raw.split(',') if excluded_lessons_raw != '' else [] # 获取当前活动 def get_current_lessons(): # 获取当前课程 global current_lessons timeline = get_timeline_data() if config_center.read_conf('General', 'enable_alt_schedule') == '1' or conf.is_temp_week(): try: if conf.get_week_type(): schedule = loaded_data.get('schedule_even') else: schedule = loaded_data.get('schedule') except Exception as e: logger.error(f'加载课程表文件[单双周]出错:{e}') schedule = loaded_data.get('schedule') else: schedule = loaded_data.get('schedule') class_count = 0 for item_name, _ in timeline.items(): if item_name.startswith('a'): if schedule[str(current_week)]: try: if schedule[str(current_week)][class_count] != '未添加': current_lessons[item_name] = schedule[str(current_week)][class_count] else: current_lessons[item_name] = '暂无课程' except IndexError: current_lessons[item_name] = '暂无课程' except Exception as e: current_lessons[item_name] = '暂无课程' logger.debug(f'加载课程表文件出错:{e}') class_count += 1 else: current_lessons[item_name] = '暂无课程' class_count += 1 # 获取倒计时、弹窗提示 def get_countdown(toast=False): # 重构好累aaaa global last_notify_time current_dt = dt.datetime.now() if last_notify_time and (current_dt - last_notify_time).seconds < notify_cooldown: return def after_school(): # 放学 if parts_type[part] == 'break': # 休息段 notification.push_notification(0, current_lesson_name) # 下课 else: if config_center.read_conf('Toast', 'after_school') == '1': notification.push_notification(2) # 放学 current_dt = dt.datetime.combine(today, dt.datetime.strptime(current_time, '%H:%M:%S').time()) # 当前时间 return_text = [] got_return_data = False if parts_start_time: c_time, part = get_part() if current_dt >= c_time: for item_name, item_time in timeline_data.items(): if item_name.startswith(f'a{str(part)}') or item_name.startswith(f'f{str(part)}'): # 判断时间是否上下课,发送通知 if current_dt == c_time and toast: if item_name.startswith('a'): notification.push_notification(1, current_lesson_name) # 上课 last_notify_time = current_dt else: if next_lessons: # 下课/放学 notification.push_notification(0, next_lessons[0]) # 下课 last_notify_time = current_dt else: after_school() if current_dt == c_time - dt.timedelta( minutes=int(config_center.read_conf('Toast', 'prepare_minutes'))): if config_center.read_conf('Toast', 'prepare_minutes') != '0' and toast and item_name.startswith('a'): if not current_state: # 课间 notification.push_notification(3, next_lessons[0]) # 准备上课(预备铃) last_notify_time = current_dt # 放学 if (c_time + dt.timedelta(minutes=int(item_time)) == current_dt and not next_lessons and not current_state and toast): after_school() last_notify_time = current_dt add_time = int(item_time) c_time += dt.timedelta(minutes=add_time) if got_return_data: break if c_time >= current_dt: # 根据所在时间段使用不同标语 if item_name.startswith('a'): return_text.append('当前活动结束还有') else: return_text.append('课间时长还有') # 返回倒计时、进度条 time_diff = c_time - current_dt minute, sec = divmod(time_diff.seconds, 60) return_text.append(f'{minute:02d}:{sec:02d}') # 进度条 seconds = time_diff.seconds return_text.append(int(100 - seconds / (int(item_time) * 60) * 100)) got_return_data = True if not return_text: return_text = ['目前课程已结束', f'00:00', 100] else: prepare_minutes_str = config_center.read_conf('Toast', 'prepare_minutes') if prepare_minutes_str != '0' and toast: prepare_minutes = int(prepare_minutes_str) if current_dt == c_time - dt.timedelta(minutes=prepare_minutes): next_lesson_name = None next_lesson_key = None if timeline_data: for key in sorted(timeline_data.keys()): if key.startswith(f'a{str(part)}'): next_lesson_key = key break if next_lesson_key and next_lesson_key in current_lessons: lesson_name = current_lessons[next_lesson_key] if lesson_name != '暂无课程': next_lesson_name = lesson_name if current_state == 0: now = dt.datetime.now() if not last_notify_time or (now - last_notify_time).seconds >= notify_cooldown: if next_lesson_name != None: notification.push_notification(3, next_lesson_name) if f'a{part}1' in timeline_data: time_diff = c_time - current_dt minute, sec = divmod(time_diff.seconds, 60) return_text = ['距离上课还有', f'{minute:02d}:{sec:02d}', 100] else: return_text = ['目前课程已结束', f'00:00', 100] return return_text # 获取将发生的活动 def get_next_lessons(): global current_lesson_name global next_lessons next_lessons = [] part = 0 current_dt = dt.datetime.combine(today, dt.datetime.strptime(current_time, '%H:%M:%S').time()) # 当前时间 if parts_start_time: c_time, part = get_part() def before_class(): if part == 0 or part == 3: return True else: if current_dt >= parts_start_time[part] - dt.timedelta(minutes=60): return True else: return False if before_class(): for item_name, item_time in timeline_data.items(): if item_name.startswith(f'a{str(part)}') or item_name.startswith(f'f{str(part)}'): add_time = int(item_time) if c_time > current_dt and item_name.startswith('a'): next_lessons.append(current_lessons[item_name]) c_time += dt.timedelta(minutes=add_time) def get_next_lessons_text(): if not next_lessons: cache_text = '当前暂无课程' else: cache_text = '' if len(next_lessons) >= 5: range_time = 5 else: range_time = len(next_lessons) for i in range(range_time): if range_time > 2: if next_lessons[i] != '暂无课程': cache_text += f'{list_.get_subject_abbreviation(next_lessons[i])} ' # 获取课程简称 else: cache_text += f'无 ' else: if next_lessons[i] != '暂无课程': cache_text += f'{next_lessons[i]} ' else: cache_text += f'暂无 ' return cache_text # 获取当前活动 def get_current_lesson_name(): global current_lesson_name, current_state current_dt = dt.datetime.combine(today, dt.datetime.strptime(current_time, '%H:%M:%S').time()) # 当前时间 current_lesson_name = '暂无课程' current_state = 0 if parts_start_time: c_time, part = get_part() if current_dt >= c_time: if parts_type[part] == 'break': # 休息段 current_lesson_name = loaded_data['part_name'][str(part)] current_state = 2 for item_name, item_time in timeline_data.items(): if item_name.startswith(f'a{str(part)}') or item_name.startswith(f'f{str(part)}'): add_time = int(item_time) c_time += dt.timedelta(minutes=add_time) if c_time > current_dt: if item_name.startswith('a'): current_lesson_name = current_lessons[item_name] current_state = 1 else: current_lesson_name = '课间' current_state = 0 return def get_hide_status(): # 1 -> hide, 0 -> show # 满分啦( # 祝所有用 Class Widgets 的、不用 Class Widgets 的学子体测满分啊(( global current_state, current_lesson_name, excluded_lessons return 1 if { '0': lambda: 0, '1': lambda: current_state, '2': lambda: check_windows_maximize() or check_fullscreen(), '3': lambda: current_state }[config_center.read_conf('General', 'hide')]() and not (current_lesson_name in excluded_lessons) else 0 # 定义 RECT 结构体 class RECT(ctypes.Structure): _fields_ = [("left", ctypes.c_long), ("top", ctypes.c_long), ("right", ctypes.c_long), ("bottom", ctypes.c_long)] def get_process_name(pid): # 获取进程名称 try: if isinstance(pid, int): pid = ctypes.windll.user32.GetWindowThreadProcessId(pid, None) return psutil.Process(pid).name().lower() except (psutil.NoSuchProcess, AttributeError, ValueError): return "unknown" def check_fullscreen(): # 检查是否全屏 if os.name != 'nt': return False user32 = ctypes.windll.user32 hwnd = user32.GetForegroundWindow() if not hwnd: return False if hwnd == user32.GetDesktopWindow(): return False if hwnd == user32.GetShellWindow(): return False pid = ctypes.c_ulong() user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid)) process_name = get_process_name(pid.value) current_pid = os.getpid() # logger.debug(f"前景窗口句柄: {hwnd}, PID: {pid.value}, 进程名: {process_name}") if pid.value == current_pid: return False # 排除特定系统进程 excluded_system_processes = { 'explorer.exe', # 文件资源管理器/桌面 'shellexperiencehost.exe', # Shell体验主机 (开始菜单、操作中心) 'searchui.exe', # Cortana/搜索界面 'applicationframehost.exe', # UWP应用框架 'systemsettings.exe', # 设置 'taskmgr.exe' # 任务管理器 } if process_name in excluded_system_processes: # logger.debug(f"前景窗口进程 '{process_name}' 在排除列表 (系统进程), 排除.") return False title_buffer = ctypes.create_unicode_buffer(256) user32.GetWindowTextW(hwnd, title_buffer, 256) window_title_lower = title_buffer.value.strip().lower() # logger.debug(f"前景窗口标题: '{title_buffer.value}' (小写: '{window_title_lower}')") # 排除特定窗口标题 excluded_system_window_titles = { "program manager", # 桌面窗口 "windows input experience", # 输入法相关 "msctfmonitor window", # 输入法相关 "startmenuexperiencehost" # 开始菜单 } if window_title_lower in excluded_system_window_titles: # logger.debug(f"前景窗口标题 '{window_title_lower}' 在排除列表 (系统窗口), 排除.") return False rect = RECT() user32.GetWindowRect(hwnd, ctypes.byref(rect)) # 使用桌面窗口作为屏幕尺寸参考 screen_rect_desktop = RECT() user32.GetWindowRect(user32.GetDesktopWindow(), ctypes.byref(screen_rect_desktop)) # logger.debug(f"窗口矩形: 左={rect.left}, 上={rect.top}, 右={rect.right}, 下={rect.bottom}") # logger.debug(f"桌面矩形: 左={screen_rect_desktop.left}, 上={screen_rect_desktop.top}, 右={screen_rect_desktop.right}, 下={screen_rect_desktop.bottom}") is_covering_screen = ( rect.left <= screen_rect_desktop.left and rect.top <= screen_rect_desktop.top and rect.right >= screen_rect_desktop.right and rect.bottom >= screen_rect_desktop.bottom ) if is_covering_screen: screen_area = (screen_rect_desktop.right - screen_rect_desktop.left) * (screen_rect_desktop.bottom - screen_rect_desktop.top) window_area = (rect.right - rect.left) * (rect.bottom - rect.top) is_fullscreen = window_area >= screen_area * 0.95 # logger.debug(f"覆盖屏幕: {is_covering_screen}, 窗口面积: {window_area}, 屏幕面积: {screen_area}, 是否全屏判断: {is_fullscreen}") return is_fullscreen return False class ErrorDialog(Dialog): # 重大错误提示框 def __init__(self, error_details='Traceback (most recent call last):', parent=None): # KeyboardInterrupt 直接 exit if error_details.endswith('KeyboardInterrupt') or error_details.endswith('KeyboardInterrupt\n'): stop() super().__init__( 'Class Widgets 崩溃报告', '抱歉!Class Widgets 发生了严重的错误从而无法正常运行。您可以保存下方的错误信息并向他人求助。' '若您认为这是程序的Bug,请点击“报告此问题”或联系开发者。', parent ) global error_dialog error_dialog = True self.is_dragging = False self.drag_position = QPoint() self.title_bar_height = 30 self.title_layout = QHBoxLayout() self.iconLabel = ImageLabel() self.iconLabel.setImage(f"{base_directory}/img/logo/favicon-error.ico") self.error_log = PlainTextEdit() self.report_problem = PushButton(fIcon.FEEDBACK, '报告此问题') self.copy_log_btn = PushButton(fIcon.COPY, '复制日志') self.ignore_error_btn = PushButton(fIcon.INFO, '忽略错误') self.ignore_same_error = CheckBox() self.ignore_same_error.setText('在下次启动之前,忽略此错误') self.restart_btn = PrimaryPushButton(fIcon.SYNC, '重新启动') self.iconLabel.setScaledContents(True) self.iconLabel.setFixedSize(50, 50) self.titleLabel.setText('出错啦!ヽ(*。>Д<)o゜') self.titleLabel.setStyleSheet("font-family: Microsoft YaHei UI; font-size: 25px; font-weight: 500;") self.error_log.setReadOnly(True) self.error_log.setPlainText(error_details) self.error_log.setFixedHeight(200) self.restart_btn.setFixedWidth(150) self.yesButton.hide() self.cancelButton.hide() # 隐藏取消按钮 self.title_layout.setSpacing(12) # 按钮事件 self.report_problem.clicked.connect( lambda: QDesktopServices.openUrl(QUrl( 'https://github.com/Class-Widgets/Class-Widgets/issues/' 'new?assignees=&labels=Bug&projects=&template=BugReport.yml&title=[Bug]:')) ) self.copy_log_btn.clicked.connect(self.copy_log) self.ignore_error_btn.clicked.connect(self.ignore_error) self.restart_btn.clicked.connect(restart) self.title_layout.addWidget(self.iconLabel) # 标题布局 self.title_layout.addWidget(self.titleLabel) self.textLayout.insertLayout(0, self.title_layout) # 页面 self.textLayout.addWidget(self.error_log) self.textLayout.addWidget(self.ignore_same_error) self.buttonLayout.insertStretch(0, 1) # 按钮布局 self.buttonLayout.insertWidget(0, self.copy_log_btn) self.buttonLayout.insertWidget(1, self.report_problem) self.buttonLayout.insertStretch(1) self.buttonLayout.insertWidget(4, self.ignore_error_btn) self.buttonLayout.insertWidget(5, self.restart_btn) def copy_log(self): # 复制日志 QApplication.clipboard().setText(self.error_log.toPlainText()) Flyout.create( icon=InfoBarIcon.SUCCESS, title='复制成功!ヾ(^▽^*)))', content="日志已成功复制到剪贴板。", target=self.copy_log_btn, parent=self, isClosable=True, aniType=FlyoutAnimationType.PULL_UP ) def ignore_error(self): global ignore_errors if self.ignore_same_error.isChecked(): ignore_errors.append(self.error_log.toPlainText()) self.close() def mousePressEvent(self, event): if event.button() == Qt.LeftButton and event.y() <= self.title_bar_height: self.is_dragging = True self.drag_position = event.globalPos() - self.frameGeometry().topLeft() def mouseMoveEvent(self, event): if self.is_dragging: self.move(event.globalPos() - self.drag_position) def mouseReleaseEvent(self, event): if event.button() == Qt.LeftButton: self.is_dragging = False class PluginManager: # 插件管理器 def __init__(self): self.cw_contexts = {} self.get_app_contexts() self.temp_window = [] self.method = PluginMethod(self.cw_contexts) def get_app_contexts(self, path=None): self.cw_contexts = { "Widgets_Width": list_.widget_width, "Widgets_Name": list_.widget_name, "Widgets_Code": list_.widget_conf, # 小组件列表 "Current_Lesson": current_lesson_name, # 当前课程名 "State": current_state, # 0:课间 1:上课(上下课状态) "Current_Part": get_part(), # 返回开始时间、Part序号 "Next_Lessons_text": get_next_lessons_text(), # 下节课程 "Next_Lessons": next_lessons, # 下节课程 "Current_Lessons": current_lessons, # 当前课程 "Current_Week": current_week, # 当前周次 "Excluded_Lessons": excluded_lessons, # 排除的课程 "Current_Time": current_time, # 当前时间 "Timeline_Data": timeline_data, # 时间线数据 "Parts_Start_Time": parts_start_time, # 节点开始时间 "Parts_Type": parts_type, # 节点类型 "Time_Offset": time_offset, # 时差偏移 "Schedule_Name": config_center.schedule_name, # 课程表名称 "Loaded_Data": loaded_data, # 加载的课程表数据 "Order": order, # 课程顺序 "Weather": weather_name, # 天气情况 "Temp": temperature, # 温度 "Weather_Data": weather_data_temp, # 天气数据 "Weather_Icon": weather_icon, # 天气图标 "Weather_API": config_center.read_conf('Weather', 'api'), # 天气API "City": city, # 城市代码 "Notification": notification.notification_contents, # 检测到的通知内容 "Last_Notify_Time": last_notify_time, # 上次通知时间 "PLUGIN_PATH": os.path.normpath(os.path.join(conf.PLUGINS_DIR, path)) if path else conf.PLUGINS_DIR, # 传递插件目录 "Config_Center": config_center, # 配置中心实例 "Schedule_Center": schedule_center, # 课程表中心实例 "Base_Directory": base_directory, # 资源目录 "Widgets_Mgr": mgr, # 组件管理器实例 "Theme": theme, # 当前主题 } return self.cw_contexts class PluginMethod: # 插件方法 def __init__(self, app_context): self.app_contexts = app_context def register_widget(self, widget_code, widget_name, widget_width): # 注册小组件 self.app_contexts['Widgets_Width'][widget_code] = widget_width self.app_contexts['Widgets_Name'][widget_code] = widget_name self.app_contexts['Widgets_Code'][widget_name] = widget_code def adjust_widget_width(self, widget_code, width): # 调整小组件宽度 self.app_contexts['Widgets_Width'][widget_code] = width @staticmethod def get_widget(widget_code): # 获取小组件实例 for widget in mgr.widgets: if widget.path == widget_code: return widget return None @staticmethod def change_widget_content(widget_code, title, content): # 修改小组件内容 for widget in mgr.widgets: if widget.path == widget_code: widget.update_widget_for_plugin([title, content]) @staticmethod def is_get_notification(): # 检查是否有通知 if notification.pushed_notification: return True else: return False @staticmethod def send_notification(state=1, lesson_name='示例课程', title='通知示例', subtitle='副标题', content='这是一条通知示例', icon=None, duration=2000): # 发送通知 notification.main(state, lesson_name, title, subtitle, content, icon, duration) @staticmethod def subprocess_exec(title, action): # 执行系统命令 w = openProgressDialog(title, action) p_mgr.temp_window = [w] w.show() @staticmethod def read_config(path, section, option): # 读取配置文件 try: with open(path, 'r', encoding='utf-8') as r: config = json.load(r) return config.get(section, option) except Exception as e: logger.error(f"插件读取配置文件失败:{e}") @staticmethod def generate_speech( text: str, engine: str = "edge", voice: Optional[str] = None, timeout: float = 10.0, auto_fallback: bool = True ) -> str: """ 同步生成语音文件(供插件调用) 参数: text (str): 要转换的文本(支持中英文混合) engine (str): 首选的TTS引擎(默认edge) voice (str): 指定语音ID(可选,默认自动选择) timeout (float): 超时时间(秒,默认10) auto_fallback (bool): 是否自动回退引擎(默认True) 返回: str: 生成的音频文件路径 """ return generate_speech_sync( text=text, engine=engine, voice=voice, auto_fallback=auto_fallback, timeout=timeout ) @staticmethod def play_audio(file_path: str, tts_delete_after: bool = True): """ 播放音频文件 参数: file_path (str): 要播放的音频文件路径 tts_delete_after (bool): 播放后是否删除文件(默认True) 说明: - 删除操作有重试机制(3次尝试) """ play_audio(file_path, tts_delete_after) class WidgetsManager: def __init__(self): self.widgets = [] # 小组件实例 self.widgets_list = [] # 小组件列表配置 self.state = 1 self.widgets_width = 0 # 小组件总宽度 self.spacing = 0 # 小组件间隔 self.start_pos_x = 0 # 小组件起始位置 self.start_pos_y = 0 self.hide_status = None # [0] -> 在 current_state 设置的灵活隐藏, [1] -> 隐藏模式 def sync_widget_animation(self, target_pos): for widget in self.widgets: if widget.path == 'widget-current-activity.ui': widget.animate_expand(target_pos) # 主组件形变动画 def init_widgets(self): # 初始化小组件 self.widgets_list = list_.get_widget_config() self.check_widgets_exist() self.spacing = conf.load_theme_config(theme)['spacing'] self.get_start_pos() cnt_all = {} # 添加小组件实例 for w in range(len(self.widgets_list)): cnt_all[self.widgets_list[w]] = cnt_all.get(self.widgets_list[w], -1) + 1 widget = DesktopWidget(self, self.widgets_list[w], True if w == 0 else False,cnt = cnt_all[self.widgets_list[w]], position=self.get_widget_pos("", w), widget_cnt = w) self.widgets.append(widget) self.create_widgets() def close_all_widgets(self): # 统一关闭所有组件 if hasattr(self, '_closing'): return self._closing = True for widget in self.widgets: widget.close() # 触发各个widget的closeEvent def check_widgets_exist(self): for widget in self.widgets_list: if widget not in list_.widget_width.keys(): self.widgets_list.remove(widget) @staticmethod def get_widget_width(path): try: width = conf.load_theme_width(theme)[path] except KeyError: width = list_.widget_width[path] return int(width) @staticmethod def get_widgets_height(): return int(conf.load_theme_config(theme)['height']) def create_widgets(self): for widget in self.widgets: widget.show() logger.info(f'显示小组件:{widget.path, widget.windowTitle()}') def adjust_ui(self): # 更新小组件UI for widget in self.widgets: # 调整窗口尺寸 width = self.get_widget_width(widget.path) height = self.get_widgets_height() pos_x = self.get_widget_pos(widget.path, widget.widget_cnt)[0] op = int(config_center.read_conf('General', 'opacity')) / 100 if widget.animation is None: widget.widget_transition(pos_x, width, height, op) def get_widget_pos(self, path, cnt=None): # 获取小组件位置 num = self.widgets_list.index(path) if cnt is None else cnt self.get_start_pos() pos_x = self.start_pos_x + self.spacing * num for i in range(num): try: pos_x += conf.load_theme_width(theme)[self.widgets_list[i]] except KeyError: pos_x += list_.widget_width[self.widgets_list[i]] except: pos_x += 0 return [int(pos_x), int(self.start_pos_y)] def get_start_pos(self): self.calculate_widgets_width() screen_geometry = app.primaryScreen().availableGeometry() screen_width = screen_geometry.width() screen_height = screen_geometry.height() margin = max(0, int(config_center.read_conf('General', 'margin'))) self.start_pos_y = margin self.start_pos_x = (screen_width - self.widgets_width) // 2 def calculate_widgets_width(self): # 计算小组件占用宽度 self.widgets_width = 0 # 累加小组件宽度 for widget in self.widgets_list: try: self.widgets_width += self.get_widget_width(widget) except Exception as e: logger.warning(f'计算小组件宽度发生错误:{e}') self.widgets_width += 0 self.widgets_width += self.spacing * (len(self.widgets_list) - 1) def hide_windows(self): self.state = 0 for widget in self.widgets: widget.animate_hide() def full_hide_windows(self): self.state = 0 for widget in self.widgets: widget.animate_hide(True) def show_windows(self): if fw.animating: # 避免动画Bug return if fw.isVisible(): fw.close() self.state = 1 for widget in self.widgets: widget.animate_show() def clear_widgets(self): global fw, was_floating_mode if fw and fw.isVisible(): fw.close() was_floating_mode = True else: was_floating_mode = False for widget in self.widgets: widget.animate_hide_opacity() for widget in self.widgets: self.widgets.remove(widget) init() def update_widgets(self): c = 0 self.adjust_ui() for widget in self.widgets: if c == 0: get_countdown(True) widget.update_data(path=widget.path) c += 1 p_loader.update_plugins() if notification.pushed_notification: notification.pushed_notification = False def decide_to_hide(self): if config_center.read_conf('General', 'hide_method') == '0': # 正常 self.hide_windows() elif config_center.read_conf('General', 'hide_method') == '1': # 单击即完全隐藏 self.full_hide_windows() elif config_center.read_conf('General', 'hide_method') == '2': # 最小化为浮窗 if not fw.animating: self.full_hide_windows() fw.show() else: self.hide_windows() def cleanup_resources(self): self.hide_status = None # 重置hide_status widgets_to_clean = list(self.widgets) self.widgets.clear() for widget in widgets_to_clean: widget_path = getattr(widget, 'path', '未知组件') try: if hasattr(widget, 'weather_timer') and widget.weather_timer: try: widget.weather_timer.stop() except RuntimeError: pass if hasattr(widget, 'weather_thread') and widget.weather_thread: try: if widget.weather_thread.isRunning(): widget.weather_thread.quit() if not widget.weather_thread.wait(500): logger.warning(f"组件 {widget_path} 的天气线程未正常退出,强制终止") widget.weather_thread.terminate() widget.weather_thread.wait() except RuntimeError: pass widget.deleteLater() except Exception as ex: logger.error(f"清理组件 {widget_path} 时发生异常: {ex}") def stop(self): if mgr: mgr.cleanup_resources() for widget in self.widgets: widget.stop() if self.animation: self.animation.stop() if self.opacity_animation: self.opacity_animation.stop() self.close() class openProgressDialog(QWidget): def __init__(self, action_title='打开 记事本', action='notepad'): super().__init__() self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnTopHint | Qt.Tool) time = int(config_center.read_conf('Plugin', 'auto_delay')) self.action = action screen_geometry = app.primaryScreen().availableGeometry() self.screen_width = screen_geometry.width() self.screen_height = screen_geometry.height() self.init_ui() self.init_font() self.move((self.screen_width - self.width()) // 2, self.screen_height - self.height() - 100) self.action_name = self.findChild(QLabel, 'action_name') self.action_name.setText(action_title) self.opening_countdown = self.findChild(ProgressRing, 'opening_countdown') self.opening_countdown.setRange(0, time - 1) self.progress_timer = QTimer(self) self.progress_timer.timeout.connect(self.update_progress) self.progress_timer.start(1000) self.timer = QTimer(self) self.timer.timeout.connect(self.execute_action) self.timer.start(time * 1000) self.cancel_opening = self.findChild(QPushButton, 'cancel_opening') self.cancel_opening.clicked.connect(self.cancel_action) self.intro_animation() def update_progress(self): self.opening_countdown.setValue(self.opening_countdown.value() + 1) def execute_action(self): self.timer.stop() subprocess.Popen(self.action) self.close() def cancel_action(self): self.timer.stop() self.close() def save_position(self): pass def init_ui(self): self.setWindowFlags( Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint | Qt.X11BypassWindowManagerHint # 绕过窗口管理器以在全屏显示通知 ) self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) if isDarkTheme(): uic.loadUi(f'{base_directory}/ui/default/dark/toast-open_dialog.ui', self) else: uic.loadUi(f'{base_directory}/ui/default/toast-open_dialog.ui', self) backgnd = self.findChild(QFrame, 'backgnd') shadow_effect = QGraphicsDropShadowEffect(self) shadow_effect.setBlurRadius(28) shadow_effect.setXOffset(0) shadow_effect.setYOffset(6) shadow_effect.setColor(QColor(0, 0, 0, 80)) backgnd.setGraphicsEffect(shadow_effect) def init_font(self): font_path = f'{base_directory}/font/HarmonyOS_Sans_SC_Bold.ttf' font_id = QFontDatabase.addApplicationFont(font_path) if font_id != -1: font_family = QFontDatabase.applicationFontFamilies(font_id)[0] self.setStyleSheet(f""" QLabel, ProgressRing, PushButton{{ font-family: "{font_family}"; font-weight: bold }} """) def intro_animation(self): # 弹出动画 self.setMinimumWidth(300) label_width = self.action_name.sizeHint().width() - 120 self.animation = QPropertyAnimation(self, b'windowOpacity') self.animation.setDuration(400) self.animation.setStartValue(0) self.animation.setEndValue(1) self.animation.setEasingCurve(QEasingCurve.Type.InOutCirc) self.animation_rect = QPropertyAnimation(self, b'geometry') self.animation_rect.setDuration(450) self.animation_rect.setStartValue( QRect(self.x(), self.screen_height, self.width(), self.height()) ) self.animation_rect.setEndValue( QRect((self.screen_width - (self.width() + label_width)) // 2, self.screen_height - 250, self.width() + label_width, self.height()) ) self.animation_rect.setEasingCurve(QEasingCurve.Type.InOutCirc) self.animation.start() self.animation_rect.start() def closeEvent(self, event): event.ignore() self.setMinimumWidth(0) self.position = self.pos() # 关闭时保存一次位置 self.save_position() self.deleteLater() self.hide() p_mgr.temp_window.clear() class FloatingWidget(QWidget): # 浮窗 def __init__(self): super().__init__() self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnTopHint | Qt.Tool) self.animation_rect = None self.animation = None self.m_Position = None self.p_Position = None self.m_flag = None self.r_Position = None self._is_topmost_callback_added = False self.init_ui() self.init_font() self.position = None self.animating = False self.focusing = False self.text_changed = False self.current_lesson_name_text = self.findChild(QLabel, 'subject') self.activity_countdown = self.findChild(QLabel, 'activity_countdown') self.countdown_progress_bar = self.findChild(ProgressRing, 'progressBar') self.setFocusPolicy(Qt.FocusPolicy.NoFocus) self.setAttribute(Qt.WA_TransparentForMouseEvents, False) # 动态获取屏幕尺寸 screen_geometry = QApplication.primaryScreen().availableGeometry() screen_width = screen_geometry.width() screen_height = screen_geometry.height() # 加载保存的位置 saved_pos = self.load_position() if saved_pos: # 边界检查 saved_pos = self.adjust_position_to_screen(saved_pos) self.position = saved_pos else: # 使用动态计算的默认位置 self.position = QPoint( (screen_width - self.width()) // 2, # 居中横向 50 # 距离顶部 50px ) update_timer.add_callback(self.update_data) def adjust_position_to_screen(self, pos): screen = QApplication.screenAt(pos) if not screen: screen = QApplication.primaryScreen() screen_geometry = screen.availableGeometry() window_width = self.width() window_height = self.height() # 计算屏幕边界 screen_left = screen_geometry.x() screen_right = screen_geometry.x() + screen_geometry.width() screen_top = screen_geometry.y() screen_bottom = screen_geometry.y() + screen_geometry.height() new_x, new_y = pos.x(), pos.y() if pos.x() < screen_left: # 当窗口可见部分不足50%时调整 visible_width = (pos.x() + window_width) - screen_left if visible_width < window_width / 2: new_x = screen_left elif (pos.x() + window_width) > screen_right: visible_width = screen_right - pos.x() if visible_width < window_width / 2: new_x = screen_right - window_width if pos.y() < screen_top: visible_height = (pos.y() + window_height) - screen_top if visible_height < window_height / 2: new_y = screen_top elif (pos.y() + window_height) > screen_bottom: visible_height = screen_bottom - pos.y() if visible_height < window_height / 2: new_y = screen_bottom - window_height return QPoint(new_x, new_y) def _ensure_topmost(self): # 始终处于顶层 if active_windows: return if os.name == 'nt': try: hwnd = self.winId().__int__() if ctypes.windll.user32.IsWindow(hwnd): HWND_TOPMOST = -1 SWP_NOMOVE = 0x0002 SWP_NOSIZE = 0x0001 SWP_SHOWWINDOW = 0x0040 SWP_NOACTIVATE = 0x0010 ctypes.windll.user32.SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOACTIVATE | SWP_NOSIZE | SWP_SHOWWINDOW) self.raise_() else: if self._is_topmost_callback_added: try: utils.update_timer.remove_callback(self._ensure_topmost) except ValueError: pass # 可能已经被移除了 self._is_topmost_callback_added = False logger.debug(f"句柄 {hwnd} 无效,已移除置顶回调。") except RuntimeError as e: if 'Internal C++ object' in str(e) and 'already deleted' in str(e): logger.debug(f"尝试访问已删除的 FloatingWidget 时出错,移除回调: {e}") if self._is_topmost_callback_added: try: utils.update_timer.remove_callback(self._ensure_topmost) except ValueError: pass # 可能已经被移除了 self._is_topmost_callback_added = False else: logger.error(f"检查或设置浮窗置顶时发生运行时错误: {e}") except Exception as e: logger.error(f"检查或设置浮窗置顶时出错: {e}") if self._is_topmost_callback_added: try: utils.update_timer.remove_callback(self._ensure_topmost) except ValueError: pass self._is_topmost_callback_added = False logger.debug(f"因错误 {e} 移除浮窗置顶回调。") def save_position(self): current_screen = QApplication.screenAt(self.pos()) if not current_screen: current_screen = QApplication.primaryScreen() screen_geometry = current_screen.availableGeometry() pos = self.pos() x = pos.x() window_width = self.width() if mgr.state: return screen_left = screen_geometry.left() screen_right = screen_geometry.right() if x < screen_left: visible_width = (x + window_width) - screen_left if visible_width < window_width / 2: x = screen_left elif (x + window_width) > screen_right: if self.animating: return visible_width = screen_right - x if visible_width < window_width / 2: x = screen_right - window_width y = min(max(pos.y(), screen_geometry.top()), screen_geometry.bottom()) pos = QPoint(x, y) config_center.write_conf('FloatingWidget', 'pos_x', str(pos.x())) if not self.animating: config_center.write_conf('FloatingWidget', 'pos_y', str(pos.y())) def load_position(self): x = config_center.read_conf('FloatingWidget', 'pos_x') y = config_center.read_conf('FloatingWidget', 'pos_y') if x and y: return QPoint(int(x), int(y)) return None def init_ui(self): setTheme_() if os.path.exists(f'{base_directory}/ui/{theme}/widget-floating.ui'): if isDarkTheme() and conf.load_theme_config(theme)['support_dark_mode']: uic.loadUi(f'{base_directory}/ui/{theme}/dark/widget-floating.ui', self) else: uic.loadUi(f'{base_directory}/ui/{theme}/widget-floating.ui', self) else: if isDarkTheme() and conf.load_theme_config(theme)['support_dark_mode']: uic.loadUi(f'{base_directory}/ui/default/dark/widget-floating.ui', self) else: uic.loadUi(f'{base_directory}/ui/default/widget-floating.ui', self) # 设置窗口无边框和透明背景 self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) # 根据平台和设置应用窗口标志 if sys.platform == 'darwin': flags = Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint | Qt.WindowType.Widget | Qt.X11BypassWindowManagerHint else: flags = Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint | Qt.WindowType.Tool | Qt.X11BypassWindowManagerHint self.setWindowFlags(flags) # 始终添加置顶回调逻辑 if os.name == 'nt': if not self._is_topmost_callback_added: try: if hasattr(utils, 'update_timer') and utils.update_timer: utils.update_timer.add_callback(self._ensure_topmost) self._is_topmost_callback_added = True self._ensure_topmost() # 立即执行一次确保初始置顶 else: logger.warning("utils.update_timer 不可用,无法为浮窗添加置顶回调。") except Exception as e: logger.error(f"为浮窗添加置顶回调时出错: {e}") if sys.platform == 'darwin': self.setWindowFlags( Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint | Qt.WindowType.Widget | # macOS 失焦时仍然显示 Qt.X11BypassWindowManagerHint # 绕过窗口管理器以在全屏显示通知 ) else: self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint | Qt.WindowType.Tool | Qt.X11BypassWindowManagerHint # 绕过窗口管理器以在全屏显示通知 ) backgnd = self.findChild(QFrame, 'backgnd') shadow_effect = QGraphicsDropShadowEffect(self) shadow_effect.setBlurRadius(28) shadow_effect.setXOffset(0) shadow_effect.setYOffset(6) shadow_effect.setColor(QColor(0, 0, 0, 75)) backgnd.setGraphicsEffect(shadow_effect) def init_font(self): font_path = f'{base_directory}/font/HarmonyOS_Sans_SC_Bold.ttf' font_id = QFontDatabase.addApplicationFont(font_path) if font_id != -1: font_family = QFontDatabase.applicationFontFamilies(font_id)[0] self.setStyleSheet(f""" QLabel, ProgressRing{{ font-family: "{font_family}"; }} """) def update_data(self): time_color = QColor(f'#{config_center.read_conf("Color", "floating_time")}') self.activity_countdown.setStyleSheet(f"color: {time_color.name()};") if self.animating: # 执行动画时跳过更新 return if platform.system() == 'Windows' and platform.release() != '7': self.setWindowOpacity(int(config_center.read_conf('General', 'opacity')) / 100) # 设置窗口透明度 else: self.setWindowOpacity(1.0) cd_list = get_countdown() self.text_changed = False if self.current_lesson_name_text.text() != current_lesson_name: self.text_changed = True self.current_lesson_name_text.setText(current_lesson_name) if cd_list: # 模糊倒计时 blur_floating = config_center.read_conf('General', 'blur_floating_countdown') == '1' if blur_floating: # 模糊显示 if cd_list[1] == '00:00': self.activity_countdown.setText(f"< - 分钟") else: minutes = int(cd_list[1].split(':')[0]) + 1 self.activity_countdown.setText(f"< {minutes} 分钟") else: # 精确显示 self.activity_countdown.setText(cd_list[1]) self.countdown_progress_bar.setValue(cd_list[2]) self.adjustSize_animation() self.update() def showEvent(self, event): # 窗口显示 logger.info('显示浮窗') current_screen = QApplication.screenAt(self.pos()) or QApplication.primaryScreen() screen_geometry = current_screen.availableGeometry() if self.position: if self.position.y() > screen_geometry.center().y(): # 下半屏 start_pos = QPoint( self.position.x(), screen_geometry.bottom() + self.height() ) else: # 上半屏 start_pos = QPoint( self.position.x(), screen_geometry.top() - self.height() ) else: # 默认:顶部中央滑入 start_pos = QPoint( (screen_geometry.width() - self.width()) // 2, screen_geometry.top() - self.height() ) self.position = QPoint( (screen_geometry.width() - self.width()) // 2, max(50, int(config_center.read_conf('General', 'margin'))) ) self.animation = QPropertyAnimation(self, b'windowOpacity') self.animation.setDuration(450) self.animation.setStartValue(0) self.animation.setEndValue(int(config_center.read_conf('General', 'opacity')) / 100) self.animation.setEasingCurve(QEasingCurve.Type.OutCubic) self.animation_rect = QPropertyAnimation(self, b'geometry') self.animation_rect.setDuration(600) self.animation_rect.setStartValue(QRect(start_pos, self.size())) self.animation_rect.setEndValue(QRect(self.position, self.size())) if platform.system() == 'Darwin': self.animation_rect.setEasingCurve(QEasingCurve.Type.OutQuad) elif platform.system() == 'Windows': self.animation_rect.setEasingCurve(QEasingCurve.Type.OutBack) else: self.animation_rect.setEasingCurve(QEasingCurve.Type.OutCubic) self.animating = True self.animation.start() self.animation_rect.start() self.animation_rect.finished.connect(self.animation_done) def animation_done(self): self.animating = False def closeEvent(self, event): # 跳过动画 if QApplication.instance().closingDown(): self.save_position() event.accept() return event.ignore() self.setMinimumWidth(0) self.position = self.pos() self.save_position() current_screen = QApplication.screenAt(self.pos()) if not current_screen: current_screen = QApplication.primaryScreen() screen_geometry = current_screen.availableGeometry() screen_center_y = screen_geometry.y() + (screen_geometry.height() // 2) # 动态动画 current_pos = self.pos() base_duration = 350 # 基础 max_duration = 550 # 最大 min_duration = 250 # 最小 # 获取主组件位置 main_widget = next( (w for w in mgr.widgets if w.path == 'widget-current-activity.ui'), None ) if main_widget: if current_pos.y() > screen_center_y: # 下半屏 # 屏幕底部 target_y = screen_geometry.bottom() + self.height() + 10 # 任务栏补偿 if platform.system() == "Windows": target_y += 30 target_pos = QPoint( main_widget.x(), target_y ) distance = abs(current_pos.y() - target_y) else: # 上半屏 target_pos = main_widget.pos() distance = abs(current_pos.y() - target_pos.y()) else: target_pos = QPoint( screen_geometry.center().x() - self.width() // 2, int(config_center.read_conf('General', 'margin')) ) distance = abs(current_pos.y() - target_pos.y()) max_distance = screen_geometry.height() distance_ratio = min(distance / max_distance, 1.0) duration = int(base_duration + (max_duration - base_duration) * (distance_ratio ** 0.7)) duration = max(min_duration, min(duration, max_duration)) # 多平台兼容 if platform.system() == "Darwin": curve = QEasingCurve.Type.OutQuad duration = int(duration * 0.85) elif platform.system() == "Windows": curve = QEasingCurve.Type.OutCubic if current_pos.y() > screen_center_y: duration += 50 # 底部移动稍慢 curve = QEasingCurve.Type.InOutQuad self.animation = QPropertyAnimation(self, b"windowOpacity") self.animation.setDuration(int(duration * 1.15)) self.animation.setStartValue(self.windowOpacity()) self.animation.setEndValue(0.0) self.animation_rect = QPropertyAnimation(self, b"geometry") self.animation_rect.setDuration(duration) self.animation_rect.setStartValue(self.geometry()) self.animation_rect.setEndValue(QRect(target_pos, self.size())) self.animation_rect.setEasingCurve(curve) self.animating = True self.animation.start() self.animation_rect.start() def cleanup(): self.hide() self.save_position() self.animating = False if self._is_topmost_callback_added: try: utils.update_timer.remove_callback(self._ensure_topmost) except ValueError: pass self._is_topmost_callback_added = False self.animation_rect.finished.connect(cleanup) def hideEvent(self, event): event.accept() logger.info('隐藏浮窗') self.animating = False self.setMinimumSize(QSize(self.width(), self.height())) def adjustSize_animation(self): if not self.text_changed: return self.setMinimumWidth(200) current_geometry = self.geometry() label_width = self.current_lesson_name_text.sizeHint().width() + 120 offset = label_width - current_geometry.width() target_geometry = current_geometry.adjusted(0, 0, offset, 0) self.animation = QPropertyAnimation(self, b'geometry') self.animation.setDuration(450) self.animation.setStartValue(current_geometry) self.animation.setEndValue(target_geometry) self.animation.setEasingCurve(QEasingCurve.Type.InOutCirc) self.animating = True # 避免动画Bug x114514 self.animation.start() self.animation.finished.connect(self.animation_done) def mousePressEvent(self, event): if event.button() == Qt.MouseButton.LeftButton: self.m_flag = True self.m_Position = event.globalPos() - self.pos() # 获取鼠标相对窗口的位置 self.p_Position = event.globalPos() # 获取鼠标相对屏幕的位置 event.accept() def mouseMoveEvent(self, event): if event.buttons() == Qt.MouseButton.LeftButton and self.m_flag: self.move(event.globalPos() - self.m_Position) # 更改窗口位置 event.accept() def mouseReleaseEvent(self, event): self.r_Position = event.globalPos() # 获取鼠标相对窗口的位置 self.m_flag = False # 保存位置到配置文件 self.save_position() # 特定隐藏模式下不执行操作 hide_mode = config_center.read_conf('General', 'hide') if hide_mode == '1' or hide_mode == '2': return # 阻止手动展开/收起 if ( hasattr(self, "p_Position") and self.r_Position == self.p_Position and not self.animating ): # 非特定隐藏模式下执行点击事件 if hide_mode == '3': if mgr.state: mgr.decide_to_hide() mgr.hide_status = (current_state, 1) else: mgr.show_windows() mgr.hide_status = (current_state, 0) elif hide_mode == '1': mgr.show_windows() self.close() def focusInEvent(self, event): self.focusing = True def focusOutEvent(self, event): self.focusing = False def stop(self): if mgr: mgr.cleanup_resources() for widget in self.widgets: widget.stop() if self.animation: self.animation.stop() if self.opacity_animation: self.opacity_animation.stop() self.close() class DesktopWidget(QWidget): # 主要小组件 def __init__(self, parent=WidgetsManager, path='widget-time.ui', enable_tray=False, cnt=0, position=None, widget_cnt = None): super().__init__() self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnTopHint | Qt.Tool) self.cnt = cnt self.widget_cnt = widget_cnt self.tray_menu = None self.last_widgets = list_.get_widget_config() self.path = path self.last_code = 101010100 self.radius = conf.load_theme_config(theme)['radius'] self.last_theme = config_center.read_conf('General', 'theme') self.last_color_mode = config_center.read_conf('General', 'color_mode') self.w = 100 # 天气预警动画相关 self.weather_alert_timer = None self.weather_alert_animation = None self.weather_alert_text = None self.alert_showing = False self.position = parent.get_widget_pos(self.path) if position is None else position self.animation = None self.opacity_animation = None mgr.hide_status = None self._is_topmost_callback_added = False # 添加一个标志来跟踪回调是否已添加 try: self.w = conf.load_theme_config(theme)['widget_width'][self.path] except KeyError: self.w = list_.widget_width[self.path] self.h = conf.load_theme_config(theme)['height'] init_config() self.init_ui(path) self.init_font() if enable_tray: self.init_tray_menu() # 初始化托盘菜单 # 样式 self.backgnd = self.findChild(QFrame, 'backgnd') if self.backgnd is None: self.backgnd = self.findChild(QLabel, 'backgnd') stylesheet = self.backgnd.styleSheet() # 应用圆角 updated_stylesheet = re.sub(r'border-radius:\d+px;', f'border-radius:{self.radius}px;', stylesheet) self.backgnd.setStyleSheet(updated_stylesheet) if path == 'widget-time.ui': # 日期显示 self.date_text = self.findChild(QLabel, 'date_text') self.date_text.setText(f'{today.year} 年 {today.month} 月') self.day_text = self.findChild(QLabel, 'day_text') self.day_text.setText(f'{today.day}日 {list_.week[today.weekday()]}') elif path == 'widget-countdown.ui': # 活动倒计时 self.countdown_progress_bar = self.findChild(QProgressBar, 'progressBar') self.activity_countdown = self.findChild(QLabel, 'activity_countdown') self.ac_title = self.findChild(QLabel, 'activity_countdown_title') elif path == 'widget-current-activity.ui': # 当前活动 self.current_subject = self.findChild(QPushButton, 'subject') self.blur_effect_label = self.findChild(QLabel, 'blurEffect') # 模糊效果 self.blur_effect = QGraphicsBlurEffect() self.current_subject.mouseReleaseEvent = self.rightReleaseEvent update_timer.add_callback(self.detect_theme_changed) elif path == 'widget-next-activity.ui': # 接下来的活动 self.nl_text = self.findChild(QLabel, 'next_lesson_text') elif path == 'widget-countdown-day.ui': # 自定义倒计时 self.custom_title = self.findChild(QLabel, 'countdown_custom_title') self.custom_countdown = self.findChild(QLabel, 'custom_countdown') elif path == 'widget-weather.ui': # 天气组件 content_layout = self.findChild(QHBoxLayout, 'horizontalLayout_2') content_layout.setSpacing(1) self.temperature = self.findChild(QLabel, 'temperature') self.weather_icon = self.findChild(QLabel, 'weather_icon') self.alert_icon = IconWidget(self) self.alert_icon.setFixedSize(22,22) self.alert_icon.hide() # 预警标签 self.weather_alert_text = QLabel(self) self.weather_alert_text.setAlignment(Qt.AlignCenter) self.weather_alert_text.setStyleSheet(self.temperature.styleSheet()) self.weather_alert_text.setFont(self.temperature.font()) self.weather_alert_text.hide() content_layout.addWidget(self.alert_icon) content_layout.addWidget(self.weather_alert_text) self.weather_alert_timer = None self.weather_alert_opacity = QGraphicsOpacityEffect(self) self.weather_alert_opacity.setOpacity(1.0) self.weather_alert_text.setGraphicsEffect(self.weather_alert_opacity) self.weather_alert_animation = QPropertyAnimation(self.weather_alert_opacity, b"opacity") self.weather_alert_animation.setDuration(700) self.weather_alert_animation.setEasingCurve(QEasingCurve.OutCubic) self.alert_icon_opacity = QGraphicsOpacityEffect(self) self.alert_icon_opacity.setOpacity(1.0) self.alert_icon.setGraphicsEffect(self.alert_icon_opacity) self.alert_icon_animation = QPropertyAnimation(self.alert_icon_opacity, b"opacity") self.alert_icon_animation.setDuration(700) self.alert_icon_animation.setEasingCurve(QEasingCurve.OutCubic) self.showing_temperature = True # 跟踪状态(预警/气温) self.get_weather_data() self.weather_timer = QTimer(self) self.weather_timer.setInterval(30 * 60 * 1000) # 30分钟更新一次 self.weather_timer.timeout.connect(self.get_weather_data) self.weather_timer.start() update_timer.add_callback(self.detect_weather_code_changed) if hasattr(self, 'img'): # 自定义图片主题兼容 img = self.findChild(QLabel, 'img') if platform.system() == 'Windows' and platform.release() != '7': opacity = QGraphicsOpacityEffect(self) opacity.setOpacity(0.65) img.setGraphicsEffect(opacity) self.resize(self.w, self.height()) # 设置窗口位置 if first_start: self.animate_window(self.position) if platform.system() == 'Windows' and platform.release() != '7': self.setWindowOpacity(int(config_center.read_conf('General', 'opacity')) / 100) else: self.setWindowOpacity(1.0) else: self.move(self.position[0], self.position[1]) self.resize(self.w, self.height()) if platform.system() == 'Windows' and platform.release() != '7': self.setWindowOpacity(0) self.animate_show_opacity() else: self.setWindowOpacity(1.0) self.show() self.update_data('') @staticmethod def _onThemeChangedFinished(): print('theme_changed') def update_widget_for_plugin(self, context=None): if context is None: context = ['title', 'desc'] try: title = self.findChild(QLabel, 'title') desc = self.findChild(QLabel, 'content') if title is not None: title.setText(context[0]) if desc is not None: desc.setText(context[1]) except Exception as e: logger.error(f"更新插件小组件时出错:{e}") def init_ui(self, path): if conf.load_theme_config(theme)['support_dark_mode']: if os.path.exists(f'{base_directory}/ui/{theme}/{path}'): if isDarkTheme(): uic.loadUi(f'{base_directory}/ui/{theme}/dark/{path}', self) else: uic.loadUi(f'{base_directory}/ui/{theme}/{path}', self) else: if isDarkTheme(): uic.loadUi(f'{base_directory}/ui/{theme}/dark/widget-base.ui', self) else: uic.loadUi(f'{base_directory}/ui/{theme}/widget-base.ui', self) else: if os.path.exists(f'{base_directory}/ui/{theme}/{path}'): uic.loadUi(f'{base_directory}/ui/{theme}/{path}', self) else: uic.loadUi(f'{base_directory}/ui/{theme}/widget-base.ui', self) # 设置窗口无边框和透明背景 self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) if config_center.read_conf('General', 'hide') == '2' or (not int(config_center.read_conf('General', 'enable_click'))): self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents) else: self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, False) if config_center.read_conf('General', 'pin_on_top') == '1': # 置顶 self.setWindowFlags( Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint | Qt.WindowType.WindowDoesNotAcceptFocus | Qt.X11BypassWindowManagerHint # 绕过窗口管理器以在全屏显示通知 ) # 修改为使用定时器确保持续置顶 if os.name == 'nt': if not self._is_topmost_callback_added: try: # 确保 utils.update_timer 存在且有效 if hasattr(utils, 'update_timer') and utils.update_timer: utils.update_timer.add_callback(self._ensure_topmost) self._is_topmost_callback_added = True self._ensure_topmost() # 立即执行一次确保初始置顶 # logger.debug("已添加置顶定时回调。") else: logger.warning("utils.update_timer 不可用,无法添加置顶回调。") except Exception as e: logger.error(f"添加置顶回调时出错: {e}") elif config_center.read_conf('General', 'pin_on_top') == '2': # 置底 # 避免使用WindowStaysOnBottomHint,防止争夺底层 self.setWindowFlags( Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowDoesNotAcceptFocus ) if os.name == 'nt': def set_window_pos(): hwnd = self.winId().__int__() # 稍高于最底层的值 ctypes.windll.user32.SetWindowPos(hwnd, 2, 0, 0, 0, 0, 0x0214) QTimer.singleShot(100, set_window_pos) else: QTimer.singleShot(100, self.lower) else: self.setWindowFlags( Qt.WindowType.FramelessWindowHint ) if sys.platform == 'darwin': self.setWindowFlag(Qt.WindowType.Widget, True) else: self.setWindowFlag(Qt.WindowType.Tool, True) def _ensure_topmost(self): # 突然忘记写移除了,不写了,应该没事( if active_windows: return if os.name == 'nt': try: hwnd = self.winId().__int__() if ctypes.windll.user32.IsWindow(hwnd): HWND_TOPMOST = -1 SWP_NOMOVE = 0x0002 SWP_NOSIZE = 0x0001 SWP_SHOWWINDOW = 0x0040 SWP_NOACTIVATE = 0x0010 ctypes.windll.user32.SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOACTIVATE | SWP_NOSIZE | SWP_SHOWWINDOW) self.raise_() else: if self._is_topmost_callback_added: try: utils.update_timer.remove_callback(self._ensure_topmost) except ValueError: pass # 可能已经被移除了 self._is_topmost_callback_added = False logger.debug(f"窗口句柄 {hwnd} 无效,已自动移除置顶回调。") except RuntimeError as e: if 'Internal C++ object' in str(e) and 'already deleted' in str(e): logger.debug(f"尝试访问已删除的 DesktopWidget 时出错,移除回调: {e}") if self._is_topmost_callback_added: try: utils.update_timer.remove_callback(self._ensure_topmost) except ValueError: pass # 可能已经被移除了 self._is_topmost_callback_added = False else: logger.error(f"检查或设置窗口置顶时发生运行时错误: {e}") except Exception as e: logger.error(f"检查或设置窗口置顶时出错: {e}") if self._is_topmost_callback_added: try: utils.update_timer.remove_callback(self._ensure_topmost) except ValueError: pass self._is_topmost_callback_added = False logger.debug(f"因错误 {e} 移除置顶回调。") def closeEvent(self, event): if self._is_topmost_callback_added: try: utils.update_timer.remove_callback(self._ensure_topmost) self._is_topmost_callback_added = False # logger.debug("窗口关闭,已移除置顶回调。") except ValueError: logger.debug("尝试移除不存在的置顶回调。") except Exception as e: logger.error(f"关闭窗口时移除置顶回调出错: {e}") super().closeEvent(event) self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) # 添加阴影效果 if conf.load_theme_config(theme)['shadow']: # 修改阴影问题 shadow_effect = QGraphicsDropShadowEffect(self) shadow_effect.setBlurRadius(28) shadow_effect.setXOffset(0) shadow_effect.setYOffset(6) shadow_effect.setColor(QColor(0, 0, 0, 75)) self.backgnd.setGraphicsEffect(shadow_effect) def init_font(self): font_path = f'{base_directory}/font/HarmonyOS_Sans_SC_Bold.ttf' font_id = QFontDatabase.addApplicationFont(font_path) if font_id != -1: font_family = QFontDatabase.applicationFontFamilies(font_id)[0] self.setStyleSheet(f""" QLabel, QPushButton{{ font-family: "{font_family}"; }} """) def animate_expand(self, target_geometry): self.animation = QPropertyAnimation(self, b"geometry") self.animation.setDuration(400) self.animation.setStartValue(QRect(target_geometry.x(), -self.height(), self.width(), self.height())) self.animation.setEndValue(target_geometry) self.animation.setEasingCurve(QEasingCurve.Type.OutBack) self.raise_() self.show() def init_tray_menu(self): if not first_start: return utils.tray_icon = utils.TrayIcon(self) utils.tray_icon.setToolTip(f"Class Widgets - {config_center.schedule_name[:-5]}") self.tray_menu = SystemTrayMenu(title='Class Widgets', parent=self) self.tray_menu.addActions([ Action(fIcon.HIDE, '完全隐藏/显示小组件', triggered=lambda: self.hide_show_widgets()), Action(fIcon.BACK_TO_WINDOW, '最小化为浮窗', triggered=lambda: self.minimize_to_floating()), ]) self.tray_menu.addSeparator() self.tray_menu.addActions([ Action(fIcon.SHOPPING_CART, '插件广场', triggered=open_plaza), Action(fIcon.DEVELOPER_TOOLS, '额外选项', triggered=self.open_extra_menu), Action(fIcon.SETTING, '设置', triggered=open_settings) ]) self.tray_menu.addSeparator() self.tray_menu.addAction(Action(fIcon.SYNC, '重新启动', triggered=restart)) self.tray_menu.addAction(Action(fIcon.CLOSE, '退出', triggered=stop)) utils.tray_icon.setContextMenu(self.tray_menu) utils.tray_icon.activated.connect(self.on_tray_icon_clicked) utils.tray_icon.show() @staticmethod def on_tray_icon_clicked(reason): # 点击托盘图标隐藏 if config_center.read_conf('General', 'hide') == '0': if reason == QSystemTrayIcon.ActivationReason.Trigger: if mgr.state: mgr.decide_to_hide() else: mgr.show_windows() elif config_center.read_conf('General', 'hide') == '3': if reason == QSystemTrayIcon.ActivationReason.Trigger: if mgr.state: mgr.decide_to_hide() mgr.hide_status = (current_state, 1) else: mgr.show_windows() mgr.hide_status = (current_state, 0) def rightReleaseEvent(self, event): # 右键事件 event.ignore() if event.button() == Qt.MouseButton.RightButton: self.open_extra_menu() def update_data(self, path=''): global current_time, current_week, start_y, time_offset, today today = dt.date.today() current_time = dt.datetime.now().strftime('%H:%M:%S') time_offset = conf.get_time_offset() get_start_time() get_current_lessons() get_current_lesson_name() get_excluded_lessons() get_next_lessons() hide_status = get_hide_status() if (hide_mode:=config_center.read_conf('General', 'hide')) in ['1','2']: # 上课自动隐藏 if hide_status: mgr.decide_to_hide() else: mgr.show_windows() elif hide_mode == '3': # 灵活隐藏 if mgr.hide_status is None: mgr.hide_status = (-1, hide_status) elif mgr.hide_status[0] != current_state: mgr.hide_status = (-1, hide_status) if mgr.hide_status[1]: mgr.decide_to_hide() else: mgr.show_windows() if conf.is_temp_week(): # 调休日 current_week = config_center.read_conf('Temp', 'set_week') else: current_week = dt.datetime.now().weekday() cd_list = get_countdown() if path == 'widget-time.ui': # 日期显示 self.date_text.setText(f'{today.year} 年 {today.month} 月') self.day_text.setText(f'{today.day} 日 {list_.week[today.weekday()]}') if path == 'widget-current-activity.ui': # 当前活动 self.current_subject.setText(f' {current_lesson_name}') if current_state != 2: # 非休息段 render = QSvgRenderer(list_.get_subject_icon(current_lesson_name)) self.blur_effect_label.setStyleSheet( f'background-color: rgba{list_.subject_color(current_lesson_name)}, 200);' ) else: # 休息段 render = QSvgRenderer(list_.get_subject_icon('课间')) self.blur_effect_label.setStyleSheet( f'background-color: rgba{list_.subject_color("课间")}, 200);' ) pixmap = QPixmap(render.defaultSize()) pixmap.fill(Qt.GlobalColor.transparent) painter = QPainter(pixmap) render.render(painter) if (isDarkTheme() and conf.load_theme_config(theme)['support_dark_mode'] or isDarkTheme() and conf.load_theme_config(theme)['default_theme'] == 'dark'): # 在暗色模式显示亮色图标 painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceIn) painter.fillRect(pixmap.rect(), QColor("#FFFFFF")) painter.end() self.current_subject.setIcon(QIcon(pixmap)) self.blur_effect.setBlurRadius(25) # 模糊半径 self.blur_effect_label.setGraphicsEffect(self.blur_effect) elif path == 'widget-next-activity.ui': # 接下来的活动 self.nl_text.setText(get_next_lessons_text()) if path == 'widget-countdown.ui': # 活动倒计时 if cd_list: if config_center.read_conf('General', 'blur_countdown') == '1': # 模糊倒计时 if cd_list[1] == '00:00': self.activity_countdown.setText(f"< - 分钟") else: self.activity_countdown.setText(f"< {int(cd_list[1].split(':')[0]) + 1} 分钟") else: self.activity_countdown.setText(cd_list[1]) self.ac_title.setText(cd_list[0]) self.countdown_progress_bar.setValue(cd_list[2]) if path == 'widget-countdown-day.ui': # 自定义倒计时 conf.update_countdown(self.cnt) self.custom_title.setText(f'距离 {conf.get_cd_text_custom()} 还有') self.custom_countdown.setText(conf.get_custom_countdown()) self.update() def get_weather_data(self): logger.info('获取天气数据') self.weather_thread = weatherReportThread() self.weather_thread.weather_signal.connect(self.update_weather_data) self.weather_thread.start() def detect_weather_code_changed(self): current_code = config_center.read_conf('Weather') if current_code != self.last_code: self.last_code = current_code self.get_weather_data() def toggle_weather_alert(self): if not hasattr(self, 'weather_alert_level') or not self.weather_alert_level: # logger.warning("未获取到天气预警等级") return if not hasattr(self, 'weather_alert_text') or not self.weather_alert_text.text(): # logger.warning("未获取到天气预警文本") return if self.showing_temperature: # 切换预警 self.weather_alert_animation.setStartValue(0.0) self.weather_alert_animation.setEndValue(1.0) self.alert_icon_animation.setStartValue(0.0) self.alert_icon_animation.setEndValue(1.0) # 渐隐 self.weather_opacity = QGraphicsOpacityEffect(self.weather_icon) self.temperature_opacity = QGraphicsOpacityEffect(self.temperature) self.weather_icon.setGraphicsEffect(self.weather_opacity) self.temperature.setGraphicsEffect(self.temperature_opacity) weather_fade_out = QPropertyAnimation(self.weather_opacity, b'opacity') temp_fade_out = QPropertyAnimation(self.temperature_opacity, b'opacity') weather_fade_out.setDuration(700) temp_fade_out.setDuration(700) weather_fade_out.setEasingCurve(QEasingCurve.Type.OutCubic) temp_fade_out.setEasingCurve(QEasingCurve.Type.OutCubic) weather_fade_out.setStartValue(1.0) weather_fade_out.setEndValue(0.0) temp_fade_out.setStartValue(1.0) temp_fade_out.setEndValue(0.0) # 重置不透明度 self.fade_out_group = QParallelAnimationGroup(self) self.fade_out_group.addAnimation(weather_fade_out) self.fade_out_group.addAnimation(temp_fade_out) if not hasattr(self, 'weather_alert_opacity') or not self.weather_alert_opacity: self.weather_alert_opacity = QGraphicsOpacityEffect(self.weather_alert_text) self.weather_alert_text.setGraphicsEffect(self.weather_alert_opacity) if not hasattr(self, 'alert_icon_opacity') or not self.alert_icon_opacity: self.alert_icon_opacity = QGraphicsOpacityEffect(self.alert_icon) self.alert_icon.setGraphicsEffect(self.alert_icon_opacity) alert_text_fade_in = QPropertyAnimation(self.weather_alert_opacity, b'opacity') alert_icon_fade_in = QPropertyAnimation(self.alert_icon_opacity, b'opacity') alert_text_fade_in.setDuration(700) alert_icon_fade_in.setDuration(700) alert_text_fade_in.setEasingCurve(QEasingCurve.Type.OutCubic) alert_icon_fade_in.setEasingCurve(QEasingCurve.Type.OutCubic) alert_text_fade_in.setStartValue(0.0) alert_text_fade_in.setEndValue(1.0) alert_icon_fade_in.setStartValue(0.0) alert_icon_fade_in.setEndValue(1.0) self.fade_in_group = QParallelAnimationGroup(self) self.fade_in_group.addAnimation(alert_text_fade_in) self.fade_in_group.addAnimation(alert_icon_fade_in) try: self.fade_out_group.finished.disconnect() except TypeError: pass def _start_alert_fade_in(): if hasattr(self, 'alert_icon') and isinstance(self.alert_icon, IconWidget) and self.alert_icon.icon() is not None and not self.alert_icon.icon().isNull(): self.weather_icon.hide() self.temperature.hide() self.weather_alert_opacity.setOpacity(0.0) self.alert_icon_opacity.setOpacity(0.0) self.weather_alert_text.show() self.alert_icon.show() self.fade_in_group.start() self.weather_info_timer.start(3000) else: self.weather_icon.show() self.temperature.show() if hasattr(self, 'weather_opacity'): self.weather_opacity.setOpacity(1.0) if hasattr(self, 'temperature_opacity'): self.temperature_opacity.setOpacity(1.0) self.showing_temperature = True self.fade_out_group.finished.connect(_start_alert_fade_in) self.fade_out_group.start() else: # 切换到气温 self.weather_alert_animation.setStartValue(1.0) self.weather_alert_animation.setEndValue(0.0) self.alert_icon_animation.setStartValue(1.0) self.alert_icon_animation.setEndValue(0.0) if not hasattr(self, 'weather_alert_opacity') or not self.weather_alert_opacity: self.weather_alert_opacity = QGraphicsOpacityEffect(self.weather_alert_text) self.weather_alert_text.setGraphicsEffect(self.weather_alert_opacity) if not hasattr(self, 'alert_icon_opacity') or not self.alert_icon_opacity: self.alert_icon_opacity = QGraphicsOpacityEffect(self.alert_icon) self.alert_icon.setGraphicsEffect(self.alert_icon_opacity) alert_text_fade_out = QPropertyAnimation(self.weather_alert_opacity, b'opacity') alert_icon_fade_out = QPropertyAnimation(self.alert_icon_opacity, b'opacity') alert_text_fade_out.setDuration(500) alert_icon_fade_out.setDuration(500) alert_text_fade_out.setEasingCurve(QEasingCurve.Type.OutCubic) alert_icon_fade_out.setEasingCurve(QEasingCurve.Type.OutCubic) alert_text_fade_out.setStartValue(1.0) alert_text_fade_out.setEndValue(0.0) alert_icon_fade_out.setStartValue(1.0) alert_icon_fade_out.setEndValue(0.0) self.fade_out_group = QParallelAnimationGroup(self) self.fade_out_group.addAnimation(alert_text_fade_out) self.fade_out_group.addAnimation(alert_icon_fade_out) if not hasattr(self, 'weather_opacity') or not self.weather_opacity: self.weather_opacity = QGraphicsOpacityEffect(self.weather_icon) self.weather_icon.setGraphicsEffect(self.weather_opacity) if not hasattr(self, 'temperature_opacity') or not self.temperature_opacity: self.temperature_opacity = QGraphicsOpacityEffect(self.temperature) self.temperature.setGraphicsEffect(self.temperature_opacity) weather_fade_in = QPropertyAnimation(self.weather_opacity, b'opacity') temp_fade_in = QPropertyAnimation(self.temperature_opacity, b'opacity') weather_fade_in.setDuration(500) temp_fade_in.setDuration(500) weather_fade_in.setEasingCurve(QEasingCurve.Type.OutCubic) temp_fade_in.setEasingCurve(QEasingCurve.Type.OutCubic) weather_fade_in.setStartValue(0.0) weather_fade_in.setEndValue(1.0) temp_fade_in.setStartValue(0.0) temp_fade_in.setEndValue(1.0) self.fade_in_group = QParallelAnimationGroup(self) self.fade_in_group.addAnimation(weather_fade_in) self.fade_in_group.addAnimation(temp_fade_in) try: self.fade_out_group.finished.disconnect() except TypeError: pass def _start_temperature_fade_in(): self.weather_alert_text.hide() self.alert_icon.hide() self.weather_opacity.setOpacity(0.0) self.temperature_opacity.setOpacity(0.0) self.weather_icon.show() self.temperature.show() self.fade_in_group.start() # 连接淡出组完成信号 self.fade_out_group.finished.connect(_start_temperature_fade_in) self.fade_out_group.start() self.showing_temperature = not self.showing_temperature def detect_theme_changed(self): theme_ = config_center.read_conf('General', 'theme') color_mode = config_center.read_conf('General', 'color_mode') widgets = list_.get_widget_config() if theme_ != self.last_theme or color_mode != self.last_color_mode or widgets != self.last_widgets: self.last_theme = theme_ self.last_color_mode = color_mode self.last_widgets = widgets logger.info(f'切换主题:{theme_},颜色模式{color_mode}') mgr.clear_widgets() def update_weather_data(self, weather_data): # 更新天气数据(已兼容多api) global weather_name, temperature, weather_data_temp if type(weather_data) is dict and hasattr(self, 'weather_icon') and 'error' not in weather_data: logger.success('已获取天气数据') alert_data = weather_data.get('alert') weather_data = weather_data.get('now') weather_data_temp = weather_data weather_name = db.get_weather_by_code(db.get_weather_data('icon', weather_data)) current_city = self.findChild(QLabel, 'current_city') try: # 天气组件 self.weather_icon.setPixmap( QPixmap(db.get_weather_icon_by_code(db.get_weather_data('icon', weather_data))) ) self.alert_icon.hide() if db.is_supported_alert(): alert_type = db.get_weather_data('alert', alert_data if alert_data else weather_data) if alert_type: self.alert_icon.setIcon( db.get_alert_image(alert_type) ) self.alert_icon.hide() try: alert_title = db.get_weather_data('alert_title', alert_data if alert_data else weather_data) if alert_title: alert_type_match = re.search(r'发布(\w+)(蓝|黄|橙|红)色预警', alert_title) if alert_type_match: alert_type = alert_type_match.group(1) # 类型 logger.success(f'天气预警: {alert_title} --> {alert_type}预警') alert_text = alert_type + '预警' else: logger.success(f'天气预警: {alert_title} --> {alert_title}') alert_text = alert_title self.weather_alert_text.setFixedWidth(80) self.weather_alert_text.setFixedHeight(40) # 调整字体大小 font = self.weather_alert_text.font() if len(alert_text) <= 4: font.setPointSize(14) elif len(alert_text) <= 6: font.setPointSize(12) else: font.setPointSize(10) self.weather_alert_text.setFont(font) self.weather_alert_text.setText(alert_text) self.weather_alert_text.setAlignment(Qt.AlignCenter) if not self.weather_alert_timer: self.weather_alert_timer = QTimer(self) self.weather_alert_timer.timeout.connect(self.toggle_weather_alert) self.weather_alert_timer.start(6000) self.weather_info_timer = QTimer(self) self.weather_info_timer.timeout.connect(self.toggle_weather_alert) self.weather_info_timer.setSingleShot(True) except Exception as e: logger.warning(f'获取天气预警标题失败:{e}') self.weather_alert_text.setText('暂无预警信息') self.temperature.setText(f"{db.get_weather_data('temp', weather_data)}") current_city.setText(f"{db.search_by_num(config_center.read_conf('Weather', 'city'))} · " f"{weather_name}") update_stylesheet = re.sub( r'border-image: url\((.*?)\);', f"border-image: url({db.get_weather_stylesheet(db.get_weather_data('icon', weather_data))});", self.backgnd.styleSheet() ) self.backgnd.setStyleSheet(update_stylesheet) except Exception as e: logger.error(f'天气组件出错:{e}') else: logger.error(f'获取天气数据出错:{weather_data}') try: if hasattr(self, 'weather_icon'): self.weather_icon.setPixmap(QPixmap(f'{base_directory}/img/weather/99.svg')) self.alert_icon.hide() self.weather_alert_text.hide() self.temperature.setText('--°') current_city = self.findChild(QLabel, 'current_city') if current_city: current_city.setText(f"{db.search_by_num(config_center.read_conf('Weather', 'city'))} · 未知") if hasattr(self, 'backgnd'): update_stylesheet = re.sub( r'border-image: url\((.*?)\);', f"border-image: url({db.get_weather_stylesheet('99')});", self.backgnd.styleSheet() ) self.backgnd.setStyleSheet(update_stylesheet) except Exception as e: logger.error(f'天气图标设置失败:{e}') def open_extra_menu(self): global ex_menu if ex_menu is None or not ex_menu.isVisible(): ex_menu = ExtraMenu() ex_menu.show() ex_menu.destroyed.connect(self.cleanup_extra_menu) logger.info('打开“额外选项”') else: ex_menu.raise_() ex_menu.activateWindow() @staticmethod def cleanup_extra_menu(): global ex_menu ex_menu = None @staticmethod def hide_show_widgets(): # 隐藏/显示主界面(全部隐藏) hide_mode = config_center.read_conf('General', 'hide') if hide_mode == '1' or hide_mode == '2': hide_mode_text = "上课时自动隐藏" if hide_mode == '1' else "窗口最大化时隐藏" w = Dialog( "暂时无法变更“状态”", f"您正在使用 {hide_mode_text} 模式,无法变更隐藏状态\n" "若变更状态,将修改隐藏模式“灵活隐藏” (您稍后可以在“设置”中更改此选项)\n" "您确定要隐藏组件吗?", None ) w.yesButton.setText("确定") w.yesButton.clicked.connect(lambda: config_center.write_conf('General', 'hide', '3')) w.cancelButton.setText("取消") w.buttonLayout.insertStretch(1) w.setFixedWidth(550) if w.exec(): if mgr.state: mgr.full_hide_windows() else: mgr.show_windows() else: if mgr.state: mgr.full_hide_windows() else: mgr.show_windows() @staticmethod def minimize_to_floating(): # 最小化到浮窗 hide_mode = config_center.read_conf('General', 'hide') if hide_mode == '1' or hide_mode == '2': hide_mode_text = "上课时自动隐藏" if hide_mode == '1' else "窗口最大化时隐藏" w = Dialog( "暂时无法变更“状态”", f"您正在使用 {hide_mode_text} 模式,无法变更隐藏状态\n" "若变更状态,将修改隐藏模式“灵活隐藏” (您可以在“设置”中更改此选项)\n" "您确定要隐藏组件吗?", None ) w.yesButton.setText("确定") w.yesButton.clicked.connect(lambda: config_center.write_conf('General', 'hide', '3')) w.cancelButton.setText("取消") w.buttonLayout.insertStretch(1) w.setFixedWidth(550) if w.exec(): if mgr.state: fw.show() mgr.full_hide_windows() else: mgr.show_windows() else: if mgr.state: fw.show() mgr.full_hide_windows() else: mgr.show_windows() def clear_animation(self): # 清除动画 self.animation = None def animate_window(self, target_pos): # **初次**启动动画 # 创建位置动画 self.animation = QPropertyAnimation(self, b"geometry") self.animation.setDuration(300) # 持续时间 if os.name == 'nt': self.animation.setStartValue(QRect(target_pos[0], -self.height(), self.w, self.h)) else: self.animation.setStartValue(QRect(target_pos[0], 0, self.w, self.h)) self.animation.setEndValue(QRect(target_pos[0], target_pos[1], self.w, self.h)) self.animation.setEasingCurve(QEasingCurve.Type.InOutCirc) # 设置动画效果 self.animation.start() self.animation.finished.connect(self.clear_animation) def animate_hide(self, full=False): # 隐藏窗口 self.animation = QPropertyAnimation(self, b"geometry") self.animation.setDuration(625) # 持续时间 height = self.height() self.setFixedHeight(height) # 防止连续打断窗口高度变小 if full and os.name == 'nt': '''全隐藏 windows''' self.animation.setEndValue(QRect(self.x(), -height, self.width(), self.height())) elif os.name == 'nt': '''半隐藏 windows''' self.animation.setEndValue(QRect(self.x(), -height + 40, self.width(), self.height())) else: '''其他系统''' self.animation.setEndValue(QRect(self.x(), 0, self.width(), self.height())) self.animation.finished.connect(lambda: self.hide()) self.animation.setEasingCurve(QEasingCurve.Type.OutExpo) # 设置动画效果 self.animation.start() self.animation.finished.connect(self.clear_animation) def animate_hide_opacity(self): # 隐藏窗口透明度 self.animation = QPropertyAnimation(self, b"windowOpacity") self.animation.setDuration(300) # 持续时间 self.animation.setStartValue(int(config_center.read_conf('General', 'opacity')) / 100) self.animation.setEndValue(0) self.animation.setEasingCurve(QEasingCurve.Type.InOutCirc) # 设置动画效果 self.animation.start() self.animation.finished.connect(self.close) def animate_show_opacity(self): # 显示窗口透明度 self.animation = QPropertyAnimation(self, b"windowOpacity") self.animation.setDuration(350) # 持续时间 self.animation.setStartValue(0) self.animation.setEndValue(int(config_center.read_conf('General', 'opacity')) / 100) self.animation.setEasingCurve(QEasingCurve.Type.InOutCirc) # 设置动画效果 self.animation.start() self.animation.finished.connect(self.clear_animation) def animate_show(self): # 显示窗口 self.animation = QPropertyAnimation(self, b"geometry") self.animation.setDuration(525) # 持续时间 # 获取当前窗口的宽度和高度,确保动画过程中保持一致 self.animation.setEndValue( QRect(self.x(), int(config_center.read_conf('General', 'margin')), self.width(), self.height())) self.animation.setEasingCurve(QEasingCurve.Type.InOutCirc) # 设置动画效果 self.animation.finished.connect(self.clear_animation) if os.name != 'nt': self.show() self.animation.start() def widget_transition(self, pos_x, width, height, opacity=1): # 窗口形变 self.animation = QPropertyAnimation(self, b"geometry") self.animation.setDuration(525) # 持续时间 self.animation.setStartValue(QRect(self.x(), self.y(), self.width(), self.height())) self.animation.setEndValue(QRect(pos_x, self.y(), width, height)) self.animation.setEasingCurve(QEasingCurve.Type.OutCubic) # 设置动画效果 self.animation.start() self.opacity_animation = QPropertyAnimation(self, b"windowOpacity") self.opacity_animation.setDuration(525) # 持续时间 self.opacity_animation.setStartValue(self.windowOpacity()) self.opacity_animation.setEndValue(opacity) self.opacity_animation.setEasingCurve(QEasingCurve.Type.InOutCirc) # 设置动画效果 self.opacity_animation.start() self.animation.finished.connect(self.clear_animation) # 点击自动隐藏 def mouseReleaseEvent(self, event): if event.button() == Qt.MouseButton.RightButton: return # 右键不执行 if config_center.read_conf('General', 'pin_on_top') == '2': # 置底 return # 置底不执行 if config_center.read_conf('General', 'hide') == '0': # 置顶 if mgr.state: mgr.decide_to_hide() else: mgr.show_windows() elif config_center.read_conf('General', 'hide') == '3': # 隐藏 if mgr.state: mgr.decide_to_hide() mgr.hide_status = (current_state, 1) else: mgr.show_windows() mgr.hide_status = (current_state, 0) else: event.ignore() def stop(self): if mgr: mgr.cleanup_resources() for widget in self.widgets: widget.stop() if self.animation: self.animation.stop() if self.opacity_animation: self.opacity_animation.stop() self.close() def check_windows_maximize(): # 检查窗口是否最大化 if os.name != 'nt' or not pygetwindow: # logger.debug("非Windows NT系统或pygetwindow未加载, 无法检查最大化.") return False # 需要排除的特定窗口标题 (全字匹配, 大小写不敏感) excluded_titles_exact_lower = { 'residentsidebar', # 希沃侧边栏 'program manager', # Windows桌面 'desktop', # Windows桌面 (备用) 'snippingtool', # 系统截图工具 # '' 空标题不再默认排除 } # 需要排除的标题中包含的关键词 (大小写不敏感) excluded_keywords_in_title_lower = { 'overlay', 'snipping', 'sidebar', 'flyout' # qfluentwidgets的浮出控件 } # 需要排除的进程名 (全字或部分匹配, 大小写不敏感) excluded_process_names_lower = { 'shellexperiencehost.exe', 'searchui.exe', 'startmenuexperiencehost.exe', 'applicationframehost.exe', 'systemsettings.exe', 'taskmgr.exe' } # 用户自定义的忽略进程列表 (全字匹配, 大小写不敏感) # 例:easinote.exe 每行一个,用逗号分隔 ignored_process_names_for_maximize_lower = { 'easinote.exe' } current_pid = os.getpid() try: all_windows = pygetwindow.getAllWindows() except Exception as e: logger.warning(f"获取窗口列表时发生错误 (pygetwindow): {str(e)}") # logger.debug("获取窗口列表失败.") return False for window in all_windows: try: if not window._hWnd: # logger.debug(f"窗口 '{getattr(window, 'title', 'N/A')}' 无效句柄, 跳过.") continue if not window.visible: # logger.debug(f"窗口 '{window.title}' 不可见, 跳过.") continue if not window.isMaximized: # logger.debug(f"窗口 '{window.title}' 未最大化, 跳过.") continue # logger.debug(f"发现可见且已最大化的窗口: '{window.title}' (句柄: {window._hWnd})") try: hwnd_int = window._hWnd pid_val = ctypes.c_ulong() ctypes.windll.user32.GetWindowThreadProcessId(hwnd_int, ctypes.byref(pid_val)) win_pid = pid_val.value if win_pid == 0: continue # 无效PID process_name = psutil.Process(win_pid).name().lower() except (psutil.NoSuchProcess, psutil.AccessDenied, AttributeError, ValueError, OSError) : # logger.debug(f"无法获取窗口 '{title}' 的进程信息,跳过.") continue if win_pid == current_pid: # logger.debug(f"窗口 '{title}' (PID: {win_pid}, 进程: {process_name}) 是自身进程, 排除.") continue title = window.title.strip() title_lower = title.lower() if process_name in ignored_process_names_for_maximize_lower: # logger.debug(f"窗口 '{title}' (进程: {process_name}) 在忽略列表, 排除.") continue if process_name in excluded_process_names_lower: # logger.debug(f"窗口 '{title}' (进程: {process_name}) 在排除的进程名列表, 排除.") continue if title_lower in excluded_titles_exact_lower: # logger.debug(f"窗口标题 '{title_lower}' 在排除列表, 排除.") continue if any(keyword in title_lower for keyword in excluded_keywords_in_title_lower): # logger.debug(f"窗口标题 '{title_lower}' 包含排除的关键词, 排除.") continue # 如果进程是 explorer.exe,但不是“资源管理器”则认为是特殊explorer(应该是桌面) if process_name == 'explorer.exe': if title_lower in excluded_titles_exact_lower or \ any(keyword in title_lower for keyword in excluded_keywords_in_title_lower): # logger.debug(f"explorer.exe 窗口 '{title_lower}' 命中标题排除规则, 排除.") continue # logger.debug(f"找到有效最大化窗口: '{title}' (PID: {win_pid}, 进程: {process_name}). 返回 True.") return True except Exception as e: if window and hasattr(window, 'title'): logger.debug(f"处理窗口 '{getattr(window, 'title', 'N/A')}' 时发生错误: {str(e)}") else: logger.debug(f"处理一个未知窗口时发生错误: {str(e)}") continue return False def init_config(): # 重设配置文件 config_center.write_conf('Temp', 'set_week', '') config_center.write_conf('Temp', 'set_schedule', '') if config_center.read_conf('Temp', 'temp_schedule') != '': # 修复换课重置 copy(f'{base_directory}/config/schedule/backup.json', f'{base_directory}/config/schedule/{config_center.schedule_name}') config_center.write_conf('Temp', 'temp_schedule', '') schedule_center.update_schedule() def init(): global theme, radius, mgr, screen_width, first_start, fw, was_floating_mode update_timer.remove_all_callbacks() theme = config_center.read_conf('General', 'theme') # 主题 if not os.path.exists(f'{base_directory}/ui/{theme}/theme.json'): logger.warning(f'主题 {theme} 不存在,使用默认主题') theme = 'default' logger.info(f'应用主题:{theme}') mgr = WidgetsManager() fw = FloatingWidget() # 获取屏幕横向分辨率 screen_geometry = app.primaryScreen().availableGeometry() screen_width = screen_geometry.width() widgets = list_.get_widget_config() for widget in widgets: # 检查组件 if widget not in list_.widget_name: widgets.remove(widget) # 移除不存在的组件(确保移除插件后不会出错) mgr.init_widgets() if not first_start and was_floating_mode: if fw: fw.show() mgr.full_hide_windows() update_timer.add_callback(mgr.update_widgets) update_timer.start() logger.info(f'Class Widgets 初始化完成。版本: {config_center.read_conf("Other", "version")}') p_loader.run_plugins() # 运行插件 first_start = False def setup_signal_handlers_optimized(app): """退出信号处理器""" def signal_handler(signum, frame): logger.debug(f'收到信号 {signal.Signals(signum).name},退出...') # utils.stop 处理退出 utils.stop(0) signal.signal(signal.SIGTERM, signal_handler) # taskkill signal.signal(signal.SIGINT, signal_handler) # Ctrl+C if os.name == 'posix': signal.signal(signal.SIGQUIT, signal_handler) # 终端退出 signal.signal(signal.SIGHUP, signal_handler) # 终端挂起 if __name__ == '__main__': if share.attach() and config_center.read_conf('Other', 'multiple_programs') != '1': logger.debug('不允许多开实例') from qfluentwidgets import Dialog app = QApplication.instance() or QApplication(sys.argv) dlg = Dialog( 'Class Widgets 正在运行', 'Class Widgets 正在运行!请勿打开多个实例,否则将会出现不可预知的问题。' '\n(若您需要打开多个实例,请在“设置”->“高级选项”中启用“允许程序多开”)' ) dlg.yesButton.setText('好') dlg.cancelButton.hide() dlg.buttonLayout.insertStretch(0, 1) dlg.setFixedWidth(550) dlg.exec() sys.exit(0) if not share.create(1): print(f'无法创建共享内存: {share.errorString()}') # logger 可能还没准备好 sys.exit(1) scale_factor = float(config_center.read_conf('General', 'scale')) os.environ['QT_SCALE_FACTOR'] = str(scale_factor) logger.info(f"当前缩放系数:{scale_factor * 100}%") app = QApplication(sys.argv) app.setQuitOnLastWindowClosed(False) share.create(1) # 创建共享内存 logger.info( f"共享内存:{share.isAttached()} 是否允许多开实例:{config_center.read_conf('Other', 'multiple_programs')}") try: dark_mode_watcher = DarkModeWatcher(parent=app) dark_mode_watcher.darkModeChanged.connect(handle_dark_mode_change) # 连接信号 # 初始主题设置依赖于 darkModeChanged 信号 except Exception as e: logger.error(f"初始化颜色模式监测器时出错: {e}") dark_mode_watcher = None if scale_factor > 1.8 or scale_factor < 1.0: logger.warning("当前缩放系数可能导致显示异常,建议使缩放系数在 100% 到 180% 之间") msg_box = Dialog('缩放系数过大', f"当前缩放系数为 {scale_factor * 100}%,可能导致显示异常。\n建议将缩放系数设置为 100% 到 180% 之间。") msg_box.yesButton.setText('好') msg_box.cancelButton.hide() msg_box.buttonLayout.insertStretch(0, 1) msg_box.setFixedWidth(550) msg_box.exec() # 优化操作系统和版本输出 system = platform.system() if system == 'Darwin': system = 'macOS' osRelease = platform.release() if system == 'Windows': osRelease = 'Windows ' + osRelease if system == 'macOS': osRelease = 'Darwin Kernel Version ' + osRelease osVersion = platform.version() if system == 'macOS': osVersion = 'macOS ' + platform.mac_ver()[0] logger.info(f"操作系统:{system},版本:{osRelease}/{osVersion}") # list_pyttsx3_voices() if share.attach() and config_center.read_conf('Other', 'multiple_programs') != '1': msg_box = Dialog( 'Class Widgets 正在运行', 'Class Widgets 正在运行!请勿打开多个实例,否则将会出现不可预知的问题。' '\n(若您需要打开多个实例,请在“设置”->“高级选项”中启用“允许程序多开”)' ) msg_box.yesButton.setText('好') msg_box.cancelButton.hide() msg_box.buttonLayout.insertStretch(0, 1) msg_box.setFixedWidth(550) msg_box.exec() stop(-1) else: mgr = WidgetsManager() app.aboutToQuit.connect(mgr.cleanup_resources) setup_signal_handlers_optimized(app) if config_center.read_conf('Other', 'initialstartup') == '1': # 首次启动 try: conf.add_shortcut('ClassWidgets.exe', f'{base_directory}/img/favicon.ico') conf.add_shortcut_to_startmenu(f'{base_directory}/ClassWidgets.exe', f'{base_directory}/img/favicon.ico') config_center.write_conf('Other', 'initialstartup', '') except Exception as e: logger.error(f'添加快捷方式失败:{e}') try: list_.create_new_profile('新课表 - 1.json') except Exception as e: logger.error(f'创建新课表失败:{e}') p_mgr = PluginManager() p_loader.set_manager(p_mgr) p_loader.load_plugins() init() get_start_time() get_current_lessons() get_current_lesson_name() get_next_lessons() # 如果在全屏或最大化模式下启动,首先折叠主组件后显示浮动窗口动画。 if check_windows_maximize() or check_fullscreen(): mgr.decide_to_hide() # 折叠动画,其实这里可用`mgr.full_hide_windows()`但是播放动画似乎更好() if current_state == 1: setThemeColor(f"#{config_center.read_conf('Color', 'attend_class')}") else: setThemeColor(f"#{config_center.read_conf('Color', 'finish_class')}") # w = ErrorDialog() # w.exec() if config_center.read_conf('Other', 'auto_check_update') == '1': check_update() status = app.exec() utils.stop(status)