461 lines
18 KiB
Python
Executable File
461 lines
18 KiB
Python
Executable File
import sys
|
|
|
|
import os
|
|
from PyQt5 import uic
|
|
from PyQt5.QtCore import Qt, QPropertyAnimation, QRect, QEasingCurve, QTimer, QPoint, pyqtProperty, QThread
|
|
from PyQt5.QtGui import QColor, QPainter, QBrush, QPixmap
|
|
from PyQt5.QtWidgets import QWidget, QApplication, QLabel, QFrame, QGraphicsBlurEffect
|
|
from loguru import logger
|
|
from qfluentwidgets import setThemeColor
|
|
|
|
import conf
|
|
from conf import base_directory
|
|
import list_
|
|
from file import config_center
|
|
from play_audio import PlayAudio
|
|
import platform
|
|
|
|
# 适配高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标识')
|
|
|
|
prepare_class = config_center.read_conf('Audio', 'prepare_class')
|
|
attend_class = config_center.read_conf('Audio', 'attend_class')
|
|
finish_class = config_center.read_conf('Audio', 'finish_class')
|
|
|
|
pushed_notification = False
|
|
notification_contents = {"state": None, "lesson_name": None, "title": None, "subtitle": None, "content": None}
|
|
|
|
# 波纹效果
|
|
normal_color = '#56CFD8'
|
|
|
|
window_list = [] # 窗口列表
|
|
active_windows = []
|
|
|
|
|
|
class tip_toast(QWidget):
|
|
def __init__(self, pos, width, state=1, lesson_name=None, title=None, subtitle=None, content=None, icon=None, duration=2000):
|
|
super().__init__()
|
|
for w in active_windows[:]:
|
|
w.close()
|
|
active_windows.append(self)
|
|
self.audio_thread = None
|
|
uic.loadUi(f"{base_directory}/view/widget-toast-bar.ui", self)
|
|
|
|
try:
|
|
dpr = self.screen().devicePixelRatio() if self.screen() else QApplication.primaryScreen().devicePixelRatio()
|
|
except AttributeError:
|
|
dpr = QApplication.primaryScreen().devicePixelRatio()
|
|
dpr = max(1.0, dpr)
|
|
|
|
# 窗口位置
|
|
if config_center.read_conf('Toast', 'pin_on_top') == '1':
|
|
self.setWindowFlags(
|
|
Qt.WindowType.WindowStaysOnTopHint | Qt.WindowType.FramelessWindowHint |
|
|
Qt.X11BypassWindowManagerHint # 绕过窗口管理器以在全屏显示通知
|
|
)
|
|
else:
|
|
self.setWindowFlags(
|
|
Qt.WindowType.WindowStaysOnBottomHint | Qt.WindowType.FramelessWindowHint
|
|
)
|
|
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
|
self.move(pos[0], pos[1])
|
|
self.resize(width, height)
|
|
|
|
# 标题
|
|
title_label = self.findChild(QLabel, 'title')
|
|
backgnd = self.findChild(QFrame, 'backgnd')
|
|
lesson = self.findChild(QLabel, 'lesson')
|
|
subtitle_label = self.findChild(QLabel, 'subtitle')
|
|
icon_label = self.findChild(QLabel, 'icon')
|
|
|
|
sound_to_play = None
|
|
if icon:
|
|
pixmap = QPixmap(icon)
|
|
icon_size = int(48 * dpr)
|
|
pixmap = pixmap.scaled(icon_size, icon_size, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
|
|
icon_label.setPixmap(pixmap)
|
|
icon_label.setFixedSize(icon_size, icon_size)
|
|
|
|
if state == 1:
|
|
logger.info('上课铃声显示')
|
|
title_label.setText('活动开始') # 修正文本,以适应不同场景
|
|
subtitle_label.setText('当前课程')
|
|
lesson.setText(lesson_name) # 课程名
|
|
sound_to_play = attend_class
|
|
setThemeColor(f"#{config_center.read_conf('Color', 'attend_class')}") # 主题色
|
|
elif state == 0:
|
|
logger.info('下课铃声显示')
|
|
title_label.setText('下课')
|
|
if lesson_name:
|
|
subtitle_label.setText('即将进行')
|
|
else:
|
|
subtitle_label.hide()
|
|
lesson.setText(lesson_name) # 课程名
|
|
sound_to_play = finish_class
|
|
setThemeColor(f"#{config_center.read_conf('Color', 'finish_class')}")
|
|
elif state == 2:
|
|
logger.info('放学铃声显示')
|
|
title_label.setText('放学')
|
|
subtitle_label.setText('当前课程已结束')
|
|
lesson.setText('') # 课程名
|
|
sound_to_play = finish_class
|
|
setThemeColor(f"#{config_center.read_conf('Color', 'finish_class')}")
|
|
elif state == 3:
|
|
logger.info('预备铃声显示')
|
|
title_label.setText('即将开始') # 同上
|
|
subtitle_label.setText('下一节')
|
|
lesson.setText(lesson_name)
|
|
sound_to_play = prepare_class
|
|
setThemeColor(f"#{config_center.read_conf('Color', 'prepare_class')}")
|
|
elif state == 4:
|
|
logger.info(f'通知显示: {title}')
|
|
title_label.setText(title)
|
|
subtitle_label.setText(subtitle)
|
|
lesson.setText(content)
|
|
sound_to_play = prepare_class
|
|
|
|
# 设置样式表
|
|
if state == 1: # 上课铃声
|
|
bg_color = [ # 1为正常、2为渐变亮色部分、3为渐变暗色部分
|
|
generate_gradient_color(attend_class_color)[0],
|
|
generate_gradient_color(attend_class_color)[1],
|
|
generate_gradient_color(attend_class_color)[2]
|
|
]
|
|
elif state == 0 or state == 2: # 下课铃声
|
|
bg_color = [
|
|
generate_gradient_color(finish_class_color)[0],
|
|
generate_gradient_color(finish_class_color)[1],
|
|
generate_gradient_color(finish_class_color)[2]
|
|
]
|
|
elif state == 3: # 预备铃声
|
|
bg_color = [
|
|
generate_gradient_color(prepare_class_color)[0],
|
|
generate_gradient_color(prepare_class_color)[1],
|
|
generate_gradient_color(prepare_class_color)[2]
|
|
]
|
|
elif state == 4: # 通知铃声
|
|
bg_color = ['rgba(110, 190, 210, 255)', 'rgba(110, 190, 210, 255)', 'rgba(90, 210, 215, 255)']
|
|
else:
|
|
bg_color = ['rgba(110, 190, 210, 255)', 'rgba(110, 190, 210, 255)', 'rgba(90, 210, 215, 255)']
|
|
|
|
backgnd.setStyleSheet(f'font-weight: bold; border-radius: {radius}; '
|
|
'background-color: qlineargradient('
|
|
'spread:pad, x1:0, y1:0, x2:1, y2:1,'
|
|
f' stop:0 {bg_color[1]}, stop:0.5 {bg_color[0]}, stop:1 {bg_color[2]}'
|
|
');'
|
|
)
|
|
|
|
# 模糊效果
|
|
self.blur_effect = QGraphicsBlurEffect(self)
|
|
if config_center.read_conf('Toast', 'wave') == '1':
|
|
backgnd.setGraphicsEffect(self.blur_effect)
|
|
|
|
mini_size_x = 150 / dpr
|
|
mini_size_y = 50 / dpr
|
|
|
|
self.timer = QTimer(self)
|
|
self.timer.setSingleShot(True)
|
|
self.timer.setInterval(duration)
|
|
self.timer.timeout.connect(self.close_window)
|
|
|
|
# 放大效果
|
|
self.geometry_animation = QPropertyAnimation(self, b"geometry")
|
|
self.geometry_animation.setDuration(750) # 动画持续时间
|
|
start_rect = QRect(int(start_x + mini_size_x / 2), int(start_y + mini_size_y / 2),
|
|
int(total_width - mini_size_x), int(height - mini_size_y))
|
|
self.geometry_animation.setStartValue(start_rect)
|
|
self.geometry_animation.setEndValue(QRect(start_x, start_y, total_width, height))
|
|
self.geometry_animation.setEasingCurve(QEasingCurve.Type.OutCirc)
|
|
self.geometry_animation.finished.connect(self.timer.start)
|
|
|
|
self.blur_animation = QPropertyAnimation(self.blur_effect, b"blurRadius")
|
|
self.blur_animation.setDuration(550)
|
|
self.blur_animation.setStartValue(25)
|
|
self.blur_animation.setEndValue(0)
|
|
|
|
# 渐显
|
|
self.opacity_animation = QPropertyAnimation(self, b"windowOpacity")
|
|
self.opacity_animation.setDuration(450)
|
|
self.opacity_animation.setStartValue(0)
|
|
self.opacity_animation.setEndValue(1)
|
|
self.opacity_animation.setEasingCurve(QEasingCurve.Type.InOutQuad)
|
|
|
|
if sound_to_play:
|
|
self.playsound(sound_to_play)
|
|
|
|
self.geometry_animation.start()
|
|
self.opacity_animation.start()
|
|
self.blur_animation.start()
|
|
|
|
def close_window(self):
|
|
try:
|
|
dpr = self.screen().devicePixelRatio() if self.screen() else QApplication.primaryScreen().devicePixelRatio()
|
|
except AttributeError:
|
|
dpr = QApplication.primaryScreen().devicePixelRatio()
|
|
dpr = max(1.0, dpr)
|
|
mini_size_x = 120 / dpr
|
|
mini_size_y = 20 / dpr
|
|
|
|
# 放大效果
|
|
self.geometry_animation_close = QPropertyAnimation(self, b"geometry")
|
|
self.geometry_animation_close.setDuration(500) # 动画持续时间
|
|
self.geometry_animation_close.setStartValue(QRect(start_x, start_y, total_width, height))
|
|
end_rect = QRect(int(start_x + mini_size_x / 2), int(start_y + mini_size_y / 2),
|
|
int(total_width - mini_size_x), int(height - mini_size_y))
|
|
self.geometry_animation_close.setEndValue(end_rect)
|
|
self.geometry_animation_close.setEasingCurve(QEasingCurve.Type.InOutQuad)
|
|
|
|
self.blur_animation_close = QPropertyAnimation(self.blur_effect, b"blurRadius")
|
|
self.blur_animation_close.setDuration(500)
|
|
self.blur_animation_close.setStartValue(0)
|
|
self.blur_animation_close.setEndValue(30)
|
|
|
|
self.opacity_animation_close = QPropertyAnimation(self, b"windowOpacity")
|
|
self.opacity_animation_close.setDuration(500)
|
|
self.opacity_animation_close.setStartValue(1)
|
|
self.opacity_animation_close.setEndValue(0)
|
|
|
|
self.geometry_animation_close.start()
|
|
self.opacity_animation_close.start()
|
|
self.blur_animation_close.start()
|
|
self.opacity_animation_close.finished.connect(self.close)
|
|
|
|
def closeEvent(self, event):
|
|
if self in active_windows:
|
|
active_windows.remove(self)
|
|
global window_list
|
|
# window_list.remove(self)
|
|
self.hide()
|
|
self.deleteLater()
|
|
event.ignore()
|
|
|
|
def playsound(self, filename):
|
|
try:
|
|
file_path = os.path.join(base_directory, 'audio', filename)
|
|
if self.audio_thread and self.audio_thread.isRunning():
|
|
self.audio_thread.quit()
|
|
self.audio_thread.wait()
|
|
self.audio_thread = PlayAudio(str(file_path))
|
|
self.audio_thread.start()
|
|
self.audio_thread.setPriority(QThread.Priority.HighestPriority) # 设置优先级
|
|
except Exception as e:
|
|
logger.error(f'播放音频文件失败:{e}')
|
|
|
|
|
|
class wave_Effect(QWidget):
|
|
def __init__(self, state=1):
|
|
super().__init__()
|
|
|
|
if config_center.read_conf('Toast', 'pin_on_top') == '1':
|
|
self.setWindowFlags(
|
|
Qt.WindowType.WindowStaysOnTopHint | Qt.WindowType.FramelessWindowHint |
|
|
Qt.X11BypassWindowManagerHint # 绕过窗口管理器以在全屏显示通知
|
|
)
|
|
else:
|
|
self.setWindowFlags(
|
|
Qt.WindowType.WindowStaysOnBottomHint | Qt.WindowType.FramelessWindowHint |
|
|
Qt.X11BypassWindowManagerHint # 绕过窗口管理器以在全屏显示通知
|
|
)
|
|
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
|
|
|
self._radius = 0
|
|
self.duration = 1200
|
|
|
|
if state == 1:
|
|
self.color = QColor(attend_class_color)
|
|
elif state == 0 or state == 2:
|
|
self.color = QColor(finish_class_color)
|
|
elif state == 3:
|
|
self.color = QColor(prepare_class_color)
|
|
elif state == 4:
|
|
self.color = QColor(normal_color)
|
|
else:
|
|
self.color = QColor(normal_color)
|
|
|
|
screen_geometry = QApplication.primaryScreen().geometry()
|
|
self.setGeometry(screen_geometry)
|
|
|
|
self.timer = QTimer(self)
|
|
self.timer.setSingleShot(True)
|
|
self.timer.setInterval(275)
|
|
self.timer.timeout.connect(self.showAnimation)
|
|
self.timer.start()
|
|
|
|
@pyqtProperty(int)
|
|
def radius(self):
|
|
return self._radius
|
|
|
|
@radius.setter
|
|
def radius(self, value):
|
|
self._radius = value
|
|
self.update()
|
|
|
|
def showAnimation(self):
|
|
self.animation = QPropertyAnimation(self, b'radius')
|
|
self.animation.setDuration(self.duration)
|
|
self.animation.setStartValue(50)
|
|
try:
|
|
dpr = self.screen().devicePixelRatio() if self.screen() else QApplication.primaryScreen().devicePixelRatio()
|
|
except AttributeError:
|
|
dpr = QApplication.primaryScreen().devicePixelRatio()
|
|
dpr = max(1.0, dpr)
|
|
fixed_end_radius = 1000 * dpr # 动画效果值
|
|
self.animation.setEndValue(fixed_end_radius)
|
|
self.animation.setEasingCurve(QEasingCurve.Type.InOutQuad)
|
|
self.animation.start()
|
|
|
|
self.fade_animation = QPropertyAnimation(self, b'windowOpacity')
|
|
self.fade_animation.setDuration(self.duration - 150)
|
|
|
|
self.fade_animation.setKeyValues([ # 关键帧
|
|
(0, 0),
|
|
(0.06, 0.9),
|
|
(1, 0)
|
|
])
|
|
|
|
self.fade_animation.setEasingCurve(QEasingCurve.Type.InOutQuad)
|
|
self.fade_animation.finished.connect(self.close)
|
|
self.fade_animation.start()
|
|
|
|
def paintEvent(self, event):
|
|
painter = QPainter(self)
|
|
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
painter.setBrush(QBrush(self.color))
|
|
painter.setPen(Qt.PenStyle.NoPen)
|
|
center = self.rect().center()
|
|
loc = QPoint(center.x(), self.rect().top() + start_y + 50)
|
|
painter.drawEllipse(loc, self._radius, self._radius)
|
|
|
|
def closeEvent(self, event):
|
|
if self in active_windows:
|
|
active_windows.remove(self)
|
|
global window_list
|
|
# window_list.remove(self)
|
|
self.deleteLater()
|
|
self.hide()
|
|
event.ignore()
|
|
|
|
|
|
def generate_gradient_color(theme_color): # 计算渐变色
|
|
def adjust_color(color, factor):
|
|
r = max(0, min(255, int(color.red() * (1 + factor))))
|
|
g = max(0, min(255, int(color.green() * (1 + factor))))
|
|
b = max(0, min(255, int(color.blue() * (1 + factor))))
|
|
# return QColor(r, g, b)
|
|
return f'rgba({r}, {g}, {b}, 255)'
|
|
|
|
color = QColor(theme_color)
|
|
gradient = [adjust_color(color, 0), adjust_color(color, 0.24), adjust_color(color, -0.11)]
|
|
return gradient
|
|
|
|
|
|
def main(state=1, lesson_name='', title='通知示例', subtitle='副标题',
|
|
content='这是一条通知示例', icon=None, duration=2000): # 0:下课铃声 1:上课铃声 2:放学铃声 3:预备铃 4:其他
|
|
if detect_enable_toast(state):
|
|
return
|
|
|
|
global start_x, start_y, total_width, height, radius, attend_class_color, finish_class_color, prepare_class_color
|
|
|
|
widgets = list_.get_widget_config()
|
|
for widget in widgets: # 检查组件
|
|
if widget not in list_.widget_name:
|
|
widgets.remove(widget) # 移除不存在的组件(确保移除插件后不会出错)
|
|
|
|
attend_class_color = f"#{config_center.read_conf('Color', 'attend_class')}"
|
|
finish_class_color = f"#{config_center.read_conf('Color', 'finish_class')}"
|
|
prepare_class_color = f"#{config_center.read_conf('Color', 'prepare_class')}"
|
|
|
|
theme = config_center.read_conf('General', 'theme')
|
|
height = conf.load_theme_config(theme)['height']
|
|
radius = conf.load_theme_config(theme)['radius']
|
|
|
|
screen_geometry = QApplication.primaryScreen().geometry()
|
|
screen_width = screen_geometry.width()
|
|
spacing = conf.load_theme_config(theme)['spacing']
|
|
try:
|
|
dpr = QApplication.primaryScreen().devicePixelRatio()
|
|
except AttributeError:
|
|
dpr = 1.0
|
|
dpr = max(1.0, dpr)
|
|
|
|
widgets_width = 0
|
|
for widget in widgets: # 计算总宽度(兼容插件)
|
|
try:
|
|
widgets_width += conf.load_theme_width(theme)[widget]
|
|
except KeyError:
|
|
widgets_width += list_.widget_width[widget]
|
|
except:
|
|
widgets_width += 0
|
|
|
|
total_width = widgets_width + spacing * (len(widgets) - 1)
|
|
|
|
start_x = int((screen_width - total_width) / 2)
|
|
margin_base = int(config_center.read_conf('General', 'margin'))
|
|
start_y = int(margin_base * dpr)
|
|
|
|
if state != 4:
|
|
window = tip_toast((start_x, start_y), total_width, state, lesson_name, duration=duration)
|
|
else:
|
|
window = tip_toast(
|
|
(start_x, start_y),
|
|
total_width, state,
|
|
'',
|
|
title,
|
|
subtitle,
|
|
content,
|
|
icon,
|
|
duration=duration
|
|
)
|
|
|
|
window.show()
|
|
window_list.append(window)
|
|
|
|
if config_center.read_conf('Toast', 'wave') == '1':
|
|
wave = wave_Effect(state)
|
|
wave.show()
|
|
window_list.append(wave)
|
|
|
|
|
|
def detect_enable_toast(state=0):
|
|
if config_center.read_conf('Toast', 'attend_class') != '1' and state == 1:
|
|
return True
|
|
if (config_center.read_conf('Toast', 'finish_class') != '1') and (state in [0, 2]):
|
|
return True
|
|
if config_center.read_conf('Toast', 'prepare_class') != '1' and state == 3:
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
|
|
def push_notification(state=1, lesson_name='', title=None, subtitle=None,
|
|
content=None): # 推送通知
|
|
global pushed_notification, notification_contents
|
|
pushed_notification = True
|
|
notification_contents = {
|
|
"state": state,
|
|
"lesson_name": lesson_name,
|
|
"title": title,
|
|
"subtitle": subtitle,
|
|
"content": content
|
|
}
|
|
main(state, lesson_name, title, subtitle, content)
|
|
return notification_contents
|
|
|
|
|
|
if __name__ == '__main__':
|
|
app = QApplication(sys.argv)
|
|
main(
|
|
state=4, # 自定义通知
|
|
title='天气预报',
|
|
subtitle='',
|
|
content='1°~-3° | 3°~-3° | 9°~1°',
|
|
icon='img/favicon.ico',
|
|
duration=2000
|
|
)
|
|
sys.exit(app.exec())
|