2857 lines
123 KiB
Python
2857 lines
123 KiB
Python
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)
|