.
diff --git a/README.md b/README.md
index 4272560..e9ea11f 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,147 @@
-# Class-Widgets
+> [!Warning]
+> Class Widgets *1* 目前完全由社区开发者进行开发
+>
+> [](https://github.com/pizeroLOL) [](https://github.com/IsHPDuwu) [](https://github.com/baiyao105) [](https://github.com/Artist-MOBAI)
+>
+> 有任何需要社区开发者帮忙的地方,请前往 QQ 群或提 issue
-Class Widgets
\ No newline at end of file
+> [!NOTE]
+> Class Widgets 有 QQ 群和 Discord 服务器啦!详见[此处](#社区)
+
+
+
+
+
+ Class Widgets
+
+
+ 全新桌面课表
+
+
+
+
+[](https://github.com/Class-Widgets/Class-Widgets)
+[](https://github.com/Class-Widgets/Class-Widgets/releases/latest)
+[](https://github.com/Class-Widgets/Class-Widgets/releases)
+[](https://github.com/Class-Widgets/Class-Widgets?tab=GPL-3.0-1-ov-file)
+[](https://github.com/Class-Widgets/Class-Widgets)
+
+
+
+
+
+
+
+
+[](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=yHXKCAjOxlpTpJ4mNdXm0mxOneYUinRs&authKey=sd3%2F06iGdOZUjkXXPBeIzGnFDIeYwmdwuM8dhk25fi%2B1CUL32MkeN2EEfjdo2pzE&noverify=0&group_code=169200380)
+[](https://discord.gg/EFF4PpqpqZ)
+
+#### [了解更多 >](https://www.bilibili.com/video/BV1xwW9eyEGu/)
+
+
+
+
+## 特性
+- 由 Python 编写的**插件**系统和插件广场(详见最新构建)
+- 将今日的课程安排以**小组件**的样式为你呈现;
+- 具有 [上下课提醒](https://www.yuque.com/rinlit/class-widgets_help/fv2ou1i1ngap0hrl) 和预备铃;
+- 拥有主题系统支持你高度自定义。
+- 简洁直观的 [课程表编辑](https://www.yuque.com/rinlit/class-widgets_help/oozelh8r56tmw0xb) 界面;
+- 同时存储多个课程表文件,并能在各个 Class Widgets 导入和导出;
+- 支持 [**通用课程表交换格式**(Course Schedule Exchange Schema)](https://github.com/SmartTeachCN/CSES) ,能在不同格式间转换;
+- 提供快捷的调休、换课 [应对方案](https://www.yuque.com/rinlit/class-widgets_help/gc4epffu7g5bf9os)。
+- 提供“天气”、“自定义倒计时”等实用小组件;
+- 通过 [“自定义”](https://www.yuque.com/rinlit/class-widgets_help/qyly70ht1ogge1pi) 个性化你的 Class Widgets;
+- 具有亮/暗色主题;
+- ……
+
+## 软件截图
+#### 主界面(亮色)
+
+#### 主界面(暗色)
+
+
+## 安装&使用
+> [!TIP]
+> 可在 [Class Widgets 官方文档](https://www.yuque.com/rinlit/class-widgets_help/gs3gsbms1iivgibm) 查看教程。
+
+> [!IMPORTANT]
+> 若要体验此页面的特性,请前往[此处](https://github.com/Class-Widgets/Class-Widgets/releases/tag/v1.1.7-b3)预发行版的页面下载。
+
+下载  中最新版的压缩文件,解压到合适位置后,打开 `ClassWidgets.exe` 即可。
+可通过托盘菜单进入设置、或退出此程序。
+
+## 协议
+此项目 (Class Widgets) 基于 GPL-3.0 许可证授权发布,详情请参阅 [LICENSE](./LICENSE) 文件。
+
+Copyright © 2025 RinLit.
+
+## 致谢
+
+### 第三方库和框架
+
+- [PyQt5](https://www.riverbankcomputing.com/static/Docs/PyQt5/)
+- [PyQt-Fluent-Widgets](https://github.com/zhiyiYo/PyQt-Fluent-Widgets)
+- [Loguru](https://github.com/Delgan/loguru)
+- [Requests](https://github.com/psf/requests)
+
+### 资源
+
+- [SF Symbols](https://developer.apple.com/cn/sf-symbols/) (部分图标已做修改)
+- [和风天气图标](https://icons.qweather.com/)(部分图标已做修改)
+- [HarmonyOS Sans](https://developer.huawei.com/consumer/cn/design/resource/)
+
+### 贡献
+
+感谢以下同学为 Class Widgets 作出贡献。
+
+[](https://github.com/Class-Widgets/Class-Widgets/graphs/contributors)
+
+
+如果您想要为 Class Widgets 作出贡献,请阅读[贡献指南](CONTRIBUTING.md)
+
+### 赞助商 / Sponsors
+
+感谢以下人员对本项目的支持。
+- [猞猁](http://dq6666.cn/)
+
+感谢以下赞助商对本项目的支持。
+
+
+
+## 代码签名策略 / Code signing policy
+
+- Free code signing provided by [SignPath.io](https://about.signpath.io/), certificate by [SignPath Foundation](https://signpath.org/)
+由 [SignPath.io](https://about.signpath.io/) 提供代码签名,由 [SignPath Foundation](https://signpath.org/) 提供证书
+
+- Committers and reviewers: [Organization Members](https://github.com/orgs/Class-Widgets/people)
+提交者和审阅者:[团队成员](https://github.com/orgs/Class-Widgets/people)
+
+- Approvers: [Owners](https://github.com/orgs/Class-Widgets/people?query=role%3Aowner)
+审批者:[所有者](https://github.com/orgs/Class-Widgets/people?query=role%3Aowner)
+
+- This program will not transfer any information to other networked systems unless specifically requested by the user or the person installing or operating it.
+除非用户或安装或操作它的人特别要求,否则本程序不会将任何信息传输到其他网络系统。
+
+## 社区
+我们目前开通了 [Discussions](https://github.com/orgs/Class-Widgets/discussions)、[QQ群](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=yHXKCAjOxlpTpJ4mNdXm0mxOneYUinRs&authKey=sd3%2F06iGdOZUjkXXPBeIzGnFDIeYwmdwuM8dhk25fi%2B1CUL32MkeN2EEfjdo2pzE&noverify=0&group_code=169200380) 和 [Discord 服务器](https://discord.gg/EFF4PpqpqZ)。
+
+## 星标历史
+
+
+
+
+
+
+##
+这仅是我作为新人的练习作品,欢迎提供更多意见!
diff --git a/Scripts/buildLinux.sh b/Scripts/buildLinux.sh
new file mode 100644
index 0000000..32ef85f
--- /dev/null
+++ b/Scripts/buildLinux.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+uv venv
+source .venv/bin/activate
+uv pip install -r requirements.txt
+uv pip install nuitka imageio
+python -m nuitka main.py \
+--enable-plugin=pyqt5 \
+--mode=app \
+-o"ClassWidgets" \
+--include-data-dir=img=img \
+--include-data-dir=ui=ui \
+--include-data-dir=view=view \
+--include-data-dir=config=config \
+--include-data-dir=plugins=plugins \
+--include-data-dir=font=font \
+--include-data-dir=audio=audio \
+--include-data-files=LICENSE=LICENSE \
+--include-package=pyttsx3.drivers
diff --git a/Scripts/buildOSX.sh b/Scripts/buildOSX.sh
new file mode 100644
index 0000000..03f722e
--- /dev/null
+++ b/Scripts/buildOSX.sh
@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+uv venv
+source .venv/bin/activate
+uv pip install -r requirements.txt
+uv pip install nuitka imageio
+python -m nuitka main.py \
+--enable-plugin=pyqt5 \
+--mode=app \
+-o"ClassWidgets" \
+--macos-app-icon=img/favicon.icns \
+--product-name="Class Widgets" \
+--product-version="1.1.7.1" \
+--file-description="全新桌面课表" \
+--include-data-dir=img=img \
+--include-data-dir=ui=ui \
+--include-data-dir=view=view \
+--include-data-dir=config=config \
+--include-data-dir=plugins=plugins \
+--include-data-dir=font=font \
+--include-data-dir=audio=audio \
+--include-data-files=LICENSE=LICENSE \
+--include-package=pyttsx3.drivers
+
+mv main.app Class\ Widgets.app
diff --git a/Scripts/buildWin.bat b/Scripts/buildWin.bat
new file mode 100644
index 0000000..9e34461
--- /dev/null
+++ b/Scripts/buildWin.bat
@@ -0,0 +1,26 @@
+@echo off
+echo 创建虚拟环境
+uv venv
+call .venv\Scripts\activate
+echo 安装依赖
+uv pip install -r requirements.txt
+uv pip install nuitka imageio
+echo 打包
+python -m nuitka main.py ^
+--enable-plugin=pyqt5 ^
+--disable-console ^
+--mode=app ^
+-o"ClassWidgets" ^
+--windows-icon-from-ico=img/favicon.icns ^
+--product-name="Class Widgets" ^
+--product-version="1.1.7.1" ^
+--file-description="全新桌面课表" ^
+--include-data-dir=img=img ^
+--include-data-dir=ui=ui ^
+--include-data-dir=view=view ^
+--include-data-dir=config=config ^
+--include-data-dir=plugins=plugins ^
+--include-data-dir=font=font ^
+--include-data-dir=audio=audio ^
+--include-data-files=LICENSE=LICENSE ^
+--include-package=pyttsx3.drivers
diff --git a/audio/attend_class.wav b/audio/attend_class.wav
new file mode 100644
index 0000000..f1ac549
Binary files /dev/null and b/audio/attend_class.wav differ
diff --git a/audio/finish_class.wav b/audio/finish_class.wav
new file mode 100644
index 0000000..86d2430
Binary files /dev/null and b/audio/finish_class.wav differ
diff --git a/audio/prepare_class.wav b/audio/prepare_class.wav
new file mode 100644
index 0000000..e49734a
Binary files /dev/null and b/audio/prepare_class.wav differ
diff --git a/basic_dirs.py b/basic_dirs.py
new file mode 100644
index 0000000..410e426
--- /dev/null
+++ b/basic_dirs.py
@@ -0,0 +1,77 @@
+import os
+from pathlib import Path
+from sys import platform
+from loguru import logger
+
+APP_NAME = "Class Widgets"
+CW_HOME = Path(__file__).parent
+
+if str(CW_HOME).endswith("MacOS"):
+ CW_HOME = Path(__file__).absolute().parent.parent / "Resources"
+
+IS_PORTABLE = os.environ.get("CLASSWIDGETS_NOT_PORTABLE", "") == ""
+
+
+def _ensure_dir(path: Path) -> Path:
+ path.mkdir(parents=True, exist_ok=True)
+ return path
+
+
+# 公共基础函数
+def _get_app_dir(
+ purpose: str,
+ default_subdir: str,
+ win_env_var: str,
+ mac_subpath: str,
+ xdg_env_var: str,
+ xdg_fallback: str,
+) -> Path:
+ """获取应用目录的通用实现"""
+ if IS_PORTABLE:
+ return _ensure_dir(CW_HOME / default_subdir)
+
+ # 处理自定义路径
+ if custom := os.environ.get(f"CLASSWIDGETS_CUSTOM_{purpose.upper()}_HOME"):
+ return _ensure_dir(Path(custom))
+
+ # Windows 逻辑
+ if platform == "win32":
+ if base := os.environ.get(win_env_var):
+ return _ensure_dir(Path(base) / APP_NAME / default_subdir)
+ logger.error(f"Missing Windows environment variable: {win_env_var}")
+ return _ensure_dir(CW_HOME / default_subdir)
+
+ # macOS 逻辑
+ if platform == "darwin":
+ return _ensure_dir(Path.home() / mac_subpath / APP_NAME / default_subdir)
+
+ # Linux/Unix 逻辑
+ base = os.environ.get(xdg_env_var) or str(Path.home() / xdg_fallback)
+ return _ensure_dir(Path(base) / APP_NAME / default_subdir)
+
+
+# 最终路径
+CONFIG_HOME = _get_app_dir(
+ purpose="CONFIG",
+ default_subdir="config",
+ win_env_var="APPDATA",
+ mac_subpath="Library/Application Support",
+ xdg_env_var="XDG_CONFIG_HOME",
+ xdg_fallback=".config",
+)
+LOG_HOME = _get_app_dir(
+ purpose="LOG",
+ default_subdir="log",
+ win_env_var="TMP",
+ mac_subpath="Library/Caches",
+ xdg_env_var="XDG_CACHE_HOME",
+ xdg_fallback=".cache",
+)
+PLUGIN_HOME = _get_app_dir(
+ purpose="PLUGIN",
+ default_subdir="plugins",
+ win_env_var="APPDATA",
+ mac_subpath="Library/Application Support",
+ xdg_env_var="XDG_DATA_HOME",
+ xdg_fallback=".local/share",
+)
diff --git a/cliff.toml b/cliff.toml
new file mode 100644
index 0000000..f9aafe7
--- /dev/null
+++ b/cliff.toml
@@ -0,0 +1,62 @@
+[changelog]
+body = """
+
+## Class Widgets 新版本!{% if version %}({{ version }}){%- endif -%}
+{% for group, commits in commits | group_by(attribute="group") %}
+
+ ### {{ group | upper_first }}
+ {% for commit in commits | unique(attribute="message") %}
+ - {% if commit.scope %}{{ commit.scope }}: {%- endif -%}{{ commit.message | upper_first }}\
+ {% if commit.remote.username %} by @{{ commit.remote.username }}{%- endif -%}
+ {% if commit.remote.pr_number %} in \
+ [#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }}) \
+ {%- endif %}
+{% endfor %}
+{%- endfor -%}
+
+{%- if github -%}
+{% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
+ {% raw %}\n{% endraw -%}
+ ## 新贡献者
+{%- endif %}\
+{% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}
+ * @{{ contributor.username }} {%- if contributor.pr_number %} 在
+ [#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \
+ {%- endif %}\
+ 第一次贡献
+
+{%- endfor -%}
+{%- endif -%}
+
+{% if version %}
+ {% if previous.version %}
+ **Full Changelog**: {{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }}
+ {% endif %}
+{% else -%}
+ {% raw %}\n{% endraw %}
+{% endif %}
+
+{%- macro remote_url() -%}
+ https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
+{%- endmacro -%}
+"""
+trim = true
+footer = ""
+output = "CHANGELOG.md"
+
+[git]
+conventional_commits = true
+filter_unconventional = true
+split_commits = false
+commit_parsers = [
+ { message = "^feat", group = "新功能"},
+ { message = "^fix", group = "Bug 修复"},
+ { message = "^refactor", group = "重构"},
+]
+protect_breaking_commits = false
+filter_commits = true
+#tag_pattern = "v[0-9].*"
+topo_order = false
+sort_commits = "oldest"
+link_parsers = []
+limit_commits = 42
diff --git a/conf.py b/conf.py
new file mode 100644
index 0000000..152df9c
--- /dev/null
+++ b/conf.py
@@ -0,0 +1,344 @@
+import json
+import os
+import re
+import configparser as config
+from pathlib import Path
+
+from datetime import datetime
+import time
+from dateutil import parser
+from loguru import logger
+from file import base_directory, config_center
+
+import list_
+
+if os.name == 'nt':
+ from win32com.client import Dispatch
+
+conf = config.ConfigParser()
+name = 'Class Widgets'
+
+PLUGINS_DIR = Path(base_directory) / 'plugins'
+
+# app 图标
+if os.name == 'nt':
+ app_icon = os.path.join(base_directory, 'img', 'favicon.ico')
+elif os.name == 'darwin':
+ app_icon = os.path.join(base_directory, 'img', 'favicon.icns')
+else:
+ app_icon = os.path.join(base_directory, 'img', 'favicon.png')
+
+update_countdown_custom_last = 0
+countdown_cnt = 0
+
+def load_theme_config(theme):
+ try:
+ with open(f'{base_directory}/ui/{theme}/theme.json', 'r', encoding='utf-8') as file:
+ data = json.load(file)
+ return data
+ except FileNotFoundError:
+ logger.warning(f"主题配置文件 {theme} 不存在,返回默认配置")
+ return f'{base_directory}/ui/default/theme.json'
+ except Exception as e:
+ logger.error(f"加载主题数据时出错: {e}")
+ return None
+
+
+def load_plugin_config():
+ try:
+ if os.path.exists(f'{base_directory}/config/plugin.json'): # 如果配置文件存在
+ with open(f'{base_directory}/config/plugin.json', 'r', encoding='utf-8') as file:
+ data = json.load(file)
+ else:
+ with open(f'{base_directory}/config/plugin.json', 'w', encoding='utf-8') as file:
+ data = {"enabled_plugins": []}
+ json.dump(data, file, ensure_ascii=False, indent=4)
+ return data
+ except Exception as e:
+ logger.error(f"加载启用插件数据时出错: {e}")
+ return None
+
+
+def save_plugin_config(data):
+ data_dict = load_plugin_config()
+ data_dict.update(data)
+ try:
+ with open(f'{base_directory}/config/plugin.json', 'w', encoding='utf-8') as file:
+ json.dump(data_dict, file, ensure_ascii=False, indent=4)
+ return True
+ except Exception as e:
+ logger.error(f"保存启用插件数据时出错: {e}")
+ return False
+
+
+def save_installed_plugin(data):
+ data = {"plugins": data}
+ try:
+ with open(f'{base_directory}/plugins/plugins_from_pp.json', 'w', encoding='utf-8') as file:
+ json.dump(data, file, ensure_ascii=False, indent=4)
+ return True
+ except Exception as e:
+ logger.error(f"保存已安装插件数据时出错: {e}")
+ return False
+
+
+def load_theme_width(theme):
+ try:
+ with open(f'{base_directory}/ui/{theme}/theme.json', 'r', encoding='utf-8') as file:
+ data = json.load(file)
+ return data['widget_width']
+ except Exception as e:
+ logger.error(f"加载主题宽度时出错: {e}")
+ return list_.widget_width
+
+
+def is_temp_week():
+ if config_center.read_conf('Temp', 'set_week') is None or config_center.read_conf('Temp', 'set_week') == '':
+ return False
+ else:
+ return config_center.read_conf('Temp', 'set_week')
+
+
+def is_temp_schedule():
+ if (
+ config_center.read_conf('Temp', 'temp_schedule') is None
+ or config_center.read_conf('Temp', 'temp_schedule') == ''
+ ):
+ return False
+ else:
+ return config_center.read_conf('Temp', 'temp_schedule')
+
+
+def add_shortcut_to_startmenu(file='', icon=''):
+ if os.name != 'nt':
+ return
+ try:
+ if file == "":
+ file_path = os.path.realpath(__file__)
+ else:
+ file_path = os.path.abspath(file) # 将相对路径转换为绝对路径
+
+ if icon == "":
+ icon_path = file_path # 如果未指定图标路径,则使用程序路径
+ else:
+ icon_path = os.path.abspath(icon) # 将相对路径转换为绝对路径
+
+ # 获取开始菜单文件夹路径
+ menu_folder = os.path.join(os.getenv('APPDATA'), 'Microsoft', 'Windows', 'Start Menu', 'Programs')
+
+ # 快捷方式文件名(使用文件名或自定义名称)
+ name = os.path.splitext(os.path.basename(file_path))[0] # 使用文件名作为快捷方式名称
+ shortcut_path = os.path.join(menu_folder, f'{name}.lnk')
+
+ # 创建快捷方式
+ shell = Dispatch('WScript.Shell')
+ shortcut = shell.CreateShortCut(shortcut_path)
+ shortcut.Targetpath = file_path
+ shortcut.WorkingDirectory = os.path.dirname(file_path)
+ shortcut.IconLocation = icon_path # 设置图标路径
+ shortcut.save()
+ except Exception as e:
+ logger.error(f"创建开始菜单快捷方式时出错: {e}")
+
+
+def add_shortcut(file='', icon=''):
+ if os.name != 'nt':
+ return
+ try:
+ if file == "":
+ file_path = os.path.realpath(__file__)
+ else:
+ file_path = os.path.abspath(file)
+
+ if icon == "":
+ icon_path = file_path
+ else:
+ icon_path = os.path.abspath(icon)
+
+ # 获取桌面文件夹路径
+ desktop_folder = os.path.join(os.environ['USERPROFILE'], 'Desktop')
+
+ # 快捷方式文件名(使用文件名或自定义名称)
+ name = os.path.splitext(os.path.basename(file_path))[0] # 使用文件名作为快捷方式名称
+ shortcut_path = os.path.join(desktop_folder, f'{name}.lnk')
+
+ # 创建快捷方式
+ shell = Dispatch('WScript.Shell')
+ shortcut = shell.CreateShortCut(shortcut_path)
+ shortcut.Targetpath = file_path
+ shortcut.WorkingDirectory = os.path.dirname(file_path)
+ shortcut.IconLocation = icon_path # 设置图标路径
+ shortcut.save()
+ except Exception as e:
+ logger.error(f"创建桌面快捷方式时出错: {e}")
+
+
+def add_to_startup(file_path=f'{base_directory}/ClassWidgets.exe', icon_path=''): # 注册到开机启动
+ if os.name != 'nt':
+ return
+ if file_path == "":
+ file_path = os.path.realpath(__file__)
+ else:
+ file_path = os.path.abspath(file_path) # 将相对路径转换为绝对路径
+
+ if icon_path == "":
+ icon_path = file_path # 如果未指定图标路径,则使用程序路径
+ else:
+ icon_path = os.path.abspath(icon_path) # 将相对路径转换为绝对路径
+
+ # 获取启动文件夹路径
+ startup_folder = os.path.join(os.getenv('APPDATA'), 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup')
+
+ # 快捷方式文件名(使用文件名或自定义名称)
+ name = os.path.splitext(os.path.basename(file_path))[0] # 使用文件名作为快捷方式名称
+ shortcut_path = os.path.join(startup_folder, f'{name}.lnk')
+
+ # 创建快捷方式
+ shell = Dispatch('WScript.Shell')
+ shortcut = shell.CreateShortCut(shortcut_path)
+ shortcut.Targetpath = file_path
+ shortcut.WorkingDirectory = os.path.dirname(file_path)
+ shortcut.IconLocation = icon_path # 设置图标路径
+ shortcut.save()
+
+
+def remove_from_startup():
+ startup_folder = os.path.join(os.getenv('APPDATA'), 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup')
+ shortcut_path = os.path.join(startup_folder, f'{name}.lnk')
+ if os.path.exists(shortcut_path):
+ os.remove(shortcut_path)
+
+
+def get_time_offset(): # 获取时差偏移
+ time_offset = config_center.read_conf('General', 'time_offset')
+ if time_offset is None or time_offset == '' or time_offset == '0':
+ return 0
+ else:
+ return int(time_offset)
+
+def update_countdown(cnt):
+ global update_countdown_custom_last
+ global countdown_cnt
+ if (length:=len(config_center.read_conf('Date', 'cd_text_custom').split(','))) == 0:
+ countdown_cnt = -1
+ elif config_center.read_conf('Date', 'countdown_custom_mode') == '1':
+ countdown_cnt = cnt
+ elif (nowtime:=time.time()) - update_countdown_custom_last > int(config_center.read_conf('Date', 'countdown_upd_cd')):
+ update_countdown_custom_last = nowtime
+ countdown_cnt += 1
+ if countdown_cnt >= length:
+ countdown_cnt = 0 if length != 0 else -1
+
+def get_cd_text_custom():
+ global countdown_cnt
+ if countdown_cnt == -1:
+ return '未设置'
+ if countdown_cnt >= len(li:=config_center.read_conf('Date', 'cd_text_custom').split(',')):
+ return '未设置'
+ return li[countdown_cnt] if countdown_cnt >= 0 else ''
+
+
+def get_custom_countdown():
+ global countdown_cnt
+ if countdown_cnt == -1:
+ return '未设置'
+ li = config_center.read_conf('Date', 'countdown_date').split(',')
+ if countdown_cnt == -1 or countdown_cnt >= len(li):
+ return '未设置' # 获取自定义倒计时
+ else:
+ custom_countdown = li[countdown_cnt]
+ if custom_countdown == '':
+ return '未设置'
+ try:
+ custom_countdown = parser.parse(custom_countdown)
+ except Exception as e:
+ logger.error(f"解析日期时出错: {custom_countdown}, 错误: {e}")
+ return '解析失败'
+ if custom_countdown < datetime.now():
+ return '0 天'
+ else:
+ cd_text = custom_countdown - datetime.now()
+ return f'{cd_text.days + 1} 天'
+ # return (
+ # f"{cd_text.days} 天 {cd_text.seconds // 3600} 小时 {cd_text.seconds // 60 % 60} 分"
+ # )
+
+
+def get_week_type():
+ if (temp_schedule := config_center.read_conf('Temp', 'set_schedule')) not in ('', None): # 获取单双周
+ return int(temp_schedule)
+ start_date_str = config_center.read_conf('Date', 'start_date')
+ if start_date_str not in ('', None):
+ try:
+ start_date = parser.parse(start_date_str)
+ except (ValueError, TypeError):
+ logger.error(f"解析日期时出错: {start_date_str}")
+ return 0 # 解析失败默认单周
+ today = datetime.now()
+ week_num = (today - start_date).days // 7 + 1
+ if week_num % 2 == 0:
+ return 1 # 双周
+ else:
+ return 0 # 单周
+ else:
+ return 0 # 默认单周
+
+
+def get_is_widget_in(widget='example.ui'):
+ widgets_list = list_.get_widget_config()
+ if widget in widgets_list:
+ return True
+ else:
+ return False
+
+
+def save_widget_conf_to_json(new_data):
+ # 初始化 data_dict 为一个空字典
+ data_dict = {}
+ if os.path.exists(f'{base_directory}/config/widget.json'):
+ try:
+ with open(f'{base_directory}/config/widget.json', 'r', encoding='utf-8') as file:
+ data_dict = json.load(file)
+ except Exception as e:
+ print(f"读取现有数据时出错: {e}")
+ return e
+ data_dict.update(new_data)
+ try:
+ with open(f'{base_directory}/config/widget.json', 'w', encoding='utf-8') as file:
+ json.dump(data_dict, file, ensure_ascii=False, indent=4)
+ return True
+ except Exception as e:
+ print(f"保存数据时出错: {e}")
+ return e
+
+
+def load_plugins(): # 加载插件配置文件
+ plugin_dict = {}
+ for folder in Path(PLUGINS_DIR).iterdir():
+ if folder.is_dir() and (folder / 'plugin.json').exists():
+ try:
+ with open(f'{base_directory}/plugins/{folder.name}/plugin.json', 'r', encoding='utf-8') as file:
+ data = json.load(file)
+ except Exception as e:
+ logger.error(f"加载插件配置文件数据时出错,将跳过: {e}") # 跳过奇怪的文件夹
+ plugin_dict[str(folder.name)] = {}
+ plugin_dict[str(folder.name)]['name'] = data['name'] # 名称
+ plugin_dict[str(folder.name)]['version'] = data['version'] # 插件版本号
+ plugin_dict[str(folder.name)]['author'] = data['author'] # 作者
+ plugin_dict[str(folder.name)]['description'] = data['description'] # 描述
+ plugin_dict[str(folder.name)]['plugin_ver'] = data['plugin_ver'] # 插件架构版本
+ plugin_dict[str(folder.name)]['settings'] = data['settings'] # 设置
+ return plugin_dict
+
+
+if __name__ == '__main__':
+ print('AL_1S')
+ print(get_week_type())
+ print(load_plugins())
+ # save_data_to_json(test_data_dict, 'schedule-1.json')
+ # loaded_data = load_from_json('schedule-1.json')
+ # print(loaded_data)
+ # schedule = loaded_data.get('schedule')
+
+ # print(schedule['0'])
+ # add_shortcut_to_startmenu('Settings.exe', 'img/favicon.ico')
diff --git a/config/config.json b/config/config.json
new file mode 100644
index 0000000..3ded55b
--- /dev/null
+++ b/config/config.json
@@ -0,0 +1,6 @@
+{
+ "QFluentWidgets": {
+ "ThemeColor": "#ff009faa",
+ "ThemeMode": "Light"
+ }
+}
\ No newline at end of file
diff --git a/config/data/amap_weather.db b/config/data/amap_weather.db
new file mode 100644
index 0000000..053ebd7
Binary files /dev/null and b/config/data/amap_weather.db differ
diff --git a/config/data/amap_weather_status.json b/config/data/amap_weather_status.json
new file mode 100644
index 0000000..d95db7b
--- /dev/null
+++ b/config/data/amap_weather_status.json
@@ -0,0 +1,156 @@
+{
+ "weatherinfo": [
+ {
+ "code": 0,
+ "wea": "晴"
+ },
+ {
+ "code": 1,
+ "wea": "多云"
+ },
+ {
+ "code": 2,
+ "wea": "阴"
+ },
+ {
+ "code": 3,
+ "wea": "阵雨"
+ },
+ {
+ "code": 4,
+ "wea": "雷阵雨"
+ },
+ {
+ "code": 5,
+ "wea": "雷阵雨并伴有冰雹"
+ },
+ {
+ "code": 6,
+ "wea": "雨夹雪"
+ },
+ {
+ "code": 7,
+ "wea": "小雨"
+ },
+ {
+ "code": 8,
+ "wea": "中雨"
+ },
+ {
+ "code": 9,
+ "wea": "大雨"
+ },
+ {
+ "code": 10,
+ "wea": "暴雨"
+ },
+ {
+ "code": 11,
+ "wea": "大暴雨"
+ },
+ {
+ "code": 12,
+ "wea": "特大暴雨"
+ },
+ {
+ "code": 13,
+ "wea": "阵雪"
+ },
+ {
+ "code": 14,
+ "wea": "小雪"
+ },
+ {
+ "code": 15,
+ "wea": "中雪"
+ },
+ {
+ "code": 16,
+ "wea": "大雪"
+ },
+ {
+ "code": 17,
+ "wea": "暴雪"
+ },
+ {
+ "code": 18,
+ "wea": "雾"
+ },
+ {
+ "code": 19,
+ "wea": "冻雨"
+ },
+ {
+ "code": 20,
+ "wea": "沙尘暴"
+ },
+ {
+ "code": 21,
+ "wea": "小雨-中雨"
+ },
+ {
+ "code": 22,
+ "wea": "中雨-大雨"
+ },
+ {
+ "code": 23,
+ "wea": "大雨-暴雨"
+ },
+ {
+ "code": 24,
+ "wea": "暴雨-大暴雨"
+ },
+ {
+ "code": 25,
+ "wea": "大暴雨-特大暴雨"
+ },
+ {
+ "code": 26,
+ "wea": "小雪-中雪"
+ },
+ {
+ "code": 27,
+ "wea": "中雪-大雪"
+ },
+ {
+ "code": 28,
+ "wea": "大雪-暴雪"
+ },
+ {
+ "code": 29,
+ "wea": "浮沉"
+ },
+ {
+ "code": 30,
+ "wea": "扬沙"
+ },
+ {
+ "code": 31,
+ "wea": "强沙尘暴"
+ },
+ {
+ "code": 32,
+ "wea": "飑"
+ },
+ {
+ "code": 33,
+ "wea": "龙卷风"
+ },
+ {
+ "code": 34,
+ "wea": "若高吹雪"
+ },
+ {
+ "code": 35,
+ "wea": "轻雾"
+ },
+ {
+ "code": 53,
+ "wea": "霾"
+ },
+ {
+ "code": 99,
+ "wea": "未知"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/config/data/qq_weather_status.json b/config/data/qq_weather_status.json
new file mode 100644
index 0000000..2e32e8d
--- /dev/null
+++ b/config/data/qq_weather_status.json
@@ -0,0 +1,222 @@
+{
+ "weatherinfo": [
+ {
+ "code": 100,
+ "wea": "晴",
+ "original_code": 0
+ },
+ {
+ "code": 101,
+ "wea": "多云",
+ "original_code": 1
+ },
+ {
+ "code": 102,
+ "wea": "少云",
+ "original_code": 1
+ },
+ {
+ "code": 103,
+ "wea": "晴间多云",
+ "original_code": 1
+ },
+ {
+ "code": 104,
+ "wea": "阴",
+ "original_code": 2
+ },
+ {
+ "code": 150,
+ "wea": "晴",
+ "original_code": 0
+ },
+ {
+ "code": 151,
+ "wea": "多云",
+ "original_code": 1
+ },
+ {
+ "code": 152,
+ "wea": "少云",
+ "original_code": 1
+ },
+ {
+ "code": 153,
+ "wea": "晴间多云",
+ "original_code": 1
+ },
+ {
+ "code": 300,
+ "wea": "阵雨",
+ "original_code": 3
+ },
+ {
+ "code": 301,
+ "wea": "强阵雨",
+ "original_code": 3
+ },
+ {
+ "code": 302,
+ "wea": "雷阵雨",
+ "original_code": 4
+ },
+ {
+ "code": 303,
+ "wea": "强雷阵雨",
+ "original_code": 4
+ },
+ {
+ "code": 304,
+ "wea": "雷阵雨伴有冰雹",
+ "original_code": 5
+ },
+ {
+ "code": 305,
+ "wea": "小雨",
+ "original_code": 7
+ },
+ {
+ "code": 306,
+ "wea": "中雨",
+ "original_code": 8
+ },
+ {
+ "code": 307,
+ "wea": "大雨",
+ "original_code": 9
+ },
+ {
+ "code": 308,
+ "wea": "极端降雨",
+ "original_code": 10
+ },
+ {
+ "code": 309,
+ "wea": "毛毛雨/细雨",
+ "original_code": 7
+ },
+ {
+ "code": 310,
+ "wea": "暴雨",
+ "original_code": 10
+ },
+ {
+ "code": 311,
+ "wea": "大暴雨",
+ "original_code": 11
+ },
+ {
+ "code": 312,
+ "wea": "特大暴雨",
+ "original_code": 12
+ },
+ {
+ "code": 313,
+ "wea": "冻雨",
+ "original_code": 19
+ },
+ {
+ "code": 400,
+ "wea": "小雪",
+ "original_code": 14
+ },
+ {
+ "code": 401,
+ "wea": "中雪",
+ "original_code": 15
+ },
+ {
+ "code": 402,
+ "wea": "大雪",
+ "original_code": 16
+ },
+ {
+ "code": 403,
+ "wea": "暴雪",
+ "original_code": 17
+ },
+ {
+ "code": 404,
+ "wea": "雨夹雪",
+ "original_code": 6
+ },
+ {
+ "code": 405,
+ "wea": "雨雪天气",
+ "original_code": 6
+ },
+ {
+ "code": 406,
+ "wea": "阵雨夹雪",
+ "original_code": 3
+ },
+ {
+ "code": 407,
+ "wea": "阵雪",
+ "original_code": 13
+ },
+ {
+ "code": 408,
+ "wea": "小到中雪",
+ "original_code": 14
+ },
+ {
+ "code": 409,
+ "wea": "中到大雪",
+ "original_code": 15
+ },
+ {
+ "code": 410,
+ "wea": "大到暴雪",
+ "original_code": 17
+ },
+ {
+ "code": 500,
+ "wea": "薄雾",
+ "original_code": 18
+ },
+ {
+ "code": 501,
+ "wea": "雾",
+ "original_code": 18
+ },
+ {
+ "code": 502,
+ "wea": "霾",
+ "original_code": 53
+ },
+ {
+ "code": 503,
+ "wea": "扬沙",
+ "original_code": 30
+ },
+ {
+ "code": 507,
+ "wea": "沙尘暴",
+ "original_code": 20
+ },
+ {
+ "code": 508,
+ "wea": "强沙尘暴",
+ "original_code": 31
+ },
+ {
+ "code": 509,
+ "wea": "浓雾",
+ "original_code": 18
+ },
+ {
+ "code": 900,
+ "wea": "热"
+ },
+ {
+ "code": 901,
+ "wea": "冷"
+ },
+ {
+ "code": 999,
+ "wea": "未知",
+ "original_code": 99
+ }
+ ]
+}
diff --git a/config/data/qweather_status.json b/config/data/qweather_status.json
new file mode 100644
index 0000000..2e32e8d
--- /dev/null
+++ b/config/data/qweather_status.json
@@ -0,0 +1,222 @@
+{
+ "weatherinfo": [
+ {
+ "code": 100,
+ "wea": "晴",
+ "original_code": 0
+ },
+ {
+ "code": 101,
+ "wea": "多云",
+ "original_code": 1
+ },
+ {
+ "code": 102,
+ "wea": "少云",
+ "original_code": 1
+ },
+ {
+ "code": 103,
+ "wea": "晴间多云",
+ "original_code": 1
+ },
+ {
+ "code": 104,
+ "wea": "阴",
+ "original_code": 2
+ },
+ {
+ "code": 150,
+ "wea": "晴",
+ "original_code": 0
+ },
+ {
+ "code": 151,
+ "wea": "多云",
+ "original_code": 1
+ },
+ {
+ "code": 152,
+ "wea": "少云",
+ "original_code": 1
+ },
+ {
+ "code": 153,
+ "wea": "晴间多云",
+ "original_code": 1
+ },
+ {
+ "code": 300,
+ "wea": "阵雨",
+ "original_code": 3
+ },
+ {
+ "code": 301,
+ "wea": "强阵雨",
+ "original_code": 3
+ },
+ {
+ "code": 302,
+ "wea": "雷阵雨",
+ "original_code": 4
+ },
+ {
+ "code": 303,
+ "wea": "强雷阵雨",
+ "original_code": 4
+ },
+ {
+ "code": 304,
+ "wea": "雷阵雨伴有冰雹",
+ "original_code": 5
+ },
+ {
+ "code": 305,
+ "wea": "小雨",
+ "original_code": 7
+ },
+ {
+ "code": 306,
+ "wea": "中雨",
+ "original_code": 8
+ },
+ {
+ "code": 307,
+ "wea": "大雨",
+ "original_code": 9
+ },
+ {
+ "code": 308,
+ "wea": "极端降雨",
+ "original_code": 10
+ },
+ {
+ "code": 309,
+ "wea": "毛毛雨/细雨",
+ "original_code": 7
+ },
+ {
+ "code": 310,
+ "wea": "暴雨",
+ "original_code": 10
+ },
+ {
+ "code": 311,
+ "wea": "大暴雨",
+ "original_code": 11
+ },
+ {
+ "code": 312,
+ "wea": "特大暴雨",
+ "original_code": 12
+ },
+ {
+ "code": 313,
+ "wea": "冻雨",
+ "original_code": 19
+ },
+ {
+ "code": 400,
+ "wea": "小雪",
+ "original_code": 14
+ },
+ {
+ "code": 401,
+ "wea": "中雪",
+ "original_code": 15
+ },
+ {
+ "code": 402,
+ "wea": "大雪",
+ "original_code": 16
+ },
+ {
+ "code": 403,
+ "wea": "暴雪",
+ "original_code": 17
+ },
+ {
+ "code": 404,
+ "wea": "雨夹雪",
+ "original_code": 6
+ },
+ {
+ "code": 405,
+ "wea": "雨雪天气",
+ "original_code": 6
+ },
+ {
+ "code": 406,
+ "wea": "阵雨夹雪",
+ "original_code": 3
+ },
+ {
+ "code": 407,
+ "wea": "阵雪",
+ "original_code": 13
+ },
+ {
+ "code": 408,
+ "wea": "小到中雪",
+ "original_code": 14
+ },
+ {
+ "code": 409,
+ "wea": "中到大雪",
+ "original_code": 15
+ },
+ {
+ "code": 410,
+ "wea": "大到暴雪",
+ "original_code": 17
+ },
+ {
+ "code": 500,
+ "wea": "薄雾",
+ "original_code": 18
+ },
+ {
+ "code": 501,
+ "wea": "雾",
+ "original_code": 18
+ },
+ {
+ "code": 502,
+ "wea": "霾",
+ "original_code": 53
+ },
+ {
+ "code": 503,
+ "wea": "扬沙",
+ "original_code": 30
+ },
+ {
+ "code": 507,
+ "wea": "沙尘暴",
+ "original_code": 20
+ },
+ {
+ "code": 508,
+ "wea": "强沙尘暴",
+ "original_code": 31
+ },
+ {
+ "code": 509,
+ "wea": "浓雾",
+ "original_code": 18
+ },
+ {
+ "code": 900,
+ "wea": "热"
+ },
+ {
+ "code": 901,
+ "wea": "冷"
+ },
+ {
+ "code": 999,
+ "wea": "未知",
+ "original_code": 99
+ }
+ ]
+}
diff --git a/config/data/subject.json b/config/data/subject.json
new file mode 100644
index 0000000..d296866
--- /dev/null
+++ b/config/data/subject.json
@@ -0,0 +1,49 @@
+{
+ "subject_icon": {
+ "语文": "chinese",
+ "数学": "math",
+ "英语": "abc",
+ "生物": "biology",
+ "地理": "geography",
+ "政治": "chinese",
+ "历史": "history",
+ "物理": "physics",
+ "化学": "chemistry",
+ "美术": "art",
+ "音乐": "music",
+ "体育": "pe",
+ "信息技术": "it",
+ "电脑": "it",
+ "课程表未加载": "xmark",
+ "班会": "meeting",
+ "自习": "self_study",
+ "课间": "break",
+ "大课间": "pe",
+ "放学": "after_school",
+ "暂无课程": "break"
+ },
+ "subject_abbreviation": {
+ "历史": "史",
+ "升旗": "旗"
+ },
+ "subject_list": [
+ "语文",
+ "数学",
+ "英语",
+ "政治",
+ "历史",
+ "生物",
+ "地理",
+ "物理",
+ "化学",
+ "体育",
+ "升旗",
+ "班会",
+ "自习",
+ "早读",
+ "大课间",
+ "美术",
+ "音乐",
+ "信息技术"
+ ]
+}
diff --git a/config/data/weather_api.json b/config/data/weather_api.json
new file mode 100644
index 0000000..0606015
--- /dev/null
+++ b/config/data/weather_api.json
@@ -0,0 +1,62 @@
+{
+ "weather_api": {
+ "xiaomi_weather": "https://weatherapi.market.xiaomi.com/wtr-v3/weather/all?latitude=0&longitude=0&locationKey=weathercn:{location_key}&appKey=weather20151024&sign=zUFJoAR2ZVrDy1vF3D07&isGlobal=false&locale=zh_cn&days={days}",
+ "qweather": "https://devapi.qweather.com/v7/weather/now?location={location_key}&key={key}",
+ "amap_weather": "https://restapi.amap.com/v3/weather/weatherInfo?key={key}&city={location_key}",
+ "qq_weather": "https://apis.map.qq.com/ws/weather/v1?key={key}&adcode={location_key}"
+ },
+ "weather_api_parameters": {
+ "xiaomi_weather": {
+ "temp": "current.temperature.value",
+ "icon": "current.weather",
+ "alerts": {
+ "url": null,
+ "title": "alerts.0.title",
+ "type": "alerts.0.level",
+ "description": "alerts.0.detail",
+ "types": {
+ "蓝色": "blue.png",
+ "黄色": "yellow.png",
+ "橙色": "orange.png",
+ "红色": "red.png"
+ }
+ },
+ "database": "xiaomi_weather.db",
+ "return_desc": false
+ },
+ "qweather": {
+ "temp": "now.temp",
+ "icon": "now.icon",
+ "alerts": {
+ "url": "https://devapi.qweather.com/v7/warning/now?location={location_key}&key={key}",
+ "title": "warning.title",
+ "description": "warning.0.text",
+ "type": "warning.0.severityColor",
+ "types": {
+ "Blue": "blue.png",
+ "Yellow": "yellow.png",
+ "Orange": "orange.png",
+ "Red": "red.png"
+ }
+ },
+ "database": "xiaomi_weather.db",
+ "return_desc": false
+ },
+ "amap_weather": {
+ "temp": "temperature",
+ "icon": "weather",
+ "alerts": {},
+ "database": "amap_weather.db",
+ "return_desc": true
+ },
+ "qq_weather": {
+ "temp": "temperature",
+ "icon": "weather",
+ "alerts": {},
+ "database": "amap_weather.db",
+ "return_desc": true
+ }
+ },
+ "weather_api_list": ["xiaomi_weather", "qweather", "amap_weather", "qq_weather"],
+ "weather_api_list_zhCN": ["小米天气", "和风天气 (需 API Key)", "高德天气 (需 API Key)", "腾讯天气 (需 API Key)"]
+}
\ No newline at end of file
diff --git a/config/data/xiaomi_weather.db b/config/data/xiaomi_weather.db
new file mode 100644
index 0000000..fcfe264
Binary files /dev/null and b/config/data/xiaomi_weather.db differ
diff --git a/config/data/xiaomi_weather_status.json b/config/data/xiaomi_weather_status.json
new file mode 100644
index 0000000..d95db7b
--- /dev/null
+++ b/config/data/xiaomi_weather_status.json
@@ -0,0 +1,156 @@
+{
+ "weatherinfo": [
+ {
+ "code": 0,
+ "wea": "晴"
+ },
+ {
+ "code": 1,
+ "wea": "多云"
+ },
+ {
+ "code": 2,
+ "wea": "阴"
+ },
+ {
+ "code": 3,
+ "wea": "阵雨"
+ },
+ {
+ "code": 4,
+ "wea": "雷阵雨"
+ },
+ {
+ "code": 5,
+ "wea": "雷阵雨并伴有冰雹"
+ },
+ {
+ "code": 6,
+ "wea": "雨夹雪"
+ },
+ {
+ "code": 7,
+ "wea": "小雨"
+ },
+ {
+ "code": 8,
+ "wea": "中雨"
+ },
+ {
+ "code": 9,
+ "wea": "大雨"
+ },
+ {
+ "code": 10,
+ "wea": "暴雨"
+ },
+ {
+ "code": 11,
+ "wea": "大暴雨"
+ },
+ {
+ "code": 12,
+ "wea": "特大暴雨"
+ },
+ {
+ "code": 13,
+ "wea": "阵雪"
+ },
+ {
+ "code": 14,
+ "wea": "小雪"
+ },
+ {
+ "code": 15,
+ "wea": "中雪"
+ },
+ {
+ "code": 16,
+ "wea": "大雪"
+ },
+ {
+ "code": 17,
+ "wea": "暴雪"
+ },
+ {
+ "code": 18,
+ "wea": "雾"
+ },
+ {
+ "code": 19,
+ "wea": "冻雨"
+ },
+ {
+ "code": 20,
+ "wea": "沙尘暴"
+ },
+ {
+ "code": 21,
+ "wea": "小雨-中雨"
+ },
+ {
+ "code": 22,
+ "wea": "中雨-大雨"
+ },
+ {
+ "code": 23,
+ "wea": "大雨-暴雨"
+ },
+ {
+ "code": 24,
+ "wea": "暴雨-大暴雨"
+ },
+ {
+ "code": 25,
+ "wea": "大暴雨-特大暴雨"
+ },
+ {
+ "code": 26,
+ "wea": "小雪-中雪"
+ },
+ {
+ "code": 27,
+ "wea": "中雪-大雪"
+ },
+ {
+ "code": 28,
+ "wea": "大雪-暴雪"
+ },
+ {
+ "code": 29,
+ "wea": "浮沉"
+ },
+ {
+ "code": 30,
+ "wea": "扬沙"
+ },
+ {
+ "code": 31,
+ "wea": "强沙尘暴"
+ },
+ {
+ "code": 32,
+ "wea": "飑"
+ },
+ {
+ "code": 33,
+ "wea": "龙卷风"
+ },
+ {
+ "code": 34,
+ "wea": "若高吹雪"
+ },
+ {
+ "code": 35,
+ "wea": "轻雾"
+ },
+ {
+ "code": 53,
+ "wea": "霾"
+ },
+ {
+ "code": 99,
+ "wea": "未知"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/config/default.json b/config/default.json
new file mode 100644
index 0000000..c0ce321
--- /dev/null
+++ b/config/default.json
@@ -0,0 +1,80 @@
+{
+ "part": {
+
+ },
+ "part_name": {
+
+ },
+ "timeline": {
+ "default": {
+
+ },
+ "0": {
+
+ },
+ "1": {
+
+ },
+ "2": {
+
+ },
+ "3": {
+
+ },
+ "4": {
+
+ },
+ "5": {
+
+ },
+ "6": {
+
+ }
+ },
+ "schedule": {
+ "0": [
+
+ ],
+ "1": [
+
+ ],
+ "2": [
+
+ ],
+ "3": [
+
+ ],
+ "4": [
+
+ ],
+ "5": [
+
+ ],
+ "6": [
+
+ ]
+ },
+ "schedule_even": {
+ "0": [
+
+ ],
+ "1": [
+
+ ],
+ "2": [
+
+ ],
+ "3": [
+
+ ],
+ "4": [
+
+ ],
+ "5": [
+
+ ],
+ "6": [
+
+ ]
+ }
+}
\ No newline at end of file
diff --git a/config/default_config.json b/config/default_config.json
new file mode 100644
index 0000000..70dd92b
--- /dev/null
+++ b/config/default_config.json
@@ -0,0 +1,77 @@
+{
+ "General": {
+ "schedule": "新课表 - 1.json",
+ "pin_on_top": 1,
+ "margin": 10,
+ "time_offset": 0,
+ "opacity": 95,
+ "auto_startup": 0,
+ "hide": 0,
+ "hide_method": 0,
+ "color_mode": 2,
+ "enable_alt_schedule": 0,
+ "blur_floating_countdown": "1",
+ "blur_countdown": 0,
+ "theme": "default",
+ "scale": 1,
+ "excluded_lesson": 0,
+ "excluded_lessons": "",
+ "enable_click": 1
+ },
+ "Toast": {
+ "wave": 1,
+ "pin_on_top": 1,
+ "ringtone": 1,
+ "prepare_minutes": 2,
+ "attend_class": 1,
+ "finish_class": 1,
+ "prepare_class": 1,
+ "after_school": 1,
+ "smooth_volume": 0
+ },
+ "Weather": {
+ "city": 0,
+ "api": "xiaomi_weather",
+ "api_key": ""
+ },
+ "Color": {
+ "floating_time": "959595",
+ "attend_class": "DD986F",
+ "finish_class": "46B878",
+ "prepare_class": "7065D8"
+ },
+ "Plugin": {
+ "version": 2,
+ "mirror": "gh_proxy",
+ "auto_delay": 5,
+ "auto_enable_plugin": 1
+ },
+ "Date": {
+ "start_date": "",
+ "cd_text_custom": "自定义",
+ "countdown_date": "",
+ "countdown_upd_cd": 30,
+ "countdown_custom_mode": 1
+ },
+ "Audio": {
+ "volume": 75,
+ "attend_class": "attend_class.wav",
+ "finish_class": "finish_class.wav",
+ "prepare_class": "prepare_class.wav"
+ },
+ "Temp": {
+ "set_week": "",
+ "temp_schedule": "",
+ "set_schedule": ""
+ },
+ "Other": {
+ "do_not_log": 0,
+ "safe_mode": 0,
+ "initialstartup": 1,
+ "multiple_programs": 0,
+ "version_channel": 0,
+ "auto_check_update": 1,
+ "cses_version": 1,
+ "version": "v1.1.7.2"
+ }
+}
diff --git a/config/mirror.json b/config/mirror.json
new file mode 100644
index 0000000..92e8e77
--- /dev/null
+++ b/config/mirror.json
@@ -0,0 +1,8 @@
+{
+ "gh_mirror": {
+ "original": "",
+ "gh_proxy": "https://gh-proxy.com/",
+ "git_mirror": "https://hub.gitmirror.com/",
+ "moeyy": "https://github.moeyy.xyz/"
+ }
+}
\ No newline at end of file
diff --git a/config/schedule/新课表 - 1.json b/config/schedule/新课表 - 1.json
new file mode 100644
index 0000000..c0ce321
--- /dev/null
+++ b/config/schedule/新课表 - 1.json
@@ -0,0 +1,80 @@
+{
+ "part": {
+
+ },
+ "part_name": {
+
+ },
+ "timeline": {
+ "default": {
+
+ },
+ "0": {
+
+ },
+ "1": {
+
+ },
+ "2": {
+
+ },
+ "3": {
+
+ },
+ "4": {
+
+ },
+ "5": {
+
+ },
+ "6": {
+
+ }
+ },
+ "schedule": {
+ "0": [
+
+ ],
+ "1": [
+
+ ],
+ "2": [
+
+ ],
+ "3": [
+
+ ],
+ "4": [
+
+ ],
+ "5": [
+
+ ],
+ "6": [
+
+ ]
+ },
+ "schedule_even": {
+ "0": [
+
+ ],
+ "1": [
+
+ ],
+ "2": [
+
+ ],
+ "3": [
+
+ ],
+ "4": [
+
+ ],
+ "5": [
+
+ ],
+ "6": [
+
+ ]
+ }
+}
\ No newline at end of file
diff --git a/cses_mgr.py b/cses_mgr.py
new file mode 100644
index 0000000..e577fc3
--- /dev/null
+++ b/cses_mgr.py
@@ -0,0 +1,271 @@
+"""
+CSES Format Support
+what is CSES: https://github.com/CSES-org/CSES
+"""
+import json
+import typing
+import cses
+from datetime import datetime, timedelta
+from loguru import logger
+
+import list_ as list_
+import conf
+from file import base_directory, config_center
+
+CSES_WEEKS_TEXTS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
+CSES_WEEKS = [1, 2, 3, 4, 5, 6, 7]
+
+
+def _get_time(time: typing.Union[str, int]) -> datetime:
+ if isinstance(time, str):
+ return datetime.strptime(str(time), '%H:%M:%S')
+ elif isinstance(time, int):
+ return datetime.strptime(f'{int(time / 60 / 60)}:{int(time / 60 % 60)}:{time % 60}','%H:%M:%S')
+ else:
+ raise ValueError(f'需要 int 或 HH:MM:SS 类型,得到 {type(time)},值为 {time}')
+
+
+class CSES_Converter:
+ """
+ CSES 文件管理器
+ 集成导入/导出CSES文件的功能
+ """
+
+ def __init__(self, path='./'):
+ self.generator = None
+ self.parser = None
+ self.path = path
+
+ def load_parser(self):
+ if not cses.CSESParser.is_cses_file(self.path):
+ return "Error: Not a CSES file" # 判定格式
+
+ self.parser = cses.CSESParser(self.path)
+ return self.parser
+
+ def load_generator(self):
+ self.generator = cses.CSESGenerator(version=int(config_center.read_conf('Other', 'cses_version')))
+
+ def convert_to_cw(self):
+ """
+ 将CSES文件转换为Class Widgets格式
+ """
+ try:
+ with open(f'{base_directory}/config/default.json', 'r', encoding='utf-8') as file: # 加载默认配置
+ cw_format = json.load(file)
+ except FileNotFoundError:
+ logger.error(f'File {base_directory}/config/default.json not found')
+ return False
+
+ if not self.parser:
+ raise Exception("Parser not loaded, please load_parser() first.")
+ # 课程表
+ cses_schedules = self.parser.get_schedules()
+ print(cses_schedules)
+
+ part_count = 0
+ part_list = []
+
+ for day in cses_schedules: # 课程
+ # name = day['name']
+ enable_day = day['enable_day']
+ weeks = day['weeks']
+ classes = day['classes']
+
+ last_end_time = None
+ class_count = 0
+
+ for class_ in classes: # 时间线
+ week = str(CSES_WEEKS.index(enable_day)) # 星期
+ subject = class_['subject'] # 课程名
+ time_diff = None
+
+ # 节点
+ if class_ == classes[0]:
+ raw_time = _get_time(class_['start_time'])
+ time = [raw_time.hour, raw_time.minute]
+ if time not in part_list: # 跳过重复的(已创建的)节点
+ cw_format['part'][str(part_count)] = time
+ cw_format['part_name'][str(part_count)] = f'Part {part_count}'
+ part_count += 1
+ part_list.append(time)
+
+ # 时间线
+ start_time = _get_time(class_['start_time'])
+ end_time = _get_time(class_['end_time'])
+ class_count += 1
+
+ # 计算时长
+ duration = int((end_time - start_time).total_seconds() / 60)
+ if last_end_time:
+ time_diff = int((start_time - last_end_time).total_seconds() / 60) # 时差
+
+ if not time_diff: # 如果连堂或第一节课
+ cw_format['timeline'][week][f'a{part_count - 1}{class_count}'] = duration
+ else:
+ cw_format['timeline'][week][f'f{part_count - 1}{class_count - 1}'] = time_diff
+ cw_format['timeline'][week][f'a{part_count - 1}{class_count}'] = duration
+
+ last_end_time = end_time
+
+ # 课程
+ if weeks == 'even':
+ cw_format['schedule_even'][week].append(subject)
+ elif weeks == 'odd':
+ cw_format['schedule'][week].append(subject)
+ elif weeks == 'all':
+ cw_format['schedule'][week].append(subject)
+ cw_format['schedule_even'][week].append(subject)
+ else:
+ logger.warning('本软件暂时不支持更多的周数循环')
+
+ print(cw_format)
+ return cw_format
+
+ def convert_to_cses(self, cw_data=None, cw_path='./'):
+ """
+ 将Class Widgets格式转换为CSES文件,需提供保存路径和Class Widgets数据/路径
+ Args:
+ cw_data: Class Widgets格式数据 (Optional)
+ cw_path: Class Widgets文件路径(Optional)
+ """
+ def convert(schedules, type_='odd'):
+ class_counter_dict = {} # 记录一个节点当天的课程数
+ for part in parts: # 节点循环
+ name = part_names[part]
+ part_start_time = datetime.strptime(f'{parts[part][0]}:{parts[part][1]}', '%H:%M')
+ print(f'Part {part}: {name} - {part_start_time.strftime("%H:%M")}')
+ class_counter_dict[part] = {}
+
+ for day, subjects in schedules.items():
+ time_counter = 0
+ class_counter = 0
+ if timelines[day]: # 自定时间线存在
+ timeline = timelines[day]
+ else: # 自定时间线不存在
+ timeline = timelines['default']
+
+ timelines_part = {str(day): []} # 一个节点的时间线列表
+ for key, time in timeline.items(): # 时间线循环
+ if key.startswith(f'a{part}'): # 科目
+ class_dict = {}
+
+ other_parts_classes = 0
+ for p, t in class_counter_dict.items(): # 超级嵌套
+ if p == part: # 排除当前节点
+ continue
+ all_time = 0
+ for c, d in t.items(): # 超级嵌套
+ if c != str(day): # 排除其他天
+ continue
+ all_time += d
+ other_parts_classes += all_time
+
+ start_time = part_start_time + timedelta(minutes=time_counter)
+ end_time = start_time + timedelta(minutes=int(time))
+ subject = subjects[int(key[2:]) - 1 + other_parts_classes]
+ class_counter += 1
+
+ if subject == '未添加': # 跳过未添加的科目
+ time_counter += int(time) # 时间叠加
+ continue
+
+ class_dict['subject'] = subject
+ class_dict['start_time'] = start_time.strftime('%H:%M:00')
+ class_dict['end_time'] = end_time.strftime('%H:%M:00')
+
+ timelines_part[str(day)].append(class_dict)
+ if key[1] == part: # 时间叠加counter
+ time_counter += int(time)
+
+ class_counter_dict[part][day] = class_counter # 记录一个节点当天的课程数
+
+ print(timelines_part)
+ if not timelines_part[str(day)]: # 跳过空时间线
+ continue
+
+ self.generator.add_schedule(
+ name=f'{name}_{CSES_WEEKS_TEXTS[int(day)]}',
+ enable_day=CSES_WEEKS[int(day)],
+ weeks=type_,
+ classes=[timelines_part[str(day)][i] for i in range(len(timelines_part[str(day)]))]
+ )
+
+ def check_subjects(schedule): # 检查课表是否有未正式设定的科目
+ unset_subjects = []
+ for _, classes in schedule.items():
+ for class_ in classes:
+ if class_ == '未添加':
+ continue
+ if class_ not in cw_subjects['subject_list']:
+ unset_subjects.append(class_)
+ return unset_subjects
+
+ """
+ 转换/CONVERT
+ """
+ # 科目
+ try:
+ with open(f'{base_directory}/config/data/subject.json', 'r', encoding='utf-8') as data:
+ cw_subjects = json.load(data)
+ except FileNotFoundError:
+ logger.error(f'File {base_directory}/config/data/subject.json not found')
+ return False
+
+ for subject_ in cw_subjects['subject_list']:
+ self.generator.add_subject(
+ name=subject_, simplified_name=list_.get_subject_abbreviation(subject_),
+ teacher=None, room=None
+ )
+
+ # 课表
+ if not self.generator:
+ raise Exception("Generator not loaded, please load_generator() first.")
+
+ if cw_path != './' and cw_data is None: # 加载Class Widgets数据
+ try:
+ with open(cw_path, 'r', encoding='utf-8') as data:
+ cw_data = json.load(data)
+ except FileNotFoundError:
+ logger.error(f'File {cw_path} not found')
+ return False
+ else:
+ raise Exception("Please provide a path or a cw_data")
+
+ parts = cw_data['part']
+ part_names = cw_data['part_name']
+ timelines = cw_data['timeline']
+ schedules_odd = cw_data['schedule']
+ schedule_even = cw_data['schedule_even']
+
+ convert(schedules_odd)
+ convert(schedule_even, 'even')
+ us_set_odd = set(check_subjects(schedules_odd))
+ us_set_even = set(check_subjects(schedule_even))
+ us_union = us_set_odd.union(us_set_even)
+
+ for subject_ in list(us_union):
+ self.generator.add_subject(
+ name=subject_, simplified_name=list_.get_subject_abbreviation(subject_),
+ teacher=None, room=None
+ )
+
+ try:
+ self.generator.save_to_file(self.path)
+ return True
+ except Exception as e:
+ logger.error(f'Error: {e}')
+ return False
+
+
+if __name__ == '__main__':
+ # EXAMPLE
+ importer = CSES_Converter(path='./config/cses_schedule/test.yaml')
+ importer.load_parser()
+ importer.convert_to_cw()
+
+ print('_____________________________', end='\n') # 输出分割线
+
+ exporter = CSES_Converter(path='./config/cses_schedule/test2.yaml')
+ exporter.load_generator()
+ exporter.convert_to_cses(cw_path='./config/schedule/default (3).json')
diff --git a/extra_menu.py b/extra_menu.py
new file mode 100644
index 0000000..7f0b3e2
--- /dev/null
+++ b/extra_menu.py
@@ -0,0 +1,224 @@
+import datetime as dt
+import sys
+from shutil import copy
+
+from PyQt5 import uic
+from PyQt5.QtCore import Qt
+from PyQt5.QtGui import QIcon
+from PyQt5.QtWidgets import QApplication, QScroller
+from loguru import logger
+from qfluentwidgets import FluentWindow, FluentIcon as fIcon, ComboBox, \
+ PrimaryPushButton, Flyout, FlyoutAnimationType, InfoBarIcon, ListWidget, LineEdit, ToolButton, HyperlinkButton, \
+ SmoothScrollArea, Dialog
+
+import conf
+import file
+from conf import base_directory
+import list_
+from file import config_center, schedule_center
+from menu import SettingsMenu
+import platform
+from loguru import logger
+
+# 适配高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标识')
+
+settings = None
+
+current_week = dt.datetime.today().weekday()
+temp_schedule = {'schedule': {}, 'schedule_even': {}}
+
+
+def open_settings():
+ if config_center.read_conf('Temp', 'temp_schedule'):
+ w = Dialog(
+ "暂时无法使用“设置”",
+ "由于您正在使用临时课表,将无法使用“设置”的课程表功能;\n若要启用“设置”,请重新启动 Class Widgets。"
+ "\n(重启后,临时课表也将会恢复)",
+ None
+ )
+ w.cancelButton.hide()
+ w.buttonLayout.insertStretch(1)
+ w.exec()
+
+ return
+
+ global settings
+ if settings is None or not settings.isVisible():
+ settings = SettingsMenu()
+ settings.closed.connect(cleanup_settings)
+ settings.show()
+ logger.info('打开“设置”')
+ else:
+ settings.raise_()
+ settings.activateWindow()
+
+
+def cleanup_settings():
+ global settings
+ logger.info('关闭“设置”')
+ del settings
+ settings = None
+
+
+class ExtraMenu(FluentWindow):
+ def __init__(self):
+ super().__init__()
+ self.menu = None
+ self.interface = uic.loadUi(f'{base_directory}/view/extra_menu.ui')
+ self.initUI()
+ self.init_interface()
+
+ def init_interface(self):
+ ex_scroll = self.findChild(SmoothScrollArea, 'ex_scroll')
+ QScroller.grabGesture(ex_scroll, QScroller.LeftMouseButtonGesture)
+ select_temp_week = self.findChild(ComboBox, 'select_temp_week') # 选择替换日期
+ select_temp_week.addItems(list_.week)
+ select_temp_week.setCurrentIndex(current_week)
+ select_temp_week.currentIndexChanged.connect(self.refresh_schedule_list) # 日期选择变化
+
+ select_temp_schedule = self.findChild(ComboBox, 'select_temp_schedule') # 选择替换课表
+ select_temp_schedule.addItems(list_.week_type)
+ select_temp_schedule.setCurrentIndex(conf.get_week_type())
+ select_temp_schedule.currentIndexChanged.connect(self.refresh_schedule_list) # 日期选择变化
+
+ tmp_schedule_list = self.findChild(ListWidget, 'schedule_list') # 换课列表
+ tmp_schedule_list.addItems(self.load_schedule())
+ tmp_schedule_list.itemChanged.connect(self.upload_item)
+
+ class_kind_combo = self.findChild(ComboBox, 'class_combo') # 课程类型
+ class_kind_combo.addItems(list_.class_kind)
+
+ set_button = self.findChild(ToolButton, 'set_button')
+ set_button.setIcon(fIcon.EDIT)
+ set_button.clicked.connect(self.edit_item)
+
+ save_temp_conf = self.findChild(PrimaryPushButton, 'save_temp_conf') # 保存设置
+ save_temp_conf.clicked.connect(self.save_temp_conf)
+
+ redirect_to_settings = self.findChild(HyperlinkButton, 'redirect_to_settings')
+ redirect_to_settings.clicked.connect(open_settings)
+
+ @staticmethod
+ def load_schedule():
+ if conf.get_week_type():
+ return schedule_center.schedule_data['schedule_even'][str(current_week)]
+ else:
+ return schedule_center.schedule_data['schedule'][str(current_week)]
+
+ def save_temp_conf(self):
+ try:
+ temp_week = self.findChild(ComboBox, 'select_temp_week')
+ temp_schedule_set = self.findChild(ComboBox, 'select_temp_schedule')
+ if temp_schedule != {'schedule': {}, 'schedule_even': {}}:
+ if config_center.read_conf('Temp', 'temp_schedule') == '': # 备份检测
+ copy(f'{base_directory}/config/schedule/{config_center.schedule_name}',
+ f'{base_directory}/config/schedule/backup.json') # 备份课表配置
+ logger.info(f'备份课表配置成功:已将 {config_center.schedule_name} -备份至-> backup.json')
+ config_center.write_conf('Temp', 'temp_schedule', config_center.schedule_name)
+ file.save_data_to_json(temp_schedule, config_center.schedule_name)
+ schedule_center.update_schedule()
+ config_center.write_conf('Temp', 'set_week', str(temp_week.currentIndex()))
+ config_center.write_conf('Temp', 'set_schedule',str(temp_schedule_set.currentIndex()))
+ Flyout.create(
+ icon=InfoBarIcon.SUCCESS,
+ title='保存成功',
+ content=f"已保存至 ./config.ini \n重启后恢复。",
+ target=self.findChild(PrimaryPushButton, 'save_temp_conf'),
+ parent=self,
+ isClosable=True,
+ aniType=FlyoutAnimationType.PULL_UP
+ )
+ except Exception as e:
+ Flyout.create(
+ icon=InfoBarIcon.ERROR,
+ title='保存失败',
+ content=f"错误信息:{e}",
+ target=self.findChild(PrimaryPushButton, 'save_temp_conf'),
+ parent=self,
+ isClosable=True,
+ aniType=FlyoutAnimationType.PULL_UP
+ )
+
+ def refresh_schedule_list(self):
+ global current_week
+ current_week = self.findChild(ComboBox, 'select_temp_week').currentIndex()
+ current_schedule = self.findChild(ComboBox, 'select_temp_schedule').currentIndex()
+ logger.debug(f'current_week: {current_week}, current_schedule: {current_schedule}')
+ tmp_schedule_list = self.findChild(ListWidget, 'schedule_list') # 换课列表
+ tmp_schedule_list.clear()
+ tmp_schedule_list.clearSelection()
+ if config_center.read_conf('Temp', 'temp_schedule') == '':
+ if current_schedule:
+ tmp_schedule_list.addItems(
+ schedule_center.schedule_data['schedule_even'][str(current_week)]
+ )
+ else:
+ tmp_schedule_list.addItems(
+ schedule_center.schedule_data['schedule'][str(current_week)]
+ )
+ else:
+ if current_schedule:
+ tmp_schedule_list.addItems(file.load_from_json('backup.json')['schedule_even'][str(current_week)])
+ else:
+ tmp_schedule_list.addItems(file.load_from_json('backup.json')['schedule'][str(current_week)])
+
+ def upload_item(self):
+ global temp_schedule
+ se_schedule_list = self.findChild(ListWidget, 'schedule_list')
+ cache_list = []
+ for i in range(se_schedule_list.count()): # 缓存ListWidget数据至列表
+ item_text = se_schedule_list.item(i).text()
+ cache_list.append(item_text)
+ if conf.get_week_type():
+ temp_schedule['schedule_even'][str(current_week)] = cache_list
+ else:
+ temp_schedule['schedule'][str(current_week)] = cache_list
+
+ def edit_item(self):
+ tmp_schedule_list = self.findChild(ListWidget, 'schedule_list')
+ class_combo = self.findChild(ComboBox, 'class_combo')
+ custom_class = self.findChild(LineEdit, 'custom_class')
+ selected_items = tmp_schedule_list.selectedItems()
+
+ if selected_items:
+ selected_item = selected_items[0]
+ if class_combo.currentIndex() != 0:
+ selected_item.setText(class_combo.currentText())
+ else:
+ if custom_class.text() != '':
+ selected_item.setText(custom_class.text())
+
+ def initUI(self):
+ # 修复设置窗口在各个屏幕分辨率DPI下的窗口大小
+ screen_geometry = QApplication.primaryScreen().geometry()
+ screen_width = screen_geometry.width()
+ screen_height = screen_geometry.height()
+
+ width = int(screen_width * 0.55)
+ height = int(screen_height * 0.65)
+
+ self.move(int(screen_width / 2 - width / 2), 150)
+ self.resize(width, height)
+
+ self.setWindowTitle('Class Widgets - 更多功能')
+ self.setWindowIcon(QIcon(f'{base_directory}/img/logo/favicon-exmenu.ico'))
+
+ self.addSubInterface(self.interface, fIcon.INFO, '更多设置')
+
+ def closeEvent(self, e):
+ self.deleteLater()
+ return super().closeEvent(e)
+
+
+if __name__ == '__main__':
+ app = QApplication(sys.argv)
+ ex = ExtraMenu()
+ ex.show()
+ sys.exit(app.exec())
diff --git a/file.py b/file.py
new file mode 100644
index 0000000..d765117
--- /dev/null
+++ b/file.py
@@ -0,0 +1,201 @@
+import json
+import os
+import sys
+from pathlib import Path
+from shutil import copy
+
+from loguru import logger
+import configparser as config
+
+base_directory = os.path.dirname(os.path.abspath(__file__))
+'''
+if base_directory.endswith('MacOS'):
+ base_directory = os.path.join(os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir)), 'Resources')
+'''
+path = f'{base_directory}/config.ini'
+
+
+class ConfigCenter:
+ """
+ Config中心
+ """
+ def __init__(self):
+ self.config = config.ConfigParser()
+ self.config.read(path, encoding='utf-8')
+ with open(f'{base_directory}/config/default_config.json', encoding="utf-8") as default:
+ self.default_data = json.load(default)
+
+ self.check_config()
+ self.schedule_name = self.read_conf('General', 'schedule')
+ self.old_schedule_name = self.schedule_name
+
+ def update_conf(self):
+ try:
+ self.config.read_file(open(path, 'r', encoding='utf-8'))
+
+ self.schedule_name = self.read_conf('General', 'schedule')
+ if self.schedule_name != self.old_schedule_name:
+ logger.info(f'已切换课程表: {self.schedule_name}')
+ schedule_center.update_schedule()
+ self.old_schedule_name = self.schedule_name
+ except Exception as e:
+ logger.error(f'更新配置文件时出错: {e}')
+
+ def read_conf(self, section='General', key=''):
+ if section in self.config and key in self.config[section]:
+ return self.config[section][key]
+ elif section in self.config and key == '':
+ return dict(self.config[section])
+ elif section in self.default_data and key in self.default_data[section]:
+ logger.info('配置文件出现问题,已尝试修复')
+ self.write_conf(section, key, self.default_data[section][key])
+ return self.default_data[section][key]
+ elif section in self.default_data and key == '':
+ logger.info('配置文件出现问题,已尝试修复')
+ self.write_conf(section, '', self.default_data[section])
+ return dict(self.default_data[section])
+ else:
+ return None
+
+ def write_conf(self, section, key, value):
+ if section not in self.config:
+ self.config.add_section(section)
+
+ self.config.set(section, key, str(value))
+
+ with open(path, 'w', encoding='utf-8') as configfile:
+ self.config.write(configfile)
+
+ def check_config(self):
+ if not os.path.exists(path): # 如果配置文件不存在,则copy默认配置文件
+ self.config.read_dict(self.default_data)
+ with open(path, 'w', encoding='utf-8') as configfile:
+ self.config.write(configfile)
+ if sys.platform != 'win32':
+ self.config.set('General', 'hide_method', '2')
+ with open(path, 'w', encoding='utf-8') as configfile:
+ self.config.write(configfile)
+ logger.info("配置文件不存在,已创建并写入默认配置。")
+ copy(f'{base_directory}/config/default.json', f'{base_directory}/config/schedule/新课表 - 1.json')
+ else:
+ with open(path, 'r', encoding='utf-8') as configfile:
+ self.config.read_file(configfile)
+
+ if self.config['Other']['version'] != self.default_data['Other']['version']: # 如果配置文件版本不同,则更新配置文件
+ logger.info(f"配置文件版本不同,将重新适配")
+ try:
+ for section, options in self.default_data.items():
+ if section not in self.config:
+ self.config[section] = options
+ else:
+ for key, value in options.items():
+ if key not in self.config[section]:
+ self.config[section][key] = str(value)
+ self.config.set('Other', 'version', self.default_data['Other']['version'])
+ with open(path, 'w', encoding='utf-8') as configfile:
+ self.config.write(configfile)
+ logger.info(f"配置文件已更新")
+ except Exception as e:
+ logger.error(f"配置文件更新失败: {e}")
+
+ if not os.path.exists(f"{base_directory}/config/schedule/{self.read_conf('General', 'schedule')}"):
+ # 如果config.ini课程表不存在,则创建
+
+ schedule_config = []
+ # 遍历目标目录下的所有文件
+ for file_name in os.listdir(f'{base_directory}/config/schedule'):
+ # 找json
+ if file_name.endswith('.json') and file_name != 'backup.json':
+ # 将文件路径添加到列表
+ schedule_config.append(file_name)
+ if not schedule_config:
+ copy(f'{base_directory}/config/default.json',
+ f'{base_directory}/config/schedule/{self.read_conf("General", "schedule")}')
+ logger.info(f"课程表不存在,已创建默认课程表")
+ else:
+ config_center.write_conf('General', 'schedule', schedule_config[0])
+ print(os.path.join(os.getcwd(), 'config', 'schedule'))
+
+ # 判断是否存在 Plugins 文件夹
+ plugins_dir = Path(base_directory) / 'plugins'
+ if not plugins_dir.exists():
+ plugins_dir.mkdir()
+ logger.info("Plugins 文件夹不存在,已创建。")
+
+ # 判断 Plugins 文件夹内是否存在 plugins_from_pp.json 文件
+ plugins_file = plugins_dir / 'plugins_from_pp.json'
+ if not plugins_file.exists():
+ with open(plugins_file, 'w', encoding='utf-8') as file:
+ # 使用 indent=4 来缩进,并确保数组元素在多行显示
+ json.dump({"plugins": []}, file, ensure_ascii=False, indent=4)
+ logger.info("plugins_from_pp.json 文件不存在,已创建。")
+
+
+class ScheduleCenter:
+ """
+ 课程表中心
+ """
+ def __init__(self):
+ self.schedule_data = None
+ self.update_schedule()
+
+ def update_schedule(self):
+ """
+ 更新课程表
+ """
+ self.schedule_data = load_from_json(config_center.schedule_name)
+
+ def save_data(self, new_data, filename):
+ # 更新,添加或覆盖新的数据
+ self.schedule_data.update(new_data)
+
+ # 将更新后的数据保存回文件
+ try:
+ with open(f'{base_directory}/config/schedule/{filename}', 'w', encoding='utf-8') as file:
+ json.dump(self.schedule_data, file, ensure_ascii=False, indent=4)
+ return f"数据已成功保存到 config/schedule/{filename}"
+ except Exception as e:
+ logger.error(f"保存数据时出错: {e}")
+
+
+def load_from_json(filename):
+ """
+ 从 JSON 文件中加载数据。
+ :param filename: 要加载的文件
+ :return: 返回从文件中加载的数据字典
+ """
+ try:
+ with open(f'{base_directory}/config/schedule/{filename}', 'r', encoding='utf-8') as file:
+ data = json.load(file)
+ return data
+ except Exception as e:
+ logger.error(f"加载数据时出错: {e}")
+ return None
+
+
+def save_data_to_json(new_data, filename):
+ # 初始化 data_dict 为一个空字典
+ data_dict = {}
+
+ # 如果文件存在,先读取文件中的现有数据
+ if os.path.exists(f'{base_directory}/config/schedule/{filename}'):
+ try:
+ with open(f'{base_directory}/config/schedule/{filename}', 'r', encoding='utf-8') as file:
+ data_dict = json.load(file)
+ except Exception as e:
+ logger.error(f"读取现有数据时出错: {e}")
+
+ # 更新 data_dict,添加或覆盖新的数据
+ data_dict.update(new_data)
+
+ # 将更新后的数据保存回文件
+ try:
+ with open(f'{base_directory}/config/schedule/{filename}', 'w', encoding='utf-8') as file:
+ json.dump(data_dict, file, ensure_ascii=False, indent=4)
+ return f"数据已成功保存到 config/schedule/{filename}"
+ except Exception as e:
+ logger.error(f"保存数据时出错: {e}")
+
+
+config_center = ConfigCenter()
+schedule_center = ScheduleCenter()
diff --git a/font/HarmonyOS_Sans_SC_Bold.ttf b/font/HarmonyOS_Sans_SC_Bold.ttf
new file mode 100644
index 0000000..5c925d1
Binary files /dev/null and b/font/HarmonyOS_Sans_SC_Bold.ttf differ
diff --git a/font/LICENSE.txt b/font/LICENSE.txt
new file mode 100644
index 0000000..211a701
Binary files /dev/null and b/font/LICENSE.txt differ
diff --git a/generate_speech.py b/generate_speech.py
new file mode 100644
index 0000000..236ac51
--- /dev/null
+++ b/generate_speech.py
@@ -0,0 +1,317 @@
+import asyncio
+import hashlib
+import os
+import platform
+import re
+import time
+from pathlib import Path
+from typing import Optional
+
+import edge_tts
+import pyttsx3
+from loguru import logger
+
+
+class TTSEngine:
+ """支持多平台和智能语音选择的多引擎TTS工具类"""
+
+ def __init__(self):
+ """
+ 初始化TTS引擎实例
+ 属性:
+ - cache_dir: 音频缓存目录路径(软件运行目录下 cache/audio文件夹)
+ - engine_priority: 引擎优先级列表
+ - voice_mapping: 跨平台语音映射配置表
+ """
+ self.cache_dir = os.path.join(os.getcwd(), "cache", "audio")
+ self._ensure_cache_dir()
+ self.engine_priority = ['edge', 'pyttsx3']
+
+ # 跨平台语音映射表
+ self.voice_mapping = {
+ 'edge': {
+ 'zh-CN': 'zh-CN-YunxiNeural',
+ 'en-US': 'en-US-AriaNeural'
+ },
+ 'pyttsx3': self._get_platform_voices()
+ }
+
+ @staticmethod
+ def _get_platform_voices():
+ """
+ 获取当前平台的默认语音配置
+
+ 返回:
+ - dict: 包含中英文语音ID的字典,结构为{'zh-CN': voice_id, 'en-US': voice_id}
+
+ 平台支持:
+ - Windows: 使用注册表路径标识语音
+ - macOS: 使用Apple语音标识符
+ - Linux: 使用espeak语音名称
+ """
+ current_os = platform.system()
+
+ # Windows默认配置
+ if current_os == 'Windows':
+ return {
+ 'zh-CN': 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech\\Voices\\Tokens\\TTS_MS_ZH-CN_HUIHUI_11.0',
+ 'en-US': 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech\\Voices\\Tokens\\TTS_MS_EN-US_DAVID_11.0'
+ }
+ # macOS默认配置
+ elif current_os == 'Darwin':
+ return {
+ 'zh-CN': 'com.apple.speech.synthesis.voice.ting-ting.premium',
+ 'en-US': 'com.apple.speech.synthesis.voice.Alex'
+ }
+ # Linux默认配置 (espeak)
+ else:
+ return {
+ 'zh-CN': 'chinese',
+ 'en-US': 'english-us'
+ }
+
+ def _ensure_cache_dir(self):
+ Path(self.cache_dir).mkdir(parents=True, exist_ok=True)
+
+ @staticmethod
+ def _generate_filename(text: str, engine: str) -> str:
+ timestamp = str(int(time.time()))
+ hash_str = hashlib.md5(text.encode()).hexdigest()[:8]
+ return f"{engine}_{hash_str}_{timestamp}.mp3"
+
+ @staticmethod
+ async def _edge_tts(text: str, voice: str, file_path: str) -> str:
+ communicate = edge_tts.Communicate(text, voice)
+ await communicate.save(file_path)
+ return file_path
+
+ async def _pyttsx3_tts(self, text: str, voice: str, file_path: str) -> str:
+ loop = asyncio.get_running_loop()
+ return await loop.run_in_executor(
+ None,
+ lambda: self._sync_pyttsx3(text, voice, file_path)
+ )
+
+ @staticmethod
+ def _sync_pyttsx3(text: str, voice: str, file_path: str):
+ engine = None
+ try:
+ engine = pyttsx3.init()
+ engine.connect('started-utterance', lambda name: None)
+ engine.connect('finished-utterance', lambda name, completed: engine.stop())
+
+ # 应用语音设置
+ if voice:
+ voices = engine.getProperty('voices')
+ found_voice = next((v for v in voices if v.id == voice), None)
+ if not found_voice:
+ raise ValueError(f"无效语音ID:{voice}")
+ engine.setProperty('voice', found_voice.id)
+
+ engine.save_to_file(text, file_path)
+ start_time = time.time()
+ engine.startLoop(False)
+ while engine.isBusy():
+ if time.time() - start_time > 10:
+ raise TimeoutError("pyttsx3生成超时")
+ time.sleep(0.1)
+ engine.iterate()
+ engine.endLoop()
+ finally:
+ if engine:
+ engine.stop()
+
+ @staticmethod
+ def _detect_language(text: str) -> str:
+ """改进的语言检测方法"""
+ if re.search(u'[\u4e00-\u9fff]', text):
+ return 'zh-CN'
+ return 'en-US'
+
+ @staticmethod
+ def _validate_pyttsx3_voice(voice_id: str, lang: str) -> str:
+ """验证语音有效性,自动回退"""
+ try:
+ engine = pyttsx3.init()
+ voices = engine.getProperty('voices')
+
+ if any(v.id == voice_id for v in voices):
+ return voice_id
+
+ lang_voices = [v for v in voices if lang in str(v.languages)]
+ if lang_voices:
+ return lang_voices[0].id
+
+ return engine.getProperty('voice')
+ except Exception as e:
+ logger.error(f"语音验证失败: {str(e)}")
+ return ''
+
+ async def _execute_engine(
+ self,
+ engine: str,
+ text: str,
+ voice: str,
+ file_path: str,
+ timeout: float
+ ) -> str:
+ """
+ 生成语音文件的核心异步方法
+
+ 参数:
+ text (str): 要转换的文本内容(支持中英文自动检测)
+ engine (str): 首选TTS引擎(默认edge)
+ voice (str): 指定语音ID(可选),不指定则根据语言自动选择
+ auto_fallback (bool): 引擎失败时是否自动回退(默认False)
+ timeout (float): 单引擎超时时间(秒,默认10)
+ filename (str): 自定义文件名(可选),不指定则自动生成
+
+ 返回:
+ str: 生成的音频文件绝对路径
+
+ 异常:
+ RuntimeError: 所有尝试的引擎均失败时抛出
+ """
+ try:
+ if engine == "edge":
+ task = self._edge_tts(text, voice, file_path)
+ elif engine == "pyttsx3":
+ task = self._pyttsx3_tts(text, voice, file_path)
+ else:
+ raise ValueError(f"不支持的引擎:{engine}")
+
+ return await asyncio.wait_for(task, timeout=timeout)
+ except asyncio.TimeoutError:
+ raise RuntimeError(f"{engine}引擎执行超时")
+ except Exception as e:
+ raise RuntimeError(f"{engine}引擎错误:{str(e)}")
+
+ async def generate_speech(
+ self,
+ text: str,
+ engine: str = "edge",
+ voice: Optional[str] = None,
+ auto_fallback: bool = False,
+ timeout: float = 10.0,
+ filename: Optional[str] = None
+ ) -> str:
+ """核心生成方法"""
+
+ # 自动语音选择逻辑
+ lang = self._detect_language(text)
+ if not voice:
+ if engine == 'pyttsx3':
+ voice = self.voice_mapping[engine].get(lang)
+ voice = self._validate_pyttsx3_voice(voice, lang)
+ else:
+ voice = self.voice_mapping[engine][lang]
+
+ filename = filename or self._generate_filename(text, engine)
+ file_path = os.path.join(self.cache_dir, filename)
+
+ errors = []
+ attempted_engines = set()
+ engines_to_try = [engine]
+ if auto_fallback:
+ for e in self.engine_priority:
+ if e != engine and e not in engines_to_try:
+ engines_to_try.append(e)
+
+ for current_engine in engines_to_try:
+ if current_engine in attempted_engines:
+ continue
+ if current_engine not in self.engine_priority:
+ continue
+
+ attempted_engines.add(current_engine)
+
+ try:
+ await self._execute_engine(
+ engine=current_engine,
+ text=text,
+ voice=voice,
+ file_path=file_path,
+ timeout=timeout
+ )
+
+ actual_filename = self._generate_filename(text, current_engine)
+ actual_path = os.path.join(self.cache_dir, actual_filename)
+ os.rename(file_path, actual_path)
+
+ if not os.path.exists(actual_path):
+ raise RuntimeError(f"语音文件生成失败: {actual_path}")
+
+ logger.info(f"成功生成语音 | 引擎: {current_engine} | 路径: {actual_path}")
+ return actual_path
+
+ except Exception as e:
+ errors.append(f"{current_engine}: {str(e)}")
+ continue
+
+ raise RuntimeError(
+ f"所有引擎尝试失败\n" +
+ "\n".join(errors)
+ )
+
+ def cleanup(self, max_age: int = 86400):
+ now = time.time()
+ for f in Path(self.cache_dir).glob("*.*"):
+ if f.is_file() and (now - f.stat().st_mtime) > max_age:
+ f.unlink()
+
+ @staticmethod
+ def delete_audio_file(file_path: str, retries: int = 3, delay: float = 0.5):
+ """
+ 安全删除音频文件
+ 参数:
+ retries: 重试次数
+ delay: 重试间隔(秒)
+ """
+ for attempt in range(retries):
+ try:
+ if os.path.exists(file_path):
+ os.remove(file_path)
+ logger.info(f"成功删除音频文件: {file_path}")
+ return True
+ except Exception as e:
+ if attempt < retries - 1:
+ logger.warning(f"删除失败,正在重试 ({attempt + 1}/{retries}): {str(e)}")
+ time.sleep(delay)
+ else:
+ logger.error(f"最终删除失败: {file_path} | 错误: {str(e)}")
+ return False
+
+
+def generate_speech_sync(
+ text: str,
+ engine: str = "edge",
+ voice: Optional[str] = None,
+ auto_fallback: bool = False,
+ timeout: float = 10.0,
+ filename: Optional[str] = None
+) -> str:
+ """同步生成方法"""
+ tts = TTSEngine()
+ return asyncio.run(tts.generate_speech(
+ text=text,
+ engine=engine,
+ voice=voice,
+ auto_fallback=auto_fallback,
+ timeout=timeout,
+ filename=filename
+ ))
+
+
+def list_pyttsx3_voices():
+ """跨平台语音列表显示"""
+ engine = pyttsx3.init()
+ voices = engine.getProperty('voices')
+ current_os = platform.system()
+
+ for idx, voice in enumerate(voices):
+ logger.info(f"\n[{current_os} 平台Pyttsx3可用语音包]"
+ f"\n{idx + 1}. ID: {voice.id}"
+ f"\n 名称: {voice.name}"
+ f"\n 语言: {voice.languages[0] if voice.languages else '未知'}"
+ f"\n 性别: {voice.gender}"
+ f"\n" + "-" * 60)
diff --git a/img/Banner.png b/img/Banner.png
new file mode 100644
index 0000000..0f6601a
Binary files /dev/null and b/img/Banner.png differ
diff --git a/img/Logo.png b/img/Logo.png
new file mode 100644
index 0000000..f546636
Binary files /dev/null and b/img/Logo.png differ
diff --git a/img/Octicons-mark-github.svg b/img/Octicons-mark-github.svg
new file mode 100644
index 0000000..89ff12f
--- /dev/null
+++ b/img/Octicons-mark-github.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/img/attend_class.svg b/img/attend_class.svg
new file mode 100644
index 0000000..d2d26bd
--- /dev/null
+++ b/img/attend_class.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/img/bilibili-website.favicon.svg b/img/bilibili-website.favicon.svg
new file mode 100644
index 0000000..2a2e144
--- /dev/null
+++ b/img/bilibili-website.favicon.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/img/favicon.icns b/img/favicon.icns
new file mode 100644
index 0000000..d4401a4
Binary files /dev/null and b/img/favicon.icns differ
diff --git a/img/favicon.ico b/img/favicon.ico
new file mode 100644
index 0000000..03ca8c7
Binary files /dev/null and b/img/favicon.ico differ
diff --git a/img/favicon.png b/img/favicon.png
new file mode 100644
index 0000000..6be578d
Binary files /dev/null and b/img/favicon.png differ
diff --git a/img/logo/favicon-error.ico b/img/logo/favicon-error.ico
new file mode 100644
index 0000000..7dca200
Binary files /dev/null and b/img/logo/favicon-error.ico differ
diff --git a/img/logo/favicon-exmenu.ico b/img/logo/favicon-exmenu.ico
new file mode 100644
index 0000000..f2db168
Binary files /dev/null and b/img/logo/favicon-exmenu.ico differ
diff --git a/img/logo/favicon-settings.ico b/img/logo/favicon-settings.ico
new file mode 100644
index 0000000..597e5ec
Binary files /dev/null and b/img/logo/favicon-settings.ico differ
diff --git a/img/logo/favicon-update.png b/img/logo/favicon-update.png
new file mode 100644
index 0000000..025df78
Binary files /dev/null and b/img/logo/favicon-update.png differ
diff --git a/img/logo/favicon.ico b/img/logo/favicon.ico
new file mode 100644
index 0000000..7d9830d
Binary files /dev/null and b/img/logo/favicon.ico differ
diff --git a/img/logo/favicon.png b/img/logo/favicon.png
new file mode 100644
index 0000000..b1cc0e1
Binary files /dev/null and b/img/logo/favicon.png differ
diff --git a/img/plaza/banner_network-failed.png b/img/plaza/banner_network-failed.png
new file mode 100644
index 0000000..0e66a64
Binary files /dev/null and b/img/plaza/banner_network-failed.png differ
diff --git a/img/plaza/banner_pre.png b/img/plaza/banner_pre.png
new file mode 100644
index 0000000..e4b10ae
Binary files /dev/null and b/img/plaza/banner_pre.png differ
diff --git a/img/plaza/plugin_pre.png b/img/plaza/plugin_pre.png
new file mode 100644
index 0000000..2aac04d
Binary files /dev/null and b/img/plaza/plugin_pre.png differ
diff --git a/img/pp_favicon.png b/img/pp_favicon.png
new file mode 100644
index 0000000..a370d73
Binary files /dev/null and b/img/pp_favicon.png differ
diff --git a/img/screenshot_0.png b/img/screenshot_0.png
new file mode 100644
index 0000000..b8d6570
Binary files /dev/null and b/img/screenshot_0.png differ
diff --git a/img/screenshot_1.png b/img/screenshot_1.png
new file mode 100644
index 0000000..5d720c0
Binary files /dev/null and b/img/screenshot_1.png differ
diff --git a/img/settings/default.png b/img/settings/default.png
new file mode 100644
index 0000000..aceb4fa
Binary files /dev/null and b/img/settings/default.png differ
diff --git a/img/settings/floating.png b/img/settings/floating.png
new file mode 100644
index 0000000..db643aa
Binary files /dev/null and b/img/settings/floating.png differ
diff --git a/img/settings/hide_all.png b/img/settings/hide_all.png
new file mode 100644
index 0000000..a2ba6d9
Binary files /dev/null and b/img/settings/hide_all.png differ
diff --git a/img/settings/plugin-icon.png b/img/settings/plugin-icon.png
new file mode 100644
index 0000000..b5162d0
Binary files /dev/null and b/img/settings/plugin-icon.png differ
diff --git a/img/subject/abc.svg b/img/subject/abc.svg
new file mode 100644
index 0000000..5a8cf87
--- /dev/null
+++ b/img/subject/abc.svg
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/img/subject/after_school.svg b/img/subject/after_school.svg
new file mode 100644
index 0000000..c05d98c
--- /dev/null
+++ b/img/subject/after_school.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/img/subject/art.svg b/img/subject/art.svg
new file mode 100644
index 0000000..3bbcc78
--- /dev/null
+++ b/img/subject/art.svg
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/img/subject/biology.svg b/img/subject/biology.svg
new file mode 100644
index 0000000..2dcf1b0
--- /dev/null
+++ b/img/subject/biology.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/img/subject/break.svg b/img/subject/break.svg
new file mode 100644
index 0000000..7c72410
--- /dev/null
+++ b/img/subject/break.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/img/subject/chemistry.svg b/img/subject/chemistry.svg
new file mode 100644
index 0000000..88e3917
--- /dev/null
+++ b/img/subject/chemistry.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/img/subject/chinese.svg b/img/subject/chinese.svg
new file mode 100644
index 0000000..128d90d
--- /dev/null
+++ b/img/subject/chinese.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/img/subject/geography.svg b/img/subject/geography.svg
new file mode 100644
index 0000000..a1342fe
--- /dev/null
+++ b/img/subject/geography.svg
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/img/subject/history.svg b/img/subject/history.svg
new file mode 100644
index 0000000..973e1ec
--- /dev/null
+++ b/img/subject/history.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/img/subject/it.svg b/img/subject/it.svg
new file mode 100644
index 0000000..e78c4d2
--- /dev/null
+++ b/img/subject/it.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/img/subject/math.svg b/img/subject/math.svg
new file mode 100644
index 0000000..ec2e8e6
--- /dev/null
+++ b/img/subject/math.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/img/subject/meeting.svg b/img/subject/meeting.svg
new file mode 100644
index 0000000..784201b
--- /dev/null
+++ b/img/subject/meeting.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/img/subject/music.svg b/img/subject/music.svg
new file mode 100644
index 0000000..a645f48
--- /dev/null
+++ b/img/subject/music.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/img/subject/pe.svg b/img/subject/pe.svg
new file mode 100644
index 0000000..e2bef6e
--- /dev/null
+++ b/img/subject/pe.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/img/subject/physics.svg b/img/subject/physics.svg
new file mode 100644
index 0000000..c72d2e9
--- /dev/null
+++ b/img/subject/physics.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/img/subject/self_study.svg b/img/subject/self_study.svg
new file mode 100644
index 0000000..eaef2cb
--- /dev/null
+++ b/img/subject/self_study.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/img/subject/xmark.svg b/img/subject/xmark.svg
new file mode 100644
index 0000000..c96a688
--- /dev/null
+++ b/img/subject/xmark.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/img/weather/0.svg b/img/weather/0.svg
new file mode 100644
index 0000000..27a5348
--- /dev/null
+++ b/img/weather/0.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/img/weather/0d.svg b/img/weather/0d.svg
new file mode 100644
index 0000000..a3e92bd
--- /dev/null
+++ b/img/weather/0d.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/img/weather/1.svg b/img/weather/1.svg
new file mode 100644
index 0000000..f7de58b
--- /dev/null
+++ b/img/weather/1.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/img/weather/10.svg b/img/weather/10.svg
new file mode 100644
index 0000000..86a14bb
--- /dev/null
+++ b/img/weather/10.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/img/weather/11.svg b/img/weather/11.svg
new file mode 100644
index 0000000..b9bcc10
--- /dev/null
+++ b/img/weather/11.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/img/weather/12.svg b/img/weather/12.svg
new file mode 100644
index 0000000..f26dcd3
--- /dev/null
+++ b/img/weather/12.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/img/weather/13.svg b/img/weather/13.svg
new file mode 100644
index 0000000..b8fdce9
--- /dev/null
+++ b/img/weather/13.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/img/weather/13d.svg b/img/weather/13d.svg
new file mode 100644
index 0000000..89f1986
--- /dev/null
+++ b/img/weather/13d.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/img/weather/14.svg b/img/weather/14.svg
new file mode 100644
index 0000000..5a689be
--- /dev/null
+++ b/img/weather/14.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/img/weather/15.svg b/img/weather/15.svg
new file mode 100644
index 0000000..83c8a96
--- /dev/null
+++ b/img/weather/15.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/img/weather/16.svg b/img/weather/16.svg
new file mode 100644
index 0000000..2292ac2
--- /dev/null
+++ b/img/weather/16.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/img/weather/17.svg b/img/weather/17.svg
new file mode 100644
index 0000000..4370e46
--- /dev/null
+++ b/img/weather/17.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/img/weather/18.svg b/img/weather/18.svg
new file mode 100644
index 0000000..f966e57
--- /dev/null
+++ b/img/weather/18.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/img/weather/19.svg b/img/weather/19.svg
new file mode 100644
index 0000000..f20fcbe
--- /dev/null
+++ b/img/weather/19.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/img/weather/1d.svg b/img/weather/1d.svg
new file mode 100644
index 0000000..5509ba4
--- /dev/null
+++ b/img/weather/1d.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/img/weather/2.svg b/img/weather/2.svg
new file mode 100644
index 0000000..e510e47
--- /dev/null
+++ b/img/weather/2.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/img/weather/20.svg b/img/weather/20.svg
new file mode 100644
index 0000000..9fddc17
--- /dev/null
+++ b/img/weather/20.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/img/weather/21.svg b/img/weather/21.svg
new file mode 100644
index 0000000..8e3c5a8
--- /dev/null
+++ b/img/weather/21.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/img/weather/22.svg b/img/weather/22.svg
new file mode 100644
index 0000000..3e0e83b
--- /dev/null
+++ b/img/weather/22.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/img/weather/23.svg b/img/weather/23.svg
new file mode 100644
index 0000000..668d134
--- /dev/null
+++ b/img/weather/23.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/img/weather/24.svg b/img/weather/24.svg
new file mode 100644
index 0000000..e4e74d4
--- /dev/null
+++ b/img/weather/24.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/img/weather/25.svg b/img/weather/25.svg
new file mode 100644
index 0000000..dc4460b
--- /dev/null
+++ b/img/weather/25.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/img/weather/26.svg b/img/weather/26.svg
new file mode 100644
index 0000000..de52050
--- /dev/null
+++ b/img/weather/26.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/img/weather/27.svg b/img/weather/27.svg
new file mode 100644
index 0000000..bf13a0c
--- /dev/null
+++ b/img/weather/27.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/img/weather/28.svg b/img/weather/28.svg
new file mode 100644
index 0000000..16fe1da
--- /dev/null
+++ b/img/weather/28.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/img/weather/29.svg b/img/weather/29.svg
new file mode 100644
index 0000000..7e9b3f3
--- /dev/null
+++ b/img/weather/29.svg
@@ -0,0 +1,3 @@
+
+ ?
+
diff --git a/img/weather/3.svg b/img/weather/3.svg
new file mode 100644
index 0000000..f2ae20a
--- /dev/null
+++ b/img/weather/3.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/img/weather/30.svg b/img/weather/30.svg
new file mode 100644
index 0000000..45361d2
--- /dev/null
+++ b/img/weather/30.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/img/weather/31.svg b/img/weather/31.svg
new file mode 100644
index 0000000..9f21685
--- /dev/null
+++ b/img/weather/31.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/img/weather/32.svg b/img/weather/32.svg
new file mode 100644
index 0000000..b49b559
--- /dev/null
+++ b/img/weather/32.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/img/weather/33.svg b/img/weather/33.svg
new file mode 100644
index 0000000..f6e30f7
--- /dev/null
+++ b/img/weather/33.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/img/weather/35.svg b/img/weather/35.svg
new file mode 100644
index 0000000..147f930
--- /dev/null
+++ b/img/weather/35.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/img/weather/3d.svg b/img/weather/3d.svg
new file mode 100644
index 0000000..e948453
--- /dev/null
+++ b/img/weather/3d.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/img/weather/4.svg b/img/weather/4.svg
new file mode 100644
index 0000000..39de26f
--- /dev/null
+++ b/img/weather/4.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/img/weather/5.svg b/img/weather/5.svg
new file mode 100644
index 0000000..69cd3a4
--- /dev/null
+++ b/img/weather/5.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/img/weather/53.svg b/img/weather/53.svg
new file mode 100644
index 0000000..10d191c
--- /dev/null
+++ b/img/weather/53.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/img/weather/6.svg b/img/weather/6.svg
new file mode 100644
index 0000000..ae12377
--- /dev/null
+++ b/img/weather/6.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/img/weather/7.svg b/img/weather/7.svg
new file mode 100644
index 0000000..b7b2459
--- /dev/null
+++ b/img/weather/7.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/img/weather/8.svg b/img/weather/8.svg
new file mode 100644
index 0000000..9dad947
--- /dev/null
+++ b/img/weather/8.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/img/weather/9.svg b/img/weather/9.svg
new file mode 100644
index 0000000..5573adc
--- /dev/null
+++ b/img/weather/9.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/img/weather/900.svg b/img/weather/900.svg
new file mode 100644
index 0000000..7071fff
--- /dev/null
+++ b/img/weather/900.svg
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/img/weather/901.svg b/img/weather/901.svg
new file mode 100644
index 0000000..4c15251
--- /dev/null
+++ b/img/weather/901.svg
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/img/weather/99.svg b/img/weather/99.svg
new file mode 100644
index 0000000..344ee7b
--- /dev/null
+++ b/img/weather/99.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/img/weather/alerts/blue.png b/img/weather/alerts/blue.png
new file mode 100644
index 0000000..f225f0b
Binary files /dev/null and b/img/weather/alerts/blue.png differ
diff --git a/img/weather/alerts/orange.png b/img/weather/alerts/orange.png
new file mode 100644
index 0000000..9e2bae5
Binary files /dev/null and b/img/weather/alerts/orange.png differ
diff --git a/img/weather/alerts/red.png b/img/weather/alerts/red.png
new file mode 100644
index 0000000..2ad5cf8
Binary files /dev/null and b/img/weather/alerts/red.png differ
diff --git a/img/weather/alerts/yellow.png b/img/weather/alerts/yellow.png
new file mode 100644
index 0000000..c72da5c
Binary files /dev/null and b/img/weather/alerts/yellow.png differ
diff --git a/img/weather/bkg/day.png b/img/weather/bkg/day.png
new file mode 100644
index 0000000..25b36c8
Binary files /dev/null and b/img/weather/bkg/day.png differ
diff --git a/img/weather/bkg/night.png b/img/weather/bkg/night.png
new file mode 100644
index 0000000..163836d
Binary files /dev/null and b/img/weather/bkg/night.png differ
diff --git a/img/weather/bkg/rain.png b/img/weather/bkg/rain.png
new file mode 100644
index 0000000..fbe649f
Binary files /dev/null and b/img/weather/bkg/rain.png differ
diff --git a/list_.py b/list_.py
new file mode 100644
index 0000000..7b7f871
--- /dev/null
+++ b/list_.py
@@ -0,0 +1,338 @@
+import json
+import os
+from copy import deepcopy
+from shutil import copy
+
+from loguru import logger
+from file import base_directory, config_center, save_data_to_json
+
+week = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
+week_type = ['单周', '双周']
+part_type = ['节点', '休息段']
+window_status = ['无', '置于顶部', '置于底部']
+color_mode = ['浅色', '深色', '跟随系统']
+hide_mode = ['无', '上课时自动隐藏', '窗口最大化时隐藏', '灵活隐藏']
+non_nt_hide_mode = ['无', '上课时自动隐藏']
+version_channel = ['正式版 (Release)', '测试版 (Beta)']
+
+theme_folder = []
+theme_names = []
+
+subject = {
+ '语文': '(255, 151, 135', # 红
+ '数学': '(105, 84, 255', # 蓝
+ '英语': '(236, 135, 255', # 粉
+ '生物': '(68, 200, 94', # 绿
+ '地理': '(80, 214, 200', # 浅蓝
+ '政治': '(255, 110, 110', # 红
+ '历史': '(180, 130, 85', # 棕
+ '物理': '(130, 85, 180', # 紫
+ '化学': '(84, 135, 190', # 蓝
+ '美术': '(0, 186, 255', # 蓝
+ '音乐': '(255, 101, 158', # 红
+ '体育': '(255, 151, 135', # 红
+ '信息技术': '(84, 135, 190', # 蓝
+ '电脑': '(84, 135, 190', # 蓝
+ '课程表未加载': '(255, 151, 135', # 红
+
+ '班会': '(255, 151, 135', # 红
+ '自习': '(115, 255, 150', # 绿
+ '课间': '(135, 255, 191', # 绿
+ '大课间': '(255, 151, 135', # 红
+ '放学': '(84, 255, 101', # 绿
+ '暂无课程': '(84, 255, 101', # 绿
+}
+
+schedule_dir = os.path.join(base_directory, 'config', 'schedule')
+
+class_activity = ['课程', '课间']
+time = ['上午', '下午', '晚修']
+class_kind = [
+ '自定义',
+ '语文',
+ '数学',
+ '英语',
+ '政治',
+ '历史',
+ '生物',
+ '地理',
+ '物理',
+ '化学',
+ '体育',
+ '班会',
+ '自习',
+ '早读',
+ '大课间',
+ '美术',
+ '音乐',
+ '心理',
+ '信息技术'
+]
+
+default_widgets = [
+ 'widget-time.ui',
+ 'widget-countdown.ui',
+ 'widget-current-activity.ui',
+ 'widget-next-activity.ui'
+]
+
+widget_width = { # 默认宽度
+ 'widget-time.ui': 210,
+ 'widget-countdown.ui': 200,
+ 'widget-current-activity.ui': 360,
+ 'widget-next-activity.ui': 290,
+ 'widget-countdown-day.ui': 200,
+ 'widget-weather.ui': 200
+}
+
+widget_conf = {
+ '当前日期': 'widget-time.ui',
+ '活动倒计时': 'widget-countdown.ui',
+ '当前活动': 'widget-current-activity.ui',
+ '更多活动': 'widget-next-activity.ui',
+ '倒计日': 'widget-countdown-day.ui',
+ '天气': 'widget-weather.ui'
+}
+
+widget_name = {
+ 'widget-time.ui': '当前日期',
+ 'widget-countdown.ui': '活动倒计时',
+ 'widget-current-activity.ui': '当前活动',
+ 'widget-next-activity.ui': '更多活动',
+ 'widget-countdown-day.ui': '倒计日',
+ 'widget-weather.ui': '天气'
+}
+
+native_widget_name = [widget_name[i] for i in widget_name]
+
+try: # 加载课程/主题配置文件
+ subject_info = json.load(open(f'{base_directory}/config/data/subject.json', 'r', encoding='utf-8'))
+ subject_icon = subject_info['subject_icon']
+ subject_abbreviation = subject_info['subject_abbreviation']
+ theme_folder = [f for f in os.listdir(f'{base_directory}/ui/')
+ if os.path.isdir(os.path.join(f'{base_directory}/ui/', f))]
+except Exception as e:
+ logger.error(f'加载课程/主题配置文件发生错误,使用默认配置:{e}')
+ config_center.write_conf('General', 'theme', 'default')
+ subject_icon = {
+ '语文': 'chinese',
+ '数学': 'math',
+ '英语': 'abc',
+ '生物': 'biology',
+ '地理': 'geography',
+ '政治': 'chinese',
+ '历史': 'history',
+ '物理': 'physics',
+ '化学': 'chemistry',
+ '美术': 'art',
+ '音乐': 'music',
+ '体育': 'pe',
+ '信息技术': 'it',
+ '电脑': 'it',
+ '课程表未加载': 'xmark',
+
+ '班会': 'meeting',
+ '自习': 'self_study',
+ '课间': 'break',
+ '大课间': 'pe',
+ '放学': 'after_school',
+ '暂无课程': 'break',
+ }
+ # 简称
+ subject_abbreviation = {
+ '历史': '史'
+ }
+
+not_exist_themes = []
+
+countdown_modes = ['轮播', '多小组件']
+
+for folder in theme_folder:
+ try:
+ json_file = json.load(open(f'{base_directory}/ui/{folder}/theme.json', 'r', encoding='utf-8'))
+ theme_names.append(json_file['name'])
+ except Exception as e:
+ logger.error(f'加载主题文件 theme.json {folder} 发生错误,跳过:{e}')
+ not_exist_themes.append(folder)
+
+for folder in not_exist_themes:
+ theme_folder.remove(folder)
+
+
+def get_widget_list():
+ rl = []
+ for item, value in widget_conf.items():
+ rl.append(item)
+ return rl
+
+
+def get_widget_names():
+ rl = []
+ for item, value in widget_name.items():
+ rl.append(value)
+ return rl
+
+
+def get_current_theme_num():
+ for i in range(len(theme_folder)):
+ if not os.path.exists(f'{base_directory}/config/schedule/{theme_folder[i]}.json'):
+ return "default"
+ if theme_folder[i] == config_center.read_conf('General', 'theme'):
+ return i
+
+
+def get_theme_ui_path(name):
+ for i in range(len(theme_folder)):
+ if theme_names[i] == name:
+ return theme_folder[i]
+ return 'default'
+
+
+def get_subject_abbreviation(key):
+ if key in subject_abbreviation:
+ return subject_abbreviation[key]
+ else:
+ return key[:1]
+
+
+# 学科图标
+def get_subject_icon(key):
+ if key in subject_icon:
+ return f'{base_directory}/img/subject/{subject_icon[key]}.svg'
+ else:
+ return f'{base_directory}/img/subject/self_study.svg'
+
+
+# 学科主题色
+def subject_color(key):
+ if key in subject:
+ return f'{subject[key]}'
+ else:
+ return '(75, 170, 255'
+
+
+def get_schedule_config():
+ schedule_config = []
+ # 遍历目标目录下的所有文件
+ for file_name in os.listdir(schedule_dir):
+ # 找json
+ if file_name.endswith('.json') and file_name != 'backup.json':
+ # 将文件路径添加到列表
+ schedule_config.append(file_name)
+ schedule_config.append('添加新课表')
+ return schedule_config
+
+
+def return_default_schedule_number():
+ total = 0
+ for file_name in os.listdir(schedule_dir):
+ # 找json
+ if file_name.startswith('新课表 - '):
+ total += 1
+ return total
+
+
+def create_new_profile(filename):
+ copy(f'{base_directory}/config/default.json', f'{base_directory}/config/schedule/{filename}')
+
+
+def import_schedule(filepath, filename): # 导入课表
+ try:
+ with open(filepath, 'r', encoding='utf-8') as file:
+ check_data = json.load(file)
+ except Exception as e:
+ logger.error(f"加载数据时出错: {e}")
+ return False
+
+ checked_data = convert_schedule(check_data)
+ # 保存文件
+ try:
+ print(check_data)
+ copy(filepath, f'{base_directory}/config/schedule/{filename}')
+ save_data_to_json(checked_data, filename)
+ config_center.write_conf('General', 'schedule', filename)
+ return True
+ except Exception as e:
+ logger.error(f"保存数据时出错: {e}")
+ return e
+
+
+def convert_schedule(check_data): # 转换课表
+ # 校验课程表
+ if check_data is None:
+ logger.warning('此文件为空')
+ return False
+ elif not check_data.get('timeline') and not check_data.get('schedule'):
+ logger.warning('此文件不是课程表文件')
+ return False
+ # 转换为标准格式
+ if not check_data.get('schedule_even'):
+ logger.warning('此课程表格式不支持单双周')
+ check_data['schedule_even'] = {str(i): [] for i in range(0, 6)}
+
+ if len(check_data.get('part').get('0')) == 2:
+ logger.warning('此课程表格式不支持休息段')
+ for i in range(len(check_data.get('part'))):
+ check_data['part'][str(i)].append('节点')
+
+ if not check_data.get('part') or not check_data.get('part_name'): # 兼容旧版本
+ logger.warning('此课程表格式不支持节点')
+ try:
+ check_data['part'] = { # 转换旧版本时间线为新版
+ "0": check_data['timeline']['start_time_m']['part'], "1": check_data['timeline']['start_time_a']['part']
+ }
+ check_data['part_name'] = {"0": "上午", "1": "下午"}
+ del check_data['timeline']['start_time_m']
+ del check_data['timeline']['start_time_a']
+ old_timeline = deepcopy(check_data['timeline'])
+ # 转换为标准格式
+ check_data['timeline']['default'] = {}
+ for i in range(0, 6):
+ check_data['timeline'][i] = {}
+
+ for item_name, _ in old_timeline.items():
+ if item_name[1] == 'a':
+ ma_to_num = 1
+ else:
+ ma_to_num = 0
+ new_name = item_name[0]+str(ma_to_num)+item_name[2]
+ check_data['timeline']['default'][new_name] = check_data['timeline'][item_name]
+ del check_data['timeline'][item_name]
+ except Exception as e:
+ logger.error(f"转换数据时出错: {e}")
+ return False
+
+ return check_data
+
+
+def export_schedule(filepath, filename): # 导出课表
+ try:
+ copy(f'{base_directory}/config/schedule/{filename}', filepath)
+ return True
+ except Exception as e:
+ logger.error(f"导出文件时出错: {e}")
+ return e
+
+
+def get_widget_config():
+ try:
+ if os.path.exists(f'{base_directory}/config/widget.json'):
+ with open(f'{base_directory}/config/widget.json', 'r', encoding='utf-8') as file:
+ data = json.load(file)
+ else:
+ with open(f'{base_directory}/config/widget.json', 'w', encoding='utf-8') as file:
+ data = {'widgets': [
+ 'widget-weather.ui', 'widget-countdown.ui', 'widget-current-activity.ui', 'widget-next-activity.ui'
+ ]}
+ json.dump(data, file, indent=4)
+ return data['widgets']
+ except Exception as e:
+ logger.error(f'ReadWidgetConfigFAILD: {e}')
+ return default_widgets
+
+
+if __name__ == '__main__':
+ print(theme_folder)
+ print(theme_names)
+ print('AL-1S')
+ print(get_widget_list())
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..4b60f5d
--- /dev/null
+++ b/main.py
@@ -0,0 +1,2856 @@
+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)
diff --git a/menu.py b/menu.py
new file mode 100644
index 0000000..345d775
--- /dev/null
+++ b/menu.py
@@ -0,0 +1,2140 @@
+import datetime as dt
+import json
+import os
+import subprocess
+import sys
+from copy import deepcopy
+from pathlib import Path
+from shutil import rmtree
+
+from PyQt5 import uic, QtCore
+from PyQt5.QtCore import Qt, QTime, QUrl, QDate, pyqtSignal
+from PyQt5.QtGui import QIcon, QDesktopServices, QColor
+from PyQt5.QtWidgets import QApplication, QHeaderView, QTableWidgetItem, QLabel, QHBoxLayout, QSizePolicy, \
+ QSpacerItem, QFileDialog, QVBoxLayout, QScroller
+from packaging.version import Version
+from loguru import logger
+from qfluentwidgets import (
+ Theme, setTheme, FluentWindow, FluentIcon as fIcon, ToolButton, ListWidget, ComboBox, CaptionLabel,
+ SpinBox, LineEdit, PrimaryPushButton, TableWidget, Flyout, InfoBarIcon, InfoBar, InfoBarPosition,
+ FlyoutAnimationType, NavigationItemPosition, MessageBox, SubtitleLabel, PushButton, SwitchButton,
+ CalendarPicker, BodyLabel, ColorDialog, isDarkTheme, TimeEdit, EditableComboBox, MessageBoxBase,
+ SearchLineEdit, Slider, PlainTextEdit, ToolTipFilter, ToolTipPosition, RadioButton, HyperlinkLabel,
+ PrimaryDropDownPushButton, Action, RoundMenu, CardWidget, ImageLabel, StrongBodyLabel,
+ TransparentDropDownToolButton, Dialog, SmoothScrollArea, TransparentToolButton, HyperlinkButton
+)
+
+import conf
+import list_ as list_
+import tip_toast
+import utils
+from utils import update_tray_tooltip
+import weather_db
+import weather_db as wd
+from conf import base_directory
+from cses_mgr import CSES_Converter
+from file import config_center, schedule_center
+from network_thread import VersionThread
+from plugin import p_loader
+from plugin_plaza import PluginPlaza
+
+# 适配高DPI缩放
+QApplication.setHighDpiScaleFactorRoundingPolicy(
+ Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)
+QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
+QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps)
+
+today = dt.date.today()
+plugin_plaza = None
+
+plugin_dict = {} # 插件字典
+enabled_plugins = {} # 启用的插件列表
+
+morning_st = 0
+afternoon_st = 0
+
+current_week = 0
+
+loaded_data = schedule_center.schedule_data
+
+schedule_dict = {} # 对应时间线的课程表
+schedule_even_dict = {} # 对应时间线的课程表(双周)
+
+timeline_dict = {} # 时间线字典
+
+countdown_dict = {}
+
+
+def open_plaza():
+ global plugin_plaza
+ if plugin_plaza is None or not plugin_plaza.isVisible():
+ plugin_plaza = PluginPlaza()
+ plugin_plaza.show()
+ plugin_plaza.closed.connect(cleanup_plaza)
+ logger.info('打开“插件广场”')
+ else:
+ plugin_plaza.raise_()
+ plugin_plaza.activateWindow()
+
+
+def cleanup_plaza():
+ global plugin_plaza
+ logger.info('关闭“插件广场”')
+ del plugin_plaza
+ plugin_plaza = None
+
+
+def get_timeline():
+ global loaded_data
+ loaded_data = schedule_center.schedule_data
+ return loaded_data['timeline']
+
+
+def open_dir(path: str):
+ if sys.platform.startswith('win32'):
+ os.startfile(path)
+ elif sys.platform.startswith('linux'):
+ subprocess.run(['xdg-open', path])
+ else:
+ msg_box = Dialog(
+ '无法打开文件夹', f'Class Widgets 在您的系统下不支持自动打开文件夹,请手动打开以下地址:\n{path}'
+ )
+ msg_box.yesButton.setText('好')
+ msg_box.cancelButton.hide()
+ msg_box.buttonLayout.insertStretch(0, 1)
+ msg_box.setFixedWidth(550)
+ msg_box.exec()
+
+
+def switch_checked(section, key, checked):
+ if checked:
+ config_center.write_conf(section, key, '1')
+ else:
+ config_center.write_conf(section, key, '0')
+ if key == 'auto_startup':
+ if checked:
+ conf.add_to_startup()
+ else:
+ conf.remove_from_startup()
+
+
+def get_theme_name():
+ theme = config_center.read_conf('General', 'theme')
+ if os.path.exists(f'{base_directory}/ui/{theme}/theme.json'):
+ return theme
+ else:
+ return 'default'
+
+
+def load_schedule_dict(schedule, part, part_name):
+ """
+ 加载课表字典
+ """
+ schedule_dict_ = {}
+ for week, item in schedule.items():
+ all_class = []
+ count = [] # 初始化计数器
+ for i in range(len(part)):
+ count.append(0)
+ if str(week) in loaded_data['timeline'] and loaded_data['timeline'][str(week)]:
+ timeline = get_timeline()[str(week)]
+ else:
+ timeline = get_timeline()['default']
+
+ for item_name, item_time in timeline.items():
+ if item_name.startswith('a'):
+ try:
+ if int(item_name[1]) == 0:
+ count_num = 0
+ else:
+ count_num = sum(count[:int(item_name[1])])
+
+ prefix = item[int(item_name[2:]) - 1 + count_num]
+ period = part_name[str(item_name[1])]
+ all_class.append(f'{prefix}-{period}')
+ except IndexError or ValueError: # 未设置值
+ prefix = '未添加'
+ period = part_name[str(item_name[1])]
+ all_class.append(f'{prefix}-{period}')
+ count[int(item_name[1])] += 1
+ schedule_dict_[week] = all_class
+ return schedule_dict_
+
+
+def convert_to_dict(data_dict_):
+ data_dict = {}
+ for week, item in data_dict_.items():
+ cache_list = item
+ replace_list = []
+ for activity_num in range(len(cache_list)):
+ item_info = cache_list[int(activity_num)].split('-')
+ replace_list.append(item_info[0])
+ data_dict[str(week)] = replace_list
+ return data_dict
+
+
+def se_load_item():
+ global schedule_dict
+ global schedule_even_dict
+ global loaded_data
+ loaded_data = schedule_center.schedule_data
+ part_name = loaded_data.get('part_name')
+ part = loaded_data.get('part')
+ schedule = loaded_data.get('schedule')
+ schedule_even = loaded_data.get('schedule_even')
+
+ schedule_dict = load_schedule_dict(schedule, part, part_name)
+ schedule_even_dict = load_schedule_dict(schedule_even, part, part_name)
+
+
+def cd_load_item():
+ global countdown_dict
+ text = config_center.read_conf('Date', 'cd_text_custom').split(',')
+ date = config_center.read_conf('Date', 'countdown_date').split(',')
+ if len(text) != len(date):
+ countdown_dict = {"Err": f"len(cd_text_custom) (={len(text)}) != len(countdown_date) (={len(date)})"}
+ raise Exception(
+ f"len(cd_text_custom) (={len(text)}) != len(countdown_date) (={len(date)})"f"len(cd_text_custom) (={len(text)}) != len(countdown_date) (={len(date)}) \n 请检查 config.ini [Date] 项!!")
+ countdown_dict = dict(zip(date, text))
+
+
+class selectCity(MessageBoxBase): # 选择城市
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ title_label = SubtitleLabel()
+ subtitle_label = BodyLabel()
+ self.search_edit = SearchLineEdit()
+
+ title_label.setText('搜索城市')
+ subtitle_label.setText('请输入当地城市名进行搜索')
+ self.yesButton.setText('选择此城市') # 按钮组件汉化
+ self.cancelButton.setText('取消')
+
+ self.search_edit.setPlaceholderText('输入城市名')
+ self.search_edit.setClearButtonEnabled(True)
+ self.search_edit.textChanged.connect(self.search_city)
+
+ self.city_list = ListWidget()
+ self.city_list.addItems(wd.search_by_name(''))
+ self.get_selected_city()
+
+ # 将组件添加到布局中
+ self.viewLayout.addWidget(title_label)
+ self.viewLayout.addWidget(subtitle_label)
+ self.viewLayout.addWidget(self.search_edit)
+ self.viewLayout.addWidget(self.city_list)
+ self.widget.setMinimumWidth(500)
+ self.widget.setMinimumHeight(600)
+
+ def search_city(self):
+ self.city_list.clear()
+ self.city_list.addItems(wd.search_by_name(self.search_edit.text()))
+ self.city_list.clearSelection() # 清除选中项
+
+ def get_selected_city(self):
+ selected_city = self.city_list.findItems(
+ wd.search_by_num(str(config_center.read_conf('Weather', 'city'))), QtCore.Qt.MatchFlag.MatchExactly
+ )
+ if selected_city: # 若找到该城市
+ item = selected_city[0]
+ # 选中该项
+ self.city_list.setCurrentItem(item)
+ # 聚焦该项
+ self.city_list.scrollToItem(item)
+
+
+class licenseDialog(MessageBoxBase): # 显示软件许可协议
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ title_label = SubtitleLabel()
+ subtitle_label = BodyLabel()
+ self.license_text = PlainTextEdit()
+
+ title_label.setText('软件许可协议')
+ subtitle_label.setText('此项目 (Class Widgets) 基于 GPL-3.0 许可证授权发布,详情请参阅:')
+ self.yesButton.setText('好') # 按钮组件汉化
+ self.cancelButton.hide()
+ self.buttonLayout.insertStretch(0, 1)
+ self.license_text.setPlainText(open('LICENSE', 'r', encoding='utf-8').read())
+ self.license_text.setReadOnly(True)
+
+ # 将组件添加到布局中
+ self.viewLayout.addWidget(title_label)
+ self.viewLayout.addWidget(subtitle_label)
+ self.viewLayout.addWidget(self.license_text)
+ self.widget.setMinimumWidth(600)
+ self.widget.setMinimumHeight(500)
+
+
+class PluginSettingsDialog(MessageBoxBase): # 插件设置对话框
+ def __init__(self, plugin_dir=None, parent=None):
+ super().__init__(parent)
+ self.plugin_widget = None
+ self.plugin_dir = plugin_dir
+ self.parent = parent
+ self.init_ui()
+
+ def init_ui(self):
+ # 加载已定义的UI
+ self.plugin_widget = p_loader.plugins_settings[self.plugin_dir]
+ self.viewLayout.addWidget(self.plugin_widget)
+ self.viewLayout.setContentsMargins(0, 0, 0, 0)
+
+ self.cancelButton.hide()
+ self.buttonLayout.insertStretch(0, 1)
+
+ self.widget.setMinimumWidth(875)
+ self.widget.setMinimumHeight(625)
+
+
+class PluginCard(CardWidget): # 插件卡片
+ def __init__(
+ self, icon, title='Unknown', content='Unknown', version='1.0.0', plugin_dir='', author=None, parent=None,
+ enable_settings=None
+ ):
+ super().__init__(parent)
+ icon_radius = 5
+ self.plugin_dir = plugin_dir
+ self.title = title
+ self.parent = parent
+
+ self.iconWidget = ImageLabel(icon) # 插件图标
+ self.titleLabel = StrongBodyLabel(title, self) # 插件名
+ self.versionLabel = BodyLabel(version, self) # 插件版本
+ self.authorLabel = BodyLabel(author, self) # 插件作者
+ self.contentLabel = CaptionLabel(content, self) # 插件描述
+ self.enableButton = SwitchButton()
+ self.moreButton = TransparentDropDownToolButton()
+ self.moreMenu = RoundMenu(parent=self.moreButton)
+ self.settingsBtn = TransparentToolButton() # 设置按钮
+ self.settingsBtn.hide()
+
+ self.hBoxLayout = QHBoxLayout(self)
+ self.hBoxLayout_Title = QHBoxLayout(self)
+ self.vBoxLayout = QVBoxLayout(self)
+
+ self.moreMenu.addActions([
+ Action(
+ fIcon.FOLDER, f'打开“{title}”插件文件夹',
+ triggered=lambda: open_dir(os.path.join(base_directory, conf.PLUGINS_DIR, self.plugin_dir))
+ ),
+ Action(
+ fIcon.DELETE, f'卸载“{title}”插件',
+ triggered=self.remove_plugin
+ )
+ ])
+
+ if plugin_dir in enabled_plugins['enabled_plugins']: # 插件是否启用
+ self.enableButton.setChecked(True)
+ if enable_settings:
+ self.moreMenu.addSeparator()
+ self.moreMenu.addAction(Action(fIcon.SETTING, f'“{title}”插件设置', triggered=self.show_settings))
+ self.settingsBtn.show()
+
+ self.setFixedHeight(73)
+ self.iconWidget.setFixedSize(48, 48)
+ self.moreButton.setFixedSize(34, 34)
+ self.iconWidget.setBorderRadius(icon_radius, icon_radius, icon_radius, icon_radius) # 圆角
+ self.contentLabel.setTextColor("#606060", "#d2d2d2")
+ self.contentLabel.setMaximumWidth(500)
+ self.contentLabel.setWordWrap(True) # 自动换行
+ self.versionLabel.setTextColor("#999999", "#999999")
+ self.authorLabel.setTextColor("#606060", "#d2d2d2")
+ self.enableButton.checkedChanged.connect(self.set_enable)
+ self.enableButton.setOffText('禁用')
+ self.enableButton.setOnText('启用')
+ self.moreButton.setMenu(self.moreMenu)
+ self.settingsBtn.setIcon(fIcon.SETTING)
+ self.settingsBtn.clicked.connect(self.show_settings)
+
+ self.hBoxLayout.setContentsMargins(20, 11, 11, 11)
+ self.hBoxLayout.setSpacing(15)
+ self.hBoxLayout.addWidget(self.iconWidget)
+
+ # 内容
+ self.vBoxLayout.setContentsMargins(0, 0, 0, 0)
+ self.vBoxLayout.setSpacing(0)
+ self.vBoxLayout.addLayout(self.hBoxLayout_Title)
+ self.vBoxLayout.addWidget(self.contentLabel, 0, Qt.AlignmentFlag.AlignVCenter)
+ self.vBoxLayout.setAlignment(Qt.AlignmentFlag.AlignVCenter)
+ self.hBoxLayout.addLayout(self.vBoxLayout, 1) # !!!
+
+ # 标题栏
+ self.hBoxLayout_Title.setSpacing(12)
+ self.hBoxLayout_Title.setAlignment(Qt.AlignmentFlag.AlignLeft)
+ self.hBoxLayout_Title.addWidget(self.titleLabel, 0, Qt.AlignmentFlag.AlignVCenter)
+ self.hBoxLayout_Title.addWidget(self.authorLabel, 0, Qt.AlignmentFlag.AlignVCenter)
+ self.hBoxLayout_Title.addWidget(self.versionLabel, 0, Qt.AlignmentFlag.AlignVCenter)
+
+ self.hBoxLayout.addStretch(1)
+ self.hBoxLayout.addWidget(self.settingsBtn, 0, Qt.AlignmentFlag.AlignRight)
+ self.hBoxLayout.addWidget(self.enableButton, 0, Qt.AlignmentFlag.AlignRight)
+ self.hBoxLayout.addWidget(self.moreButton, 0, Qt.AlignmentFlag.AlignRight)
+
+ def set_enable(self):
+ global enabled_plugins
+ if self.enableButton.isChecked():
+ enabled_plugins['enabled_plugins'].append(self.plugin_dir)
+ conf.save_plugin_config(enabled_plugins)
+ else:
+ enabled_plugins['enabled_plugins'].remove(self.plugin_dir)
+ conf.save_plugin_config(enabled_plugins)
+
+ def show_settings(self):
+ w = PluginSettingsDialog(self.plugin_dir, self.parent)
+ w.exec()
+
+ def remove_plugin(self):
+ alert = MessageBox(f"您确定要删除插件“{self.title}”吗?", "删除此插件后,将无法恢复。", self.parent)
+ alert.yesButton.setText('永久删除')
+ alert.yesButton.setStyleSheet("""
+ PushButton{
+ border-radius: 5px;
+ padding: 5px 12px 6px 12px;
+ outline: none;
+ }
+ PrimaryPushButton{
+ color: white;
+ background-color: #FF6167;
+ border: 1px solid #FF8585;
+ border-bottom: 1px solid #943333;
+ }
+ PrimaryPushButton:hover{
+ background-color: #FF7E83;
+ border: 1px solid #FF8084;
+ border-bottom: 1px solid #B13939;
+ }
+ PrimaryPushButton:pressed{
+ color: rgba(255, 255, 255, 0.63);
+ background-color: #DB5359;
+ border: 1px solid #DB5359;
+ }
+ """)
+ alert.cancelButton.setText('我再想想……')
+ if alert.exec():
+ success = p_loader.delete_plugin(self.plugin_dir)
+ if success:
+ try:
+ with open(f'{base_directory}/plugins/plugins_from_pp.json', 'r', encoding='utf-8') as f:
+ installed_data = json.load(f)
+ installed_plugins = installed_data.get('plugins', [])
+ if self.plugin_dir in installed_plugins:
+ installed_plugins.remove(self.plugin_dir)
+ conf.save_installed_plugin(installed_plugins)
+ except Exception as e:
+ logger.error(f"更新已安装插件列表失败: {e}")
+
+ InfoBar.success(
+ title='卸载成功',
+ content=f'插件 “{self.title}” 已卸载。请重启 Class Widgets 以完全移除。',
+ orient=Qt.Horizontal,
+ isClosable=True,
+ position=InfoBarPosition.BOTTOM_RIGHT,
+ duration=5000,
+ parent=self.window()
+ )
+ self.deleteLater() # 删除卡片
+ else:
+ InfoBar.error(
+ title='卸载失败',
+ content=f'卸载插件 “{self.title}” 时出错,请查看日志获取详细信息。',
+ orient=Qt.Horizontal,
+ isClosable=True,
+ position=InfoBarPosition.BOTTOM_RIGHT,
+ duration=5000,
+ parent=self.window()
+ )
+
+
+class TextFieldMessageBox(MessageBoxBase):
+ """ Custom message box """
+
+ def __init__(
+ self, parent=None, title='标题', text='请输入内容', default_text='', enable_check=False):
+ super().__init__(parent)
+ self.fail_color = (QColor('#c42b1c'), QColor('#ff99a4'))
+ self.success_color = (QColor('#0f7b0f'), QColor('#6ccb5f'))
+ self.check_list = enable_check
+
+ self.titleLabel = SubtitleLabel()
+ self.titleLabel.setText(title)
+ self.subtitleLabel = BodyLabel()
+ self.subtitleLabel.setText(text)
+ self.textField = LineEdit()
+ self.tipsLabel = CaptionLabel()
+ self.tipsLabel.setText('')
+ self.yesButton.setText('确定')
+
+ self.fieldLayout = QVBoxLayout()
+ self.textField.setPlaceholderText(default_text)
+ self.textField.setClearButtonEnabled(True)
+ if enable_check:
+ self.textField.textChanged.connect(self.check_text)
+ self.yesButton.setEnabled(False)
+
+ # 将组件添加到布局中
+ self.viewLayout.addWidget(self.titleLabel)
+ self.viewLayout.addWidget(self.subtitleLabel)
+ self.viewLayout.addLayout(self.fieldLayout)
+ self.fieldLayout.addWidget(self.textField)
+ self.fieldLayout.addWidget(self.tipsLabel)
+
+ # 设置对话框的最小宽度
+ self.widget.setMinimumWidth(350)
+
+ def check_text(self):
+ self.tipsLabel.setTextColor(self.fail_color[0], self.fail_color[1])
+ self.yesButton.setEnabled(False)
+ if self.textField.text() == '':
+ self.tipsLabel.setText('不能为空值啊 ( •̀ ω •́ )✧')
+ return
+ if f'{self.textField.text()}.json' in self.check_list:
+ self.tipsLabel.setText('不可以和之前的课程名重复哦 o(TヘTo)')
+ return
+
+ self.yesButton.setEnabled(True)
+ self.tipsLabel.setTextColor(self.success_color[0], self.success_color[1])
+ self.tipsLabel.setText('很好!就这样!ヾ(≧▽≦*)o')
+
+
+class SettingsMenu(FluentWindow):
+ closed = pyqtSignal()
+
+ def __init__(self):
+ super().__init__()
+ self.button_clear_log = None
+ self.version_thread = None
+
+ # 创建子页面
+ self.spInterface = uic.loadUi(f'{base_directory}/view/menu/preview.ui') # 预览
+ self.spInterface.setObjectName("spInterface")
+ self.teInterface = uic.loadUi(f'{base_directory}/view/menu/timeline_edit.ui') # 时间线编辑
+ self.teInterface.setObjectName("teInterface")
+ self.seInterface = uic.loadUi(f'{base_directory}/view/menu/schedule_edit.ui') # 课程表编辑
+ self.seInterface.setObjectName("seInterface")
+ self.cdInterface = uic.loadUi(f'{base_directory}/view/menu/countdown_custom_edit.ui') # 倒计日编辑
+ self.cdInterface.setObjectName("cdInterface")
+ self.adInterface = uic.loadUi(f'{base_directory}/view/menu/advance.ui') # 高级选项
+ self.adInterface.setObjectName("adInterface")
+ self.ifInterface = uic.loadUi(f'{base_directory}/view/menu/about.ui') # 关于
+ self.ifInterface.setObjectName("ifInterface")
+ self.ctInterface = uic.loadUi(f'{base_directory}/view/menu/custom.ui') # 自定义
+ self.ctInterface.setObjectName("ctInterface")
+ self.cfInterface = uic.loadUi(f'{base_directory}/view/menu/configs.ui') # 配置文件
+ self.cfInterface.setObjectName("cfInterface")
+ self.sdInterface = uic.loadUi(f'{base_directory}/view/menu/sound.ui') # 通知
+ self.sdInterface.setObjectName("sdInterface")
+ self.hdInterface = uic.loadUi(f'{base_directory}/view/menu/help.ui') # 帮助
+ self.hdInterface.setObjectName("hdInterface")
+ self.plInterface = uic.loadUi(f'{base_directory}/view/menu/plugin_mgr.ui') # 插件
+ self.plInterface.setObjectName("plInterface")
+
+ self.init_nav()
+ self.init_window()
+
+ def init_font(self): # 设置字体
+ self.setStyleSheet("""QLabel {
+ font-family: 'Microsoft YaHei';
+ }""")
+
+ def load_all_item(self):
+ self.setup_timeline_edit()
+ self.setup_schedule_edit()
+ self.setup_schedule_preview()
+ self.setup_advance_interface()
+ self.setup_about_interface()
+ self.setup_customization_interface()
+ self.setup_configs_interface()
+ self.setup_sound_interface()
+ self.setup_help_interface()
+ self.setup_plugin_mgr_interface()
+ self.setup_countdown_edit()
+
+ # 初始化界面
+ def setup_plugin_mgr_interface(self):
+ pm_scroll = self.findChild(SmoothScrollArea, 'pm_scroll')
+ QScroller.grabGesture(pm_scroll.viewport(), QScroller.LeftMouseButtonGesture) # 触摸屏适配
+
+ global plugin_dict, enabled_plugins
+ enabled_plugins = conf.load_plugin_config() # 加载启用的插件
+ plugin_dict = (conf.load_plugins()) # 加载插件信息
+
+ open_pp = self.findChild(PushButton, 'open_plugin_plaza')
+ open_pp.clicked.connect(open_plaza) # 打开插件广场
+
+ open_pp2 = self.findChild(PushButton, 'open_plugin_plaza_2')
+ open_pp2.clicked.connect(open_plaza) # 打开插件广场
+
+ auto_delay = self.findChild(SpinBox, 'auto_delay')
+ auto_delay.setValue(int(config_center.read_conf('Plugin', 'auto_delay')))
+ auto_delay.valueChanged.connect(
+ lambda: config_center.write_conf('Plugin', 'auto_delay', str(auto_delay.value())))
+ # 设置自动化延迟
+
+ plugin_card_layout = self.findChild(QVBoxLayout, 'plugin_card_layout')
+ open_plugin_folder = self.findChild(PushButton, 'open_plugin_folder')
+ open_plugin_folder.clicked.connect(lambda: open_dir(os.path.join(base_directory, conf.PLUGINS_DIR))) # 打开插件目录
+
+ if not p_loader.plugins_settings: # 若插件设置为空
+ p_loader.load_plugins() # 加载插件设置
+
+ for plugin in plugin_dict:
+ if (Path(conf.PLUGINS_DIR) / plugin / 'icon.png').exists(): # 若插件目录存在icon.png
+ icon_path = f'{base_directory}/plugins/{plugin}/icon.png'
+ else:
+ icon_path = f'{base_directory}/img/settings/plugin-icon.png'
+ card = PluginCard(
+ icon=icon_path,
+ title=plugin_dict[plugin]['name'],
+ version=plugin_dict[plugin]['version'],
+ author=plugin_dict[plugin]['author'],
+ plugin_dir=plugin,
+ content=plugin_dict[plugin]['description'],
+ enable_settings=plugin_dict[plugin]['settings'],
+ parent=self
+ )
+ plugin_card_layout.addWidget(card)
+
+ tips_plugin_empty = self.findChild(QLabel, 'tips_plugin_empty')
+ if plugin_dict:
+ tips_plugin_empty.hide()
+
+ def setup_help_interface(self):
+ open_by_browser = self.findChild(PushButton, 'open_by_browser')
+ open_by_browser.setIcon(fIcon.LINK)
+ open_by_browser.clicked.connect(lambda: QDesktopServices.openUrl(QUrl(
+ 'https://classwidgets.rinlit.cn/docs-user/'
+ )))
+
+ def setup_sound_interface(self):
+ sd_scroll = self.findChild(SmoothScrollArea, 'sd_scroll') # 触摸屏适配
+ QScroller.grabGesture(sd_scroll.viewport(), QScroller.LeftMouseButtonGesture)
+
+ switch_enable_toast = self.findChild(SwitchButton, 'switch_enable_attend')
+ switch_enable_toast.setChecked(int(config_center.read_conf('Toast', 'attend_class')))
+ switch_enable_toast.checkedChanged.connect(lambda checked: switch_checked('Toast', 'attend_class', checked))
+ # 上课提醒开关
+
+ switch_enable_finish = self.findChild(SwitchButton, 'switch_enable_finish')
+ switch_enable_finish.setChecked(int(config_center.read_conf('Toast', 'finish_class')))
+ switch_enable_finish.checkedChanged.connect(lambda checked: switch_checked('Toast', 'finish_class', checked))
+ # 下课提醒开关
+
+ switch_enable_finish = self.findChild(SwitchButton, 'switch_enable_schoolout')
+ switch_enable_finish.setChecked(int(config_center.read_conf('Toast', 'after_school')))
+ switch_enable_finish.checkedChanged.connect(lambda checked: switch_checked('Toast', 'after_school', checked))
+ # 放学提醒开关
+
+ switch_enable_prepare = self.findChild(SwitchButton, 'switch_enable_prepare')
+ switch_enable_prepare.setChecked(int(config_center.read_conf('Toast', 'prepare_class')))
+ switch_enable_prepare.checkedChanged.connect(lambda checked: switch_checked('Toast', 'prepare_class', checked))
+ # 预备铃开关
+
+ switch_enable_pin_toast = self.findChild(SwitchButton, 'switch_enable_pin_toast')
+ switch_enable_pin_toast.setChecked(int(config_center.read_conf('Toast', 'pin_on_top')))
+ switch_enable_pin_toast.checkedChanged.connect(lambda checked: switch_checked('Toast', 'pin_on_top', checked))
+ # 置顶开关
+
+ slider_volume = self.findChild(Slider, 'slider_volume')
+ slider_volume.setValue(int(config_center.read_conf('Audio', 'volume')))
+ slider_volume.valueChanged.connect(self.save_volume) # 音量滑块
+
+ preview_toast_button = self.findChild(PrimaryDropDownPushButton, 'preview')
+
+ pre_toast_menu = RoundMenu(parent=preview_toast_button)
+ pre_toast_menu.addActions([
+ Action(fIcon.EDUCATION, '上课提醒',
+ triggered=lambda: tip_toast.push_notification(1, lesson_name='信息技术')),
+ Action(fIcon.CAFE, '下课提醒',
+ triggered=lambda: tip_toast.push_notification(0, lesson_name='信息技术')),
+ Action(fIcon.BOOK_SHELF, '预备提醒',
+ triggered=lambda: tip_toast.push_notification(3, lesson_name='信息技术')),
+ Action(fIcon.CODE, '其他提醒',
+ triggered=lambda: tip_toast.push_notification(4, title='通知', subtitle='测试通知示例',
+ content='这是一条测试通知ヾ(≧▽≦*)o'))
+ ])
+ preview_toast_button.setMenu(pre_toast_menu) # 预览通知栏
+
+ switch_wave_effect = self.findChild(SwitchButton, 'switch_enable_wave')
+ switch_wave_effect.setChecked(int(config_center.read_conf('Toast', 'wave')))
+ switch_wave_effect.checkedChanged.connect(lambda checked: switch_checked('Toast', 'wave', checked)) # 波纹开关
+
+ spin_prepare_time = self.findChild(SpinBox, 'spin_prepare_class')
+ spin_prepare_time.setValue(int(config_center.read_conf('Toast', 'prepare_minutes')))
+ spin_prepare_time.valueChanged.connect(self.save_prepare_time) # 准备时间
+
+ def setup_configs_interface(self): # 配置界面
+ cf_import_schedule = self.findChild(PushButton, 'im_schedule')
+ cf_import_schedule.clicked.connect(self.cf_import_schedule) # 导入课程表
+ cf_export_schedule = self.findChild(PushButton, 'ex_schedule')
+ cf_export_schedule.clicked.connect(self.cf_export_schedule) # 导出课程表
+ cf_open_schedule_folder = self.findChild(PushButton, 'open_schedule_folder') # 打开课程表文件夹
+ cf_open_schedule_folder.clicked.connect(lambda: open_dir(os.path.join(base_directory, 'config/schedule')))
+
+ cf_import_schedule_cses = self.findChild(PushButton, 'im_schedule_cses')
+ cf_import_schedule_cses.clicked.connect(self.cf_import_schedule_cses) # 导入课程表(CSES)
+ cf_export_schedule_cses = self.findChild(PushButton, 'ex_schedule_cses')
+ cf_export_schedule_cses.clicked.connect(self.cf_export_schedule_cses) # 导出课程表(CSES)
+ cf_what_is_cses = self.findChild(HyperlinkButton, 'what_is')
+ cf_what_is_cses.setUrl(QUrl('https://github.com/CSES-org/CSES'))
+
+ def setup_customization_interface(self):
+ ct_scroll = self.findChild(SmoothScrollArea, 'ct_scroll') # 触摸屏适配
+ QScroller.grabGesture(ct_scroll.viewport(), QScroller.LeftMouseButtonGesture)
+
+ self.ct_update_preview()
+
+ widgets_list_widgets = self.findChild(ListWidget, 'widgets_list')
+ widgets_list = []
+ for key in list_.get_widget_config():
+ try:
+ widgets_list.append(list_.widget_name[key])
+ except KeyError:
+ logger.warning(f'未知的组件:{key}')
+ except Exception as e:
+ logger.error(f'获取组件名称时发生错误:{sys.exc_info()[0]}/{e}')
+ widgets_list_widgets.addItems(widgets_list)
+ widgets_list_widgets.sizePolicy().setVerticalPolicy(QSizePolicy.Policy.MinimumExpanding)
+
+ save_config_button = self.findChild(PrimaryPushButton, 'save_config')
+ save_config_button.clicked.connect(self.ct_save_widget_config)
+
+ set_ac_color = self.findChild(PushButton, 'set_ac_color') # 主题色
+ set_ac_color.clicked.connect(self.ct_set_ac_color)
+ set_fc_color = self.findChild(PushButton, 'set_fc_color')
+ set_fc_color.clicked.connect(self.ct_set_fc_color)
+ set_floating_time_color = self.findChild(PushButton, 'set_fc_color_2')
+ set_floating_time_color.clicked.connect(self.ct_set_floating_time_color)
+
+ open_theme_folder = self.findChild(HyperlinkLabel, 'open_theme_folder') # 打开主题文件夹
+ open_theme_folder.clicked.connect(lambda: open_dir(os.path.join(base_directory, 'ui')))
+
+ select_theme_combo = self.findChild(ComboBox, 'combo_theme_select') # 主题选择
+ select_theme_combo.addItems(list_.theme_names)
+ print(list_.theme_folder, list_.theme_names, get_theme_name())
+ select_theme_combo.setCurrentIndex(list_.theme_folder.index(get_theme_name()))
+ select_theme_combo.currentIndexChanged.connect(
+ lambda: config_center.write_conf('General', 'theme',
+ list_.get_theme_ui_path(select_theme_combo.currentText())))
+
+ color_mode_combo = self.findChild(ComboBox, 'combo_color_mode') # 颜色模式选择
+ color_mode_combo.addItems(list_.color_mode)
+ color_mode_combo.setCurrentIndex(int(config_center.read_conf('General', 'color_mode')))
+ color_mode_combo.currentIndexChanged.connect(self.ct_change_color_mode)
+
+ widgets_combo = self.findChild(ComboBox, 'widgets_combo') # 组件选择
+ widgets_combo.addItems(list_.get_widget_names())
+
+ search_city_button = self.findChild(PushButton, 'select_city') # 查找城市
+ search_city_button.clicked.connect(self.show_search_city)
+
+ add_widget_button = self.findChild(PrimaryPushButton, 'add_widget')
+ add_widget_button.clicked.connect(self.ct_add_widget)
+
+ remove_widget_button = self.findChild(PushButton, 'remove_widget')
+ remove_widget_button.clicked.connect(self.ct_remove_widget)
+
+ slider_opacity = self.findChild(Slider, 'slider_opacity')
+ slider_opacity.setValue(int(config_center.read_conf('General', 'opacity')))
+ slider_opacity.valueChanged.connect(
+ lambda: config_center.write_conf('General', 'opacity', str(slider_opacity.value()))
+ ) # 透明度
+
+ blur_countdown = self.findChild(SwitchButton, 'switch_blur_countdown')
+ blur_countdown.setChecked(int(config_center.read_conf('General', 'blur_countdown')))
+ blur_countdown.checkedChanged.connect(lambda checked: switch_checked('General', 'blur_countdown', checked))
+ # 模糊倒计时
+ switch_blur_floating = self.findChild(SwitchButton, 'switch_blur_countdown_2')
+ switch_blur_floating.setChecked(int(config_center.read_conf('General', 'blur_floating_countdown')))
+ switch_blur_floating.checkedChanged.connect(
+ lambda checked: config_center.write_conf('General', 'blur_floating_countdown', int(checked))
+ )
+
+ select_weather_api = self.findChild(ComboBox, 'select_weather_api') # 天气API选择
+ select_weather_api.addItems(weather_db.api_config['weather_api_list_zhCN'])
+ select_weather_api.setCurrentIndex(weather_db.api_config['weather_api_list'].index(
+ config_center.read_conf('Weather', 'api')
+ ))
+ select_weather_api.currentIndexChanged.connect(
+ lambda: config_center.write_conf('Weather', 'api',
+ weather_db.api_config['weather_api_list'][
+ select_weather_api.currentIndex()])
+ )
+
+ api_key_edit = self.findChild(LineEdit, 'api_key_edit') # API密钥
+ api_key_edit.setText(config_center.read_conf('Weather', 'api_key'))
+ api_key_edit.textChanged.connect(lambda: config_center.write_conf('Weather', 'api_key', api_key_edit.text()))
+
+ def setup_about_interface(self):
+ ab_scroll = self.findChild(SmoothScrollArea, 'ab_scroll') # 触摸屏适配
+ QScroller.grabGesture(ab_scroll.viewport(), QScroller.LeftMouseButtonGesture)
+
+ self.version = self.findChild(BodyLabel, 'version')
+
+ check_update_btn = self.findChild(PrimaryPushButton, 'check_update')
+ check_update_btn.setIcon(fIcon.SYNC)
+ check_update_btn.clicked.connect(self.check_update)
+
+ self.auto_check_update = self.ifInterface.findChild(SwitchButton, 'auto_check_update')
+ self.auto_check_update.setChecked(int(config_center.read_conf("Other", "auto_check_update")))
+ self.auto_check_update.checkedChanged.connect(
+ lambda checked: switch_checked("Other", "auto_check_update", checked)
+ ) # 自动检查更新
+
+ self.version_channel = self.findChild(ComboBox, 'version_channel')
+ self.version_channel.addItems(list_.version_channel)
+ self.version_channel.setCurrentIndex(int(config_center.read_conf("Other", "version_channel")))
+ self.version_channel.currentIndexChanged.connect(
+ lambda: config_center.write_conf("Other", "version_channel", self.version_channel.currentIndex())
+ ) # 版本更新通道
+
+ github_page = self.findChild(PushButton, "button_github")
+ github_page.clicked.connect(lambda: QDesktopServices.openUrl(QUrl(
+ 'https://github.com/RinLit-233-shiroko/Class-Widgets')))
+
+ bilibili_page = self.findChild(PushButton, 'button_bilibili')
+ bilibili_page.clicked.connect(lambda: QDesktopServices.openUrl(QUrl(
+ 'https://space.bilibili.com/569522843')))
+
+ license_button = self.findChild(PushButton, 'button_show_license')
+ license_button.clicked.connect(self.show_license)
+
+ thanks_button = self.findChild(PushButton, 'button_thanks')
+ thanks_button.clicked.connect(lambda: QDesktopServices.openUrl(QUrl(
+ 'https://github.com/RinLit-233-shiroko/Class-Widgets?tab=readme-ov-file#致谢')))
+
+ self.check_update()
+
+ def setup_advance_interface(self):
+ adv_scroll = self.adInterface.findChild(SmoothScrollArea, 'adv_scroll') # 触摸屏适配
+ QScroller.grabGesture(adv_scroll.viewport(), QScroller.LeftMouseButtonGesture)
+
+ margin_spin = self.adInterface.findChild(SpinBox, 'margin_spin')
+ margin_spin.setValue(int(config_center.read_conf('General', 'margin')))
+ margin_spin.valueChanged.connect(
+ lambda: config_center.write_conf('General', 'margin', str(margin_spin.value()))
+ ) # 保存边距设定
+
+ self.conf_combo = self.adInterface.findChild(ComboBox, 'conf_combo')
+ self.conf_combo.clear()
+ self.conf_combo.addItems(list_.get_schedule_config())
+ self.conf_combo.setCurrentIndex(
+ list_.get_schedule_config().index(config_center.read_conf('General', 'schedule')))
+ self.conf_combo.currentIndexChanged.connect(self.ad_change_file) # 切换配置文件
+
+ conf_name = self.adInterface.findChild(LineEdit, 'conf_name')
+ conf_name.setText(config_center.schedule_name[:-5])
+ conf_name.textEdited.connect(self.ad_change_file_name)
+
+ window_status_combo = self.adInterface.findChild(ComboBox, 'window_status_combo')
+ window_status_combo.addItems(list_.window_status)
+ window_status_combo.setCurrentIndex(int(config_center.read_conf('General', 'pin_on_top')))
+ window_status_combo.currentIndexChanged.connect(
+ lambda: config_center.write_conf('General', 'pin_on_top', str(window_status_combo.currentIndex()))
+ ) # 窗口状态
+
+ switch_startup = self.adInterface.findChild(SwitchButton, 'switch_startup')
+ switch_startup.setChecked(int(config_center.read_conf('General', 'auto_startup')))
+ switch_startup.checkedChanged.connect(lambda checked: switch_checked('General', 'auto_startup', checked))
+ # 开机自启
+ if os.name != 'nt':
+ switch_startup.setEnabled(False)
+
+ hide_mode_combo = self.adInterface.findChild(ComboBox, 'hide_mode_combo')
+ hide_mode_combo.addItems(list_.hide_mode if os.name == 'nt' else list_.non_nt_hide_mode)
+ hide_mode_combo.setCurrentIndex(int(config_center.read_conf('General', 'hide')))
+ hide_mode_combo.currentIndexChanged.connect(
+ lambda: config_center.write_conf('General', 'hide', str(hide_mode_combo.currentIndex()))
+ ) # 隐藏模式
+
+ hide_method_default = self.adInterface.findChild(RadioButton, 'hide_method_default')
+ hide_method_default.setChecked(config_center.read_conf('General', 'hide_method') == '0')
+ hide_method_default.toggled.connect(lambda: config_center.write_conf('General', 'hide_method', '0'))
+ if os.name != 'nt':
+ hide_method_default.setEnabled(False)
+ # 默认隐藏
+
+ hide_method_all = self.adInterface.findChild(RadioButton, 'hide_method_all')
+ hide_method_all.setChecked(config_center.read_conf('General', 'hide_method') == '1')
+ hide_method_all.toggled.connect(lambda: config_center.write_conf('General', 'hide_method', '1'))
+ # 单击全部隐藏
+
+ hide_method_floating = self.adInterface.findChild(RadioButton, 'hide_method_floating')
+ hide_method_floating.setChecked(config_center.read_conf('General', 'hide_method') == '2')
+ hide_method_floating.toggled.connect(lambda: config_center.write_conf('General', 'hide_method', '2'))
+ # 最小化为浮窗
+
+ switch_enable_exclude = self.adInterface.findChild(SwitchButton, 'switch_exclude_startup')
+ switch_enable_exclude.setChecked(int(config_center.read_conf('General', 'excluded_lesson')))
+ switch_enable_exclude.checkedChanged.connect(
+ lambda checked: switch_checked('General', 'excluded_lesson', checked))
+ # 允许排除课程
+
+ exclude_lesson = self.adInterface.findChild(LineEdit, 'excluded_lessons')
+ exclude_lesson.setText(config_center.read_conf('General', 'excluded_lessons'))
+ exclude_lesson.textChanged.connect(
+ lambda: config_center.write_conf('General', 'excluded_lessons', exclude_lesson.text()))
+ # 排除课程
+
+ switch_enable_click = self.adInterface.findChild(SwitchButton, 'switch_enable_click')
+ switch_enable_click.setChecked(int(config_center.read_conf('General', 'enable_click')))
+ switch_enable_click.checkedChanged.connect(lambda checked: switch_checked('General', 'enable_click', checked))
+ # 允许点击
+
+ switch_enable_alt_schedule = self.adInterface.findChild(SwitchButton, 'switch_enable_alt_schedule')
+ switch_enable_alt_schedule.setChecked(int(config_center.read_conf('General', 'enable_alt_schedule')))
+ switch_enable_alt_schedule.checkedChanged.connect(
+ lambda checked: switch_checked('General', 'enable_alt_schedule', checked)
+ ) # 安全模式
+
+ switch_enable_safe_mode = self.adInterface.findChild(SwitchButton, 'switch_safe_mode')
+ switch_enable_safe_mode.setChecked(int(config_center.read_conf('Other', 'safe_mode')))
+ switch_enable_safe_mode.checkedChanged.connect(
+ lambda checked: switch_checked('Other', 'safe_mode', checked)
+ )
+ # 安全模式开关
+
+ switch_enable_multiple_programs = self.adInterface.findChild(SwitchButton, 'switch_multiple_programs')
+ switch_enable_multiple_programs.setChecked(int(config_center.read_conf('Other', 'multiple_programs')))
+ switch_enable_multiple_programs.checkedChanged.connect(
+ lambda checked: switch_checked('Other', 'multiple_programs', checked)
+ ) # 多开程序
+
+ switch_disable_log = self.adInterface.findChild(SwitchButton, 'switch_disable_log')
+ switch_disable_log.setChecked(int(config_center.read_conf('Other', 'do_not_log')))
+ switch_disable_log.checkedChanged.connect(
+ lambda checked: switch_checked('Other', 'do_not_log', checked)
+ ) # 禁用日志
+
+ button_clear_log = self.adInterface.findChild(PushButton, 'button_clear_log')
+ button_clear_log.clicked.connect(self.clear_log) # 清空日志
+
+ set_start_date = self.adInterface.findChild(CalendarPicker, 'set_start_date') # 日期
+ if config_center.read_conf('Date', 'start_date') != '':
+ set_start_date.setDate(QDate.fromString(config_center.read_conf('Date', 'start_date'), 'yyyy-M-d'))
+ set_start_date.dateChanged.connect(
+ lambda: config_center.write_conf('Date', 'start_date', set_start_date.date.toString('yyyy-M-d'))) # 开学日期
+
+ offset_spin = self.adInterface.findChild(SpinBox, 'offset_spin')
+ offset_spin.setValue(int(config_center.read_conf('General', 'time_offset')))
+ offset_spin.valueChanged.connect(
+ lambda: config_center.write_conf('General', 'time_offset', str(offset_spin.value()))
+ ) # 保存时差偏移
+
+ text_scale_factor = self.adInterface.findChild(LineEdit, 'text_scale_factor')
+ text_scale_factor.setText(str(float(config_center.read_conf('General', 'scale')) * 100) + '%') # 初始化缩放系数显示
+
+ slider_scale_factor = self.adInterface.findChild(Slider, 'slider_scale_factor')
+ slider_scale_factor.setValue(int(float(config_center.read_conf('General', 'scale')) * 100))
+ slider_scale_factor.valueChanged.connect(
+ lambda: (config_center.write_conf('General', 'scale', str(slider_scale_factor.value() / 100)),
+ text_scale_factor.setText(str(slider_scale_factor.value()) + '%'))
+ ) # 保存缩放系数
+
+ what_is_hide_mode_3 = self.adInterface.findChild(HyperlinkLabel, 'what_is_hide_mode_3')
+
+ def what_is_hide_mode_3_clicked():
+ w = MessageBox('灵活模式', '灵活模式为上课时自动隐藏,可手动改变隐藏状态,当前课程状态(上课/课间)改变后会清除手动隐藏状态,重新转为自动隐藏。', self)
+ w.cancelButton.hide()
+ w.exec()
+ what_is_hide_mode_3.clicked.connect(what_is_hide_mode_3_clicked)
+
+ def setup_schedule_edit(self):
+ se_load_item()
+ se_set_button = self.findChild(ToolButton, 'set_button')
+ se_set_button.setIcon(fIcon.EDIT)
+ se_set_button.setToolTip('编辑课程')
+ se_set_button.installEventFilter(ToolTipFilter(se_set_button, showDelay=300, position=ToolTipPosition.TOP))
+ se_set_button.clicked.connect(self.se_edit_item)
+
+ se_clear_button = self.findChild(ToolButton, 'clear_button')
+ se_clear_button.setIcon(fIcon.DELETE)
+ se_clear_button.setToolTip('清空课程')
+ se_clear_button.installEventFilter(ToolTipFilter(se_clear_button, showDelay=300, position=ToolTipPosition.TOP))
+ se_clear_button.clicked.connect(self.se_delete_item)
+
+ se_class_kind_combo = self.findChild(ComboBox, 'class_combo') # 课程类型
+ se_class_kind_combo.addItems(list_.class_kind)
+
+ se_week_combo = self.findChild(ComboBox, 'week_combo') # 星期
+ se_week_combo.addItems(list_.week)
+ se_week_combo.currentIndexChanged.connect(self.se_upload_list)
+
+ se_schedule_list = self.findChild(ListWidget, 'schedule_list')
+ se_schedule_list.addItems(schedule_dict[str(current_week)])
+ se_schedule_list.itemChanged.connect(self.se_upload_item)
+ QScroller.grabGesture(se_schedule_list.viewport(), QScroller.LeftMouseButtonGesture) # 触摸屏适配
+
+ se_save_button = self.findChild(PrimaryPushButton, 'save_schedule')
+ se_save_button.clicked.connect(self.se_save_item)
+
+ se_week_type_combo = self.findChild(ComboBox, 'week_type_combo')
+ se_week_type_combo.addItems(list_.week_type)
+ se_week_type_combo.currentIndexChanged.connect(self.se_upload_list)
+
+ se_copy_schedule_button = self.findChild(PushButton, 'copy_schedule')
+ se_copy_schedule_button.hide()
+ se_copy_schedule_button.clicked.connect(self.se_copy_odd_schedule)
+
+ quick_set_schedule = self.findChild(ListWidget, 'subject_list')
+ quick_set_schedule.addItems(list_.class_kind[1:])
+ quick_set_schedule.itemClicked.connect(self.se_quick_set_schedule)
+
+ quick_select_week_button = self.findChild(PushButton, 'quick_select_week')
+ quick_select_week_button.clicked.connect(self.se_quick_select_week)
+
+ def setup_timeline_edit(self): # 底层大改
+ self.te_load_item() # 加载时段
+ # teInterface
+ te_add_button = self.findChild(ToolButton, 'add_button') # 添加
+ te_add_button.setIcon(fIcon.ADD)
+ te_add_button.setToolTip('添加时间线') # 增加提示
+ te_add_button.installEventFilter(ToolTipFilter(te_add_button, showDelay=300, position=ToolTipPosition.TOP))
+ te_add_button.clicked.connect(self.te_add_item)
+ te_add_button.clicked.connect(self.te_upload_item)
+
+ te_add_part_button = self.findChild(ToolButton, 'add_part_button') # 添加节点
+ te_add_part_button.setIcon(fIcon.ADD)
+ te_add_part_button.setToolTip('添加节点')
+ te_add_part_button.installEventFilter(
+ ToolTipFilter(te_add_part_button, showDelay=300, position=ToolTipPosition.TOP))
+ te_add_part_button.clicked.connect(self.te_add_part)
+
+ te_part_type_combo = self.findChild(ComboBox, 'part_type') # 节次类型
+ te_part_type_combo.clear()
+ te_part_type_combo.addItems(list_.part_type)
+
+ te_name_edit = self.findChild(EditableComboBox, 'name_part_combo') # 名称
+ te_name_edit.addItems(list_.time)
+
+ te_delete_part_button = self.findChild(ToolButton, 'delete_part_button') # 删除节点
+ te_delete_part_button.setIcon(fIcon.DELETE)
+ te_delete_part_button.setToolTip('删除节点')
+ te_delete_part_button.installEventFilter(
+ ToolTipFilter(te_delete_part_button, showDelay=300, position=ToolTipPosition.TOP))
+ te_delete_part_button.clicked.connect(self.te_delete_part)
+
+ te_edit_button = self.findChild(ToolButton, 'edit_button') # 编辑
+ te_edit_button.setIcon(fIcon.EDIT)
+ te_edit_button.setToolTip('编辑时间线')
+ te_edit_button.installEventFilter(ToolTipFilter(te_edit_button, showDelay=300, position=ToolTipPosition.TOP))
+ te_edit_button.clicked.connect(self.te_edit_item)
+
+ te_delete_button = self.findChild(ToolButton, 'delete_button') # 删除
+ te_delete_button.setIcon(fIcon.DELETE)
+ te_delete_button.setToolTip('删除时间线')
+ te_delete_button.installEventFilter(
+ ToolTipFilter(te_delete_button, showDelay=300, position=ToolTipPosition.TOP))
+ te_delete_button.clicked.connect(self.te_delete_item)
+ te_delete_button.clicked.connect(self.te_upload_item)
+
+ te_class_activity_combo = self.findChild(ComboBox, 'class_activity') # 活动类型
+ te_class_activity_combo.addItems(list_.class_activity)
+ te_class_activity_combo.setToolTip('选择活动类型(“课程”或“课间”)')
+ te_class_activity_combo.currentIndexChanged.connect(self.te_sync_time)
+
+ te_select_timeline = self.findChild(ComboBox, 'select_timeline') # 选择时间线
+ te_select_timeline.addItem('默认')
+ te_select_timeline.addItems(list_.week)
+ te_select_timeline.setToolTip('选择一周内的某一天的时间线')
+ te_select_timeline.currentIndexChanged.connect(self.te_upload_list)
+
+ te_timeline_list = self.findChild(ListWidget, 'timeline_list') # 所选时间线列表
+ te_timeline_list.addItems(timeline_dict['default'])
+ te_timeline_list.itemChanged.connect(self.te_upload_item)
+
+ te_part_time = self.teInterface.findChild(TimeEdit, 'part_time') # 节次时间
+ te_part_time.timeChanged.connect(
+ lambda: self.show_tip_flyout('重要提示', '请使用 24 小时制', te_part_time)
+ )
+
+ te_save_button = self.findChild(PrimaryPushButton, 'save') # 保存
+ te_save_button.clicked.connect(self.te_save_item)
+
+ part_list = self.findChild(ListWidget, 'part_list')
+ QScroller.grabGesture(te_timeline_list.viewport(), QScroller.LeftMouseButtonGesture) # 触摸屏适配
+ QScroller.grabGesture(part_list.viewport(), QScroller.LeftMouseButtonGesture) # 触摸屏适配
+ self.te_detect_item()
+ self.te_update_parts_name() # 修复在启动时无法添加时段到下拉框的问题
+
+ def setup_schedule_preview(self):
+ subtitle = self.findChild(SubtitleLabel, 'subtitle_file')
+ subtitle.setText(f'预览 - {config_center.schedule_name[:-5]}')
+
+ schedule_view = self.findChild(TableWidget, 'schedule_view')
+ schedule_view.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) # 使列表自动等宽
+
+ sp_week_type_combo = self.findChild(ComboBox, 'pre_week_type_combo')
+ sp_week_type_combo.addItems(list_.week_type)
+ sp_week_type_combo.currentIndexChanged.connect(self.sp_fill_grid_row)
+
+ # 设置表格
+ schedule_view.setColumnCount(7)
+ schedule_view.setHorizontalHeaderLabels(list_.week[0:7])
+ schedule_view.setBorderVisible(True)
+ schedule_view.verticalHeader().hide()
+ schedule_view.setBorderRadius(8)
+ QScroller.grabGesture(schedule_view.viewport(), QScroller.LeftMouseButtonGesture) # 触摸屏适配
+ self.sp_fill_grid_row()
+
+ def save_volume(self):
+ slider_volume = self.findChild(Slider, 'slider_volume')
+ config_center.write_conf('Audio', 'volume', str(slider_volume.value()))
+
+ def show_search_city(self):
+ search_city_dialog = selectCity(self)
+ if search_city_dialog.exec():
+ selected_city = search_city_dialog.city_list.selectedItems()
+ if selected_city:
+ config_center.write_conf('Weather', 'city', wd.search_code_by_name((selected_city[0].text(),'')))
+
+ def show_license(self):
+ license_dialog = licenseDialog(self)
+ license_dialog.exec()
+
+ def save_prepare_time(self):
+ prepare_time_spin = self.findChild(SpinBox, 'spin_prepare_class')
+ config_center.write_conf('Toast', 'prepare_minutes', str(prepare_time_spin.value()))
+
+ def clear_log(self): # 清空日志
+ def get_directory_size(path): # 计算目录大小
+ total_size = 0
+ for dir_path, dir_names, filenames in os.walk(path):
+ for file_name in filenames:
+ file_path = os.path.join(dir_path, file_name)
+ total_size += os.path.getsize(file_path)
+ total_size /= 1024
+ return round(total_size, 2)
+
+ self.button_clear_log = self.adInterface.findChild(PushButton, 'button_clear_log')
+ size = get_directory_size('log')
+
+ try:
+ if os.path.exists('log'):
+ rmtree('log')
+ Flyout.create(
+ icon=InfoBarIcon.SUCCESS,
+ title='已清除日志',
+ content=f"已清空所有日志文件,约 {size} KB",
+ target=self.button_clear_log,
+ parent=self,
+ isClosable=True,
+ aniType=FlyoutAnimationType.PULL_UP
+ )
+ else:
+ Flyout.create(
+ icon=InfoBarIcon.INFORMATION,
+ title='未找到日志',
+ content="日志目录下为空,已清理完成。",
+ target=self.button_clear_log,
+ parent=self,
+ isClosable=True,
+ aniType=FlyoutAnimationType.PULL_UP
+ )
+ except OSError: # 遇到程序正在使用的log,忽略
+ Flyout.create(
+ icon=InfoBarIcon.SUCCESS,
+ title='已清除日志',
+ content=f"已清空所有日志文件,约 {size} KB",
+ target=self.button_clear_log,
+ parent=self,
+ isClosable=True,
+ aniType=FlyoutAnimationType.PULL_UP
+ )
+ except Exception as e:
+ Flyout.create(
+ icon=InfoBarIcon.ERROR,
+ title='清除日志失败!',
+ content=f"清除日志失败:{e}",
+ target=self.button_clear_log,
+ parent=self,
+ isClosable=True,
+ aniType=FlyoutAnimationType.PULL_UP
+ )
+
+ def ct_change_color_mode(self):
+ color_mode_combo = self.findChild(ComboBox, 'combo_color_mode')
+ config_center.write_conf('General', 'color_mode', str(color_mode_combo.currentIndex()))
+ if color_mode_combo.currentIndex() == 0:
+ tg_theme = Theme.LIGHT
+ elif color_mode_combo.currentIndex() == 1:
+ tg_theme = Theme.DARK
+ else:
+ tg_theme = Theme.AUTO
+ setTheme(tg_theme)
+ self.ct_update_preview()
+
+ def ct_add_widget(self):
+ widgets_list = self.findChild(ListWidget, 'widgets_list')
+ widgets_combo = self.findChild(ComboBox, 'widgets_combo')
+ if (not widgets_list.findItems(widgets_combo.currentText(), QtCore.Qt.MatchFlag.MatchExactly)) or widgets_combo.currentText() in list_.native_widget_name:
+ widgets_list.addItem(widgets_combo.currentText())
+ self.ct_update_preview()
+
+ def ct_remove_widget(self):
+ widgets_list = self.findChild(ListWidget, 'widgets_list')
+ if widgets_list.count() > 2:
+ widgets_list.takeItem(widgets_list.currentRow())
+ self.ct_update_preview()
+ else:
+ w = MessageBox('无法删除', '至少需要保留两个小组件。', self)
+ w.cancelButton.hide() # 隐藏取消按钮
+ w.buttonLayout.insertStretch(0, 1)
+ w.exec()
+
+ def ct_set_ac_color(self):
+ current_color = QColor(f'#{config_center.read_conf("Color", "attend_class")}')
+ w = ColorDialog(current_color, "更改上课时主题色", self, enableAlpha=False)
+ w.colorChanged.connect(lambda color: config_center.write_conf('Color', 'attend_class', color.name()[1:]))
+ w.exec()
+
+ def ct_set_fc_color(self):
+ current_color = QColor(f'#{config_center.read_conf("Color", "finish_class")}')
+ w = ColorDialog(current_color, "更改课间时主题色", self, enableAlpha=False)
+ w.colorChanged.connect(lambda color: config_center.write_conf('Color', 'finish_class', color.name()[1:]))
+ w.exec()
+
+ def ct_set_floating_time_color(self):
+ current_color = QColor(f'#{config_center.read_conf("Color", "floating_time")}')
+ w = ColorDialog(current_color, "更改浮窗时间颜色", self, enableAlpha=False)
+ w.colorChanged.connect(lambda color: config_center.write_conf('Color', 'floating_time', color.name()[1:]))
+ w.exec()
+ self.ct_update_preview()
+
+ def cf_export_schedule(self): # 导出课程表
+ file_path, _ = QFileDialog.getSaveFileName(self, "保存文件", config_center.schedule_name,
+ "Json 配置文件 (*.json)")
+ if file_path:
+ if list_.export_schedule(file_path, config_center.schedule_name):
+ alert = MessageBox('您已成功导出课程表配置文件',
+ f'文件将导出于{file_path}', self)
+ alert.cancelButton.hide()
+ alert.buttonLayout.insertStretch(0, 1)
+ if alert.exec():
+ return 0
+ else:
+ print('导出失败!')
+ alert = MessageBox('导出失败!',
+ '课程表文件导出失败,\n'
+ '可能为文件损坏,请将此情况反馈给开发者。', self)
+ alert.cancelButton.hide()
+ alert.buttonLayout.insertStretch(0, 1)
+ if alert.exec():
+ return 0
+
+ def check_update(self):
+ self.version.setText(f'当前版本:{config_center.read_conf("Other", "version")}\n正在检查最新版本…')
+ self.version_thread = VersionThread()
+ self.version_thread.version_signal.connect(self.check_version)
+ self.version_thread.start()
+
+ def check_version(self, version): # 检查更新
+ if 'error' in version:
+ self.version.setText(f'当前版本:{config_center.read_conf("Other", "version")}\n{version["error"]}')
+
+ if utils.tray_icon:
+ utils.tray_icon.push_error_notification(
+ "检查更新失败!",
+ f"检查更新失败!\n{version['error']}"
+ )
+ return False
+
+ channel = int(config_center.read_conf("Other", "version_channel"))
+ new_version = version['version_release' if channel == 0 else 'version_beta']
+ local_version = config_center.read_conf("Other", "version")
+
+ logger.debug(f"服务端版本: {Version(new_version)},本地版本: {Version(local_version)}")
+ if Version(new_version) <= Version(local_version):
+ self.version.setText(f'当前版本:{local_version}\n当前为最新版本')
+ else:
+ self.version.setText(f'当前版本:{local_version}\n最新版本:{new_version}')
+
+ if utils.tray_icon:
+ utils.tray_icon.push_update_notification(f"新版本速递:{new_version}")
+
+ def cf_import_schedule_cses(self): # 导入课程表(CSES)
+ file_path, _ = QFileDialog.getOpenFileName(self, "选择文件", "", "CSES 通用课程表交换文件 (*.yaml)")
+ if file_path:
+ file_name = file_path.split("/")[-1]
+ save_path = f"{base_directory}/config/schedule/{file_name.replace('.yaml', '.json')}"
+
+ print(save_path)
+ importer = CSES_Converter(file_path)
+ importer.load_parser()
+ cw_data = importer.convert_to_cw()
+ if not cw_data:
+ alert = MessageBox('转换失败!',
+ '课程表文件转换失败!\n'
+ '可能为格式错误或文件损坏,请检查此文件是否为正确的 CSES 课程表文件。\n'
+ '详情请查看Log日志,日志位于./log/下。', self)
+ alert.cancelButton.hide() # 隐藏取消按钮
+ alert.buttonLayout.insertStretch(0, 1)
+ alert.exec()
+ try:
+ with open(save_path, 'w', encoding='utf-8') as f:
+ json.dump(cw_data, f, ensure_ascii=False, indent=4)
+ self.conf_combo.addItem(file_name.replace('.yaml', '.json'))
+ alert = MessageBox('您已成功导入 CSES 课程表配置文件',
+ '请在“高级选项”中手动切换您的配置文件。', self)
+ alert.cancelButton.hide()
+ alert.buttonLayout.insertStretch(0, 1)
+ alert.exec()
+ except Exception as e:
+ logger.error(f'导入课程表时发生错误:{e}')
+ alert = MessageBox('导入失败!',
+ '课程表文件导入失败!\n'
+ '可能为格式错误或文件损坏,请检查此文件是否为正确的 CSES 课程表文件。\n'
+ '详情请查看Log日志,日志位于./log/下。', self)
+ alert.cancelButton.hide() # 隐藏取消按钮
+ alert.buttonLayout.insertStretch(0, 1)
+ alert.exec()
+
+ def cf_export_schedule_cses(self): # 导出课程表(CSES)
+ file_path, _ = QFileDialog.getSaveFileName(
+ self, "保存文件", config_center.schedule_name.replace('.json', '.yaml'), "CSES 通用课程表交换文件 (*.yaml)")
+ if file_path:
+ exporter = CSES_Converter(file_path)
+ exporter.load_generator()
+ if exporter.convert_to_cses(cw_path=f'{base_directory}/config/schedule/{config_center.schedule_name}'):
+ alert = MessageBox('您已成功导出课程表配置文件',
+ f'文件将导出于{file_path}', self)
+ alert.cancelButton.hide()
+ alert.buttonLayout.insertStretch(0, 1)
+ if alert.exec():
+ return 0
+ else:
+ print('导出失败!')
+ alert = MessageBox('导出失败!',
+ '课程表文件导出失败,\n'
+ '可能为文件损坏,请将此情况反馈给开发者。', self)
+ alert.cancelButton.hide()
+ alert.buttonLayout.insertStretch(0, 1)
+ if alert.exec():
+ return 0
+
+ def cf_import_schedule(self): # 导入课程表
+ file_path, _ = QFileDialog.getOpenFileName(self, "选择文件", "", "Json 配置文件 (*.json)")
+ if file_path:
+ file_name = file_path.split("/")[-1]
+ if list_.import_schedule(file_path, file_name):
+ self.conf_combo.addItem(file_name)
+ alert = MessageBox('您已成功导入课程表配置文件',
+ '请在“高级选项”中手动切换您的配置文件。', self)
+ alert.cancelButton.hide() # 隐藏取消按钮,必须重启
+ alert.buttonLayout.insertStretch(0, 1)
+ else:
+ print('导入失败!')
+ alert = MessageBox('导入失败!',
+ '课程表文件导入失败!\n'
+ '可能为格式错误或文件损坏,请检查此文件是否为 Class Widgets 课程表文件。\n'
+ '详情请查看Log日志,日志位于./log/下。', self)
+ alert.cancelButton.hide() # 隐藏取消按钮
+ alert.buttonLayout.insertStretch(0, 1)
+ if alert.exec():
+ return 0
+
+ def ct_save_widget_config(self):
+ widgets_list = self.findChild(ListWidget, 'widgets_list')
+ widget_config = {'widgets': []}
+ for i in range(widgets_list.count()):
+ widget_config['widgets'].append(list_.widget_conf[widgets_list.item(i).text()])
+ if conf.save_widget_conf_to_json(widget_config):
+ self.ct_update_preview()
+ Flyout.create(
+ icon=InfoBarIcon.SUCCESS,
+ title='保存成功',
+ content=f"已保存至 ./config/widget.json",
+ target=self.findChild(PrimaryPushButton, 'save_config'),
+ parent=self,
+ isClosable=True,
+ aniType=FlyoutAnimationType.PULL_UP
+ )
+
+ def ct_update_preview(self):
+ try:
+ widgets_preview = self.findChild(QHBoxLayout, 'widgets_preview')
+ # 获取配置列表
+ widget_config = list_.get_widget_config()
+ while widgets_preview.count() > 0: # 清空预览界面
+ item = widgets_preview.itemAt(0)
+ if item:
+ widget = item.widget()
+ if widget:
+ widget.deleteLater()
+ widgets_preview.removeItem(item)
+
+ left_spacer = QSpacerItem(20, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
+ widgets_preview.addItem(left_spacer)
+
+ theme_folder = config_center.read_conf("General", "theme")
+ if not os.path.exists(f'{base_directory}/ui/{theme_folder}/theme.json'):
+ theme_folder = 'default' # 主题文件夹不存在,使用默认主题
+ logger.warning(f'主题文件夹不存在,使用默认主题:{theme_folder}')
+
+ for i in range(len(widget_config)):
+ widget_name = widget_config[i]
+ if isDarkTheme() and conf.load_theme_config(theme_folder)['support_dark_mode']:
+ if os.path.exists(f'{base_directory}/ui/{theme_folder}/dark/preview/{widget_name[:-3]}.png'):
+ path = f'{base_directory}/ui/{theme_folder}/dark/preview/{widget_name[:-3]}.png'
+ else:
+ path = f'{base_directory}/ui/{theme_folder}/dark/preview/widget-custom.png'
+ else:
+ if os.path.exists(f'ui/{theme_folder}/preview/{widget_name[:-3]}.png'):
+ path = f'{base_directory}/ui/{theme_folder}/preview/{widget_name[:-3]}.png'
+ else:
+ path = f'{base_directory}/ui/{theme_folder}/preview/widget-custom.png'
+
+ label = ImageLabel()
+ label.setImage(path)
+ widgets_preview.addWidget(label)
+ widget_config[i] = label
+ right_spacer = QSpacerItem(20, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
+ widgets_preview.addItem(right_spacer)
+ except Exception as e:
+ logger.error(f'更新预览界面时发生错误:{e}')
+
+ def ad_change_file_name(self):
+ try:
+ conf_name = self.findChild(LineEdit, 'conf_name')
+ old_name = config_center.schedule_name
+ new_name = conf_name.text()
+ os.rename(f'{base_directory}/config/schedule/{old_name}',
+ f'{base_directory}/config/schedule/{new_name}.json') # 重命名
+ config_center.write_conf('General', 'schedule', f'{new_name}.json')
+ config_center.schedule_name = new_name + '.json'
+ conf_combo = self.findChild(ComboBox, 'conf_combo')
+ conf_combo.clear()
+ conf_combo.addItems(list_.get_schedule_config())
+ conf_combo.setCurrentIndex(list_.get_schedule_config().index(f'{new_name}.json'))
+ except Exception as e:
+ print(f'修改课程文件名称时发生错误:{e}')
+ logger.error(f'修改课程文件名称时发生错误:{e}')
+
+ def ad_change_file(self): # 切换课程文件
+ try:
+ conf_name = self.findChild(LineEdit, 'conf_name')
+ # 添加新课表
+ if self.conf_combo.currentText() == '添加新课表':
+ self.conf_combo.setCurrentIndex(-1) # 取消
+ # new_name = f'新课表 - {list.return_default_schedule_number() + 1}'
+ n2_dialog = TextFieldMessageBox(
+ self, '请输入新课表名称',
+ '请命名您的课程表计划:', '新课表 - 1', list_.get_schedule_config()
+ )
+ if not n2_dialog.exec():
+ return
+
+ new_name = n2_dialog.textField.text()
+ list_.create_new_profile(f'{new_name}.json')
+ self.conf_combo.clear()
+ self.conf_combo.addItems(list_.get_schedule_config())
+ config_center.write_conf('General', 'schedule', f'{new_name}.json')
+ self.conf_combo.setCurrentIndex(
+ list_.get_schedule_config().index(config_center.read_conf('General', 'schedule')))
+ conf_name.setText(new_name)
+ update_tray_tooltip()
+
+ elif self.conf_combo.currentText().endswith('.json'):
+ new_name = self.conf_combo.currentText()
+ config_center.write_conf('General', 'schedule', new_name)
+ conf_name.setText(new_name[:-5])
+ update_tray_tooltip()
+
+ else:
+ logger.error(f'切换课程文件时列表选择异常:{self.conf_combo.currentText()}')
+ Flyout.create(
+ icon=InfoBarIcon.ERROR,
+ title='错误!',
+ content=f"列表选项异常!{self.conf_combo.currentText()}",
+ target=self.conf_combo,
+ parent=self,
+ isClosable=True,
+ aniType=FlyoutAnimationType.PULL_UP
+ )
+ return
+ global loaded_data
+
+ config_center.schedule_name = config_center.read_conf('General', 'schedule')
+ schedule_center.update_schedule()
+ loaded_data = schedule_center.schedule_data
+ self.te_load_item()
+ self.te_upload_list()
+ self.te_update_parts_name()
+ se_load_item()
+ self.se_upload_list()
+ self.sp_fill_grid_row()
+ except Exception as e:
+ print(f'切换配置文件时发生错误:{e}')
+ logger.error(f'切换配置文件时发生错误:{e}')
+
+ def sp_fill_grid_row(self): # 填充预览表格
+ subtitle = self.findChild(SubtitleLabel, 'subtitle_file')
+ subtitle.setText(f'预览 - {config_center.schedule_name[:-5]}')
+ sp_week_type_combo = self.findChild(ComboBox, 'pre_week_type_combo')
+ schedule_view = self.findChild(TableWidget, 'schedule_view')
+ schedule_view.setRowCount(sp_get_class_num())
+ if sp_week_type_combo.currentIndex() == 1:
+ schedule_dict_sp = schedule_even_dict
+ else:
+ schedule_dict_sp = schedule_dict
+ for i in range(len(schedule_dict_sp)): # 周数
+ for j in range(len(schedule_dict_sp[str(i)])): # 一天内全部课程
+ item_text = schedule_dict_sp[str(i)][j].split('-')[0]
+ if item_text != '未添加':
+ item = QTableWidgetItem(item_text)
+ else:
+ item = QTableWidgetItem('')
+ schedule_view.setItem(j, i, item)
+ item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) # 设置单元格文本居中对齐
+
+ # 加载时间线
+ def te_load_item(self):
+ global morning_st, afternoon_st, loaded_data, timeline_dict
+ loaded_data = schedule_center.schedule_data
+ part = loaded_data.get('part')
+ part_name = loaded_data.get('part_name')
+ timeline = get_timeline()
+ # 找控件
+ te_timeline_list = self.findChild(ListWidget, 'timeline_list')
+ te_timeline_list.clear()
+ part_list = self.findChild(ListWidget, 'part_list')
+ part_list.clear()
+
+ for part_num, part_time in part.items(): # 加载节点
+ prefix = part_name[part_num]
+ time = QTime(int(part_time[0]), int(part_time[1])).toString('h:mm')
+ period = time
+ try:
+ part_type = part_time[2]
+ except IndexError:
+ part_type = 'part'
+
+ part_type = list_.part_type[part_type == 'break']
+ text = f'{prefix} - {period} - {part_type}'
+ part_list.addItem(text)
+
+ for week, _ in timeline.items(): # 加载节点
+ all_line = []
+ for item_name, time in timeline[week].items(): # 加载时间线
+ prefix = ''
+ item_time = f'{timeline[week][item_name]}分钟'
+ # 判断前缀和时段
+ if item_name.startswith('a'):
+ prefix = '课程'
+ elif item_name.startswith('f'):
+ prefix = '课间'
+ period = part_name[item_name[1]]
+
+ # 还原 item_text
+ item_text = f"{prefix} - {item_time} - {period}"
+ all_line.append(item_text)
+ timeline_dict[week] = all_line
+
+ def se_copy_odd_schedule(self):
+ logger.info('复制单周课表')
+ global schedule_dict, schedule_even_dict
+ schedule_even_dict = deepcopy(schedule_dict)
+ self.se_upload_list()
+
+ def te_upload_list(self): # 更新时间线到列表组件
+ logger.info('更新列表:时间线编辑')
+ te_timeline_list = self.findChild(ListWidget, 'timeline_list')
+ te_select_timeline = self.findChild(ComboBox, 'select_timeline')
+ try:
+ if te_select_timeline.currentIndex() == 0:
+ te_timeline_list.clear()
+ te_timeline_list.addItems(timeline_dict['default'])
+ else:
+ te_timeline_list.clear()
+ te_timeline_list.addItems(timeline_dict[str(te_select_timeline.currentIndex() - 1)])
+ self.te_detect_item()
+ except Exception as e:
+ print(f'加载时间线时发生错误:{e}')
+
+ def show_tip_flyout(self, title, content, target):
+ Flyout.create(
+ icon=InfoBarIcon.WARNING,
+ title=title,
+ content=content,
+ target=target,
+ parent=self,
+ isClosable=True,
+ aniType=FlyoutAnimationType.PULL_UP
+ )
+
+ # 上传课表到列表组件
+ def se_upload_list(self): # 更新课表到列表组件
+ logger.info('更新列表:课程表编辑')
+ se_schedule_list = self.findChild(ListWidget, 'schedule_list')
+ se_schedule_list.clearSelection()
+ se_week_combo = self.findChild(ComboBox, 'week_combo')
+ se_week_type_combo = self.findChild(ComboBox, 'week_type_combo')
+ se_copy_schedule_button = self.findChild(PushButton, 'copy_schedule')
+ global current_week
+ try:
+ if se_week_type_combo.currentIndex() == 1:
+ se_copy_schedule_button.show()
+ current_week = se_week_combo.currentIndex()
+ se_schedule_list.clear()
+ se_schedule_list.addItems(schedule_even_dict[str(current_week)])
+ else:
+ se_copy_schedule_button.hide()
+ current_week = se_week_combo.currentIndex()
+ se_schedule_list.clear()
+ se_schedule_list.addItems(schedule_dict[str(current_week)])
+ except Exception as e:
+ print(f'加载课表时发生错误:{e}')
+
+ def se_upload_item(self): # 保存列表内容到课表文件
+ se_schedule_list = self.findChild(ListWidget, 'schedule_list')
+ se_week_type_combo = self.findChild(ComboBox, 'week_type_combo')
+ if se_week_type_combo.currentIndex() == 1:
+ global schedule_even_dict
+ try:
+ cache_list = []
+ for i in range(se_schedule_list.count()):
+ item_text = se_schedule_list.item(i).text()
+ cache_list.append(item_text)
+ schedule_even_dict[str(current_week)][:] = cache_list
+ except Exception as e:
+ print(f'加载双周课表时发生错误:{e}')
+ else:
+ global schedule_dict
+ cache_list = []
+ for i in range(se_schedule_list.count()):
+ item_text = se_schedule_list.item(i).text()
+ cache_list.append(item_text)
+ schedule_dict[str(current_week)][:] = cache_list
+
+ # 保存课程
+ def se_save_item(self):
+ try:
+ data_dict = deepcopy(schedule_dict)
+ data_dict_even = deepcopy(schedule_even_dict) # 单双周保存
+
+ data_dict = convert_to_dict(data_dict)
+ data_dict_even = convert_to_dict(data_dict_even)
+
+ # 写入
+ data_dict_even = {"schedule_even": data_dict_even}
+ schedule_center.save_data(data_dict_even, config_center.schedule_name)
+ data_dict = {"schedule": data_dict}
+ schedule_center.save_data(data_dict, config_center.schedule_name)
+ Flyout.create(
+ icon=InfoBarIcon.SUCCESS,
+ title='保存成功',
+ content=f"已保存至 ./config/schedule/{config_center.schedule_name}",
+ target=self.findChild(PrimaryPushButton, 'save_schedule'),
+ parent=self,
+ isClosable=True,
+ aniType=FlyoutAnimationType.PULL_UP
+ )
+ self.sp_fill_grid_row()
+ except Exception as e:
+ logger.error(f'保存课表时发生错误: {e}')
+
+ def te_upload_item(self): # 上传时间线到列表组件
+ te_timeline_list = self.findChild(ListWidget, 'timeline_list')
+ te_select_timeline = self.findChild(ComboBox, 'select_timeline')
+ global timeline_dict
+ cache_list = []
+ for i in range(te_timeline_list.count()):
+ item_text = te_timeline_list.item(i).text()
+ cache_list.append(item_text)
+ if te_select_timeline.currentIndex() == 0:
+ timeline_dict['default'] = cache_list
+ else:
+ timeline_dict[str(te_select_timeline.currentIndex() - 1)] = cache_list
+
+ # 保存时间线
+ def te_save_item(self):
+ te_part_list = self.findChild(ListWidget, 'part_list')
+ data_dict = {"part": {}, "part_name": {}, "timeline": {'default': {}, **{str(w): {} for w in range(7)}}}
+ data_timeline_dict = deepcopy(timeline_dict)
+ # 逐条把列表里的信息整理保存
+ for i in range(te_part_list.count()):
+ item_text = te_part_list.item(i).text()
+ item_info = item_text.split(' - ')
+ time_tostring = item_info[1].split(':')
+ if len(item_info) == 3:
+ part_type = ['part', 'break'][item_info[2] == '休息段']
+ else:
+ part_type = 'part'
+ data_dict['part'][str(i)] = [int(time_tostring[0]), int(time_tostring[1]), part_type]
+ data_dict['part_name'][str(i)] = item_info[0]
+
+ try:
+ for week, _ in data_timeline_dict.items():
+ counter = [] # 初始化计数器
+ for i in range(len(data_dict['part'])):
+ counter.append(0)
+ counter_key = 0
+ lesson_num = 0
+ for i in range(len(data_timeline_dict[week])):
+ item_text = data_timeline_dict[week][i]
+ item_info = item_text.split(' - ')
+ item_name = ''
+ if item_info[0] == '课程':
+ item_name += 'a'
+ lesson_num += 1
+ if item_info[0] == '课间':
+ item_name += 'f'
+
+ for key, value in data_dict['part_name'].items(): # 节点计数
+ if value == item_info[2]:
+ item_name += str(key) # +节点序数
+ counter_key = int(key) # 记录节点序数
+ break
+
+ if item_name.startswith('a'):
+ counter[counter_key] += 1
+
+ item_name += str(lesson_num - sum(counter[:counter_key])) # 课程序数
+ item_time = item_info[1][0:len(item_info[1]) - 2]
+ data_dict['timeline'][str(week)][item_name] = item_time
+
+ schedule_center.save_data(data_dict, config_center.schedule_name)
+ self.te_detect_item()
+ se_load_item()
+ self.se_upload_list()
+ self.se_upload_item()
+ self.te_upload_item()
+ self.sp_fill_grid_row()
+ Flyout.create(
+ icon=InfoBarIcon.SUCCESS,
+ title='保存成功',
+ content=f"已保存至 ./config/schedule/{config_center.schedule_name}",
+ target=self.findChild(PrimaryPushButton, 'save'),
+ parent=self,
+ isClosable=True,
+ aniType=FlyoutAnimationType.PULL_UP
+ )
+ except Exception as e:
+ logger.error(f'保存时间线时发生错误: {e}')
+ Flyout.create(
+ icon=InfoBarIcon.ERROR,
+ title='保存失败!',
+ content=f"{e}\n保存失败,请将 ./log/ 中的日志提交给开发者以反馈问题。",
+ target=self.findChild(PrimaryPushButton, 'save'),
+ parent=self,
+ isClosable=True,
+ aniType=FlyoutAnimationType.PULL_UP
+ )
+
+ def te_sync_time(self):
+ te_class_activity_combo = self.findChild(ComboBox, 'class_activity')
+ spin_time = self.findChild(SpinBox, 'spin_time')
+ if te_class_activity_combo.currentIndex() == 0:
+ spin_time.setValue(40)
+ if te_class_activity_combo.currentIndex() == 1:
+ spin_time.setValue(10)
+
+ def te_detect_item(self):
+ timeline_list = self.findChild(ListWidget, 'timeline_list')
+ part_list = self.findChild(ListWidget, 'part_list')
+ tips = self.findChild(CaptionLabel, 'tips_2')
+ tips_part = self.findChild(CaptionLabel, 'tips_1')
+ if part_list.count() > 0:
+ tips_part.hide()
+ else:
+ tips_part.show()
+ if timeline_list.count() > 0:
+ tips.hide()
+ else:
+ tips.show()
+
+ def te_add_item(self):
+ te_timeline_list = self.findChild(ListWidget, 'timeline_list')
+ class_activity = self.findChild(ComboBox, 'class_activity')
+ spin_time = self.findChild(SpinBox, 'spin_time')
+ time_period = self.findChild(ComboBox, 'time_period')
+ if time_period.currentText() == "": # 时间段不能为空 修复 #184
+ Flyout.create(
+ icon=InfoBarIcon.WARNING,
+ title='无法添加时间线 o(TヘTo)',
+ content='在添加时间线前,先任意添加一个节点',
+ target=self.findChild(ToolButton, 'add_button'),
+ parent=self,
+ isClosable=True,
+ aniType=FlyoutAnimationType.PULL_UP
+ )
+ return # 时间段不能为空
+ te_timeline_list.addItem(
+ f'{class_activity.currentText()} - {spin_time.value()}分钟 - {time_period.currentText()}'
+ )
+ self.te_detect_item()
+
+ def te_add_part(self):
+ te_part_list = self.findChild(ListWidget, 'part_list')
+ te_name_part = self.findChild(EditableComboBox, 'name_part_combo')
+ te_part_time = self.findChild(TimeEdit, 'part_time')
+ te_part_type = self.findChild(ComboBox, 'part_type')
+ if te_part_list.count() < 10:
+ te_part_list.addItem(
+ f'{te_name_part.currentText()} - {te_part_time.time().toString("h:mm")} - {te_part_type.currentText()}'
+ )
+ else: # 最多只能添加9个节点
+ Flyout.create(
+ icon=InfoBarIcon.WARNING,
+ title='没办法继续添加了 o(TヘTo)',
+ content='Class Widgets 最多只能添加10个“节点”!',
+ target=self.findChild(ToolButton, 'add_part_button'),
+ parent=self,
+ isClosable=True,
+ aniType=FlyoutAnimationType.PULL_UP
+ )
+ self.te_detect_item()
+ self.te_update_parts_name()
+
+ def te_delete_part(self):
+ alert = MessageBox("您确定要删除这个时段吗?", "删除该节点后,将一并删除该节点下所有课程安排,且无法恢复。", self)
+ alert.yesButton.setText('删除')
+ alert.yesButton.setStyleSheet("""
+ PushButton{
+ border-radius: 5px;
+ padding: 5px 12px 6px 12px;
+ outline: none;
+ }
+ PrimaryPushButton{
+ color: white;
+ background-color: #FF6167;
+ border: 1px solid #FF8585;
+ border-bottom: 1px solid #943333;
+ }
+ PrimaryPushButton:hover{
+ background-color: #FF7E83;
+ border: 1px solid #FF8084;
+ border-bottom: 1px solid #B13939;
+ }
+ PrimaryPushButton:pressed{
+ color: rgba(255, 255, 255, 0.63);
+ background-color: #DB5359;
+ border: 1px solid #DB5359;
+ }
+ """)
+ alert.cancelButton.setText('取消')
+ if alert.exec():
+ global timeline_dict, schedule_dict
+ te_part_list = self.findChild(ListWidget, 'part_list')
+ selected_items = te_part_list.selectedItems()
+ if not selected_items:
+ return
+
+ deleted_part_name = selected_items[0].text().split(' - ')[0]
+ for item in selected_items:
+ te_part_list.takeItem(te_part_list.row(item))
+
+ # 修复了删除时段没能同步删除时间线的Bug #123
+ for day in timeline_dict: # 删除时间线
+ count = 0
+ break_count = 0
+ delete_schedule_list = []
+ delete_schedule_even_list = []
+ delete_part_list = []
+ for i in range(len(timeline_dict[day])):
+ act = timeline_dict[day][i]
+ count += 1
+ item_info = act.split(' - ')
+
+ if item_info[0] == '课间':
+ break_count += 1
+
+ if item_info[2] == deleted_part_name:
+ delete_part_list.append(act)
+ if item_info[0] != '课间':
+ if day != 'default':
+ delete_schedule_list.append(schedule_dict[day][count - break_count - 1])
+ delete_schedule_even_list.append(schedule_even_dict[day][count - break_count - 1])
+ else:
+ for j in range(7):
+ try:
+ for item in schedule_dict[str(j)]:
+ if item.split('-')[1] == deleted_part_name:
+ delete_schedule_list.append(
+ schedule_dict[str(j)][count - break_count - 1])
+ for item in schedule_even_dict[str(j)]:
+ if item.split('-')[1] == deleted_part_name:
+ delete_schedule_even_list.append(
+ schedule_dict[str(j)][count - break_count - 1])
+ except Exception as e:
+ logger.warning(f'删除时段时发生错误:{e}')
+
+ for item in delete_part_list: # 删除时间线
+ timeline_dict[day].remove(item)
+ if day != 'default': # 删除课表
+ for item in delete_schedule_list:
+ schedule_dict[day].remove(item)
+
+ for day in range(7): # 删除默认课程表
+ delete_schedule_list = []
+ delete_schedule_even_list = []
+ for item in schedule_dict[str(day)]: # 单周
+ if item.split('-')[1] == deleted_part_name:
+ delete_schedule_list.append(item)
+ for item in delete_schedule_list:
+ schedule_dict[str(day)].remove(item)
+
+ for item in schedule_even_dict[str(day)]: # 双周
+ if item.split('-')[1] == deleted_part_name:
+ delete_schedule_even_list.append(item)
+ for item in delete_schedule_even_list:
+ schedule_even_dict[str(day)].remove(item)
+
+ self.te_upload_list()
+ self.se_upload_list()
+ self.te_update_parts_name()
+ else:
+ return
+
+ def te_update_parts_name(self):
+ rl = []
+ te_time_combo = self.findChild(ComboBox, 'time_period') # 时段
+ te_time_combo.clear()
+ part_list = self.findChild(ListWidget, 'part_list')
+ for i in range(part_list.count()):
+ info = part_list.item(i).text().split(' - ')
+ rl.append(info[0])
+ te_time_combo.addItems(rl)
+
+ def te_edit_item(self):
+ te_timeline_list = self.findChild(ListWidget, 'timeline_list')
+ class_activity = self.findChild(ComboBox, 'class_activity')
+ spin_time = self.findChild(SpinBox, 'spin_time')
+ time_period = self.findChild(ComboBox, 'time_period')
+ selected_items = te_timeline_list.selectedItems()
+
+ if selected_items:
+ selected_item = selected_items[0] # 取第一个选中的项目
+ selected_item.setText(
+ f'{class_activity.currentText()} - {spin_time.value()}分钟 - {time_period.currentText()}'
+ )
+
+ def se_edit_item(self):
+ se_schedule_list = self.findChild(ListWidget, 'schedule_list')
+ se_class_combo = self.findChild(ComboBox, 'class_combo')
+ se_custom_class_text = self.findChild(LineEdit, 'custom_class')
+ selected_items = se_schedule_list.selectedItems()
+
+ if selected_items:
+ selected_item = selected_items[0]
+ name_list = selected_item.text().split('-')
+ if se_class_combo.currentIndex() != 0:
+ selected_item.setText(
+ f'{se_class_combo.currentText()}-{name_list[1]}'
+ )
+ else:
+ if se_custom_class_text.text() != '':
+ selected_item.setText(
+ f'{se_custom_class_text.text()}-{name_list[1]}'
+ )
+ se_class_combo.addItem(se_custom_class_text.text())
+
+ def se_quick_set_schedule(self): # 快速设置课表
+ se_schedule_list = self.findChild(ListWidget, 'schedule_list')
+ quick_set_schedule = self.findChild(ListWidget, 'subject_list')
+ selected_items = se_schedule_list.selectedItems()
+ selected_subject = quick_set_schedule.currentItem().text()
+ if se_schedule_list.count() > 0:
+ if not selected_items:
+ se_schedule_list.setCurrentRow(0)
+
+ selected_row = se_schedule_list.currentRow()
+ selected_item = se_schedule_list.item(selected_row)
+ name_list = selected_item.text().split('-')
+ selected_item.setText(
+ f'{selected_subject}-{name_list[1]}'
+ )
+
+ if se_schedule_list.count() > selected_row + 1: # 选择下一行
+ se_schedule_list.setCurrentRow(selected_row + 1)
+
+ def se_quick_select_week(self): # 快速选择周
+ se_week_combo = self.findChild(ComboBox, 'week_combo')
+ if se_week_combo.currentIndex() != 6:
+ se_week_combo.setCurrentIndex(se_week_combo.currentIndex() + 1)
+
+ def te_delete_item(self):
+ te_timeline_list = self.findChild(ListWidget, 'timeline_list')
+ selected_items = te_timeline_list.selectedItems()
+ for item in selected_items:
+ te_timeline_list.takeItem(te_timeline_list.row(item))
+ self.te_detect_item()
+
+ def se_delete_item(self):
+ se_schedule_list = self.findChild(ListWidget, 'schedule_list')
+ selected_items = se_schedule_list.selectedItems()
+ if selected_items:
+ selected_item = selected_items[0]
+ name_list = selected_item.text().split('-')
+ selected_item.setText(
+ f'未添加-{name_list[1]}'
+ )
+
+ def cd_edit_item(self):
+ cd_countdown_list = self.findChild(ListWidget, 'countdown_list')
+ cd_text_cd = self.findChild(LineEdit, 'text_cd')
+ cd_set_countdown_date = self.findChild(CalendarPicker, 'set_countdown_date')
+ selected_items = cd_countdown_list.selectedItems()
+ if selected_items:
+ selected_item = selected_items[0]
+ selected_item.setText(
+ f"{cd_set_countdown_date.date.toString('yyyy-M-d')} - {cd_text_cd.text()}"
+ )
+
+ def cd_delete_item(self):
+ cd_countdown_list = self.findChild(ListWidget, 'countdown_list')
+ selected_items = cd_countdown_list.selectedItems()
+ if selected_items:
+ item = selected_items[0]
+ cd_countdown_list.takeItem(cd_countdown_list.row(item))
+
+ def cd_add_item(self):
+ cd_countdown_list = self.findChild(ListWidget, 'countdown_list')
+ cd_text_cd = self.findChild(LineEdit, 'text_cd')
+ cd_set_countdown_date = self.findChild(CalendarPicker, 'set_countdown_date')
+ cd_countdown_list.addItem(
+ f"{cd_set_countdown_date.date.toString('yyyy-M-d')} - {cd_text_cd.text()}"
+ )
+
+ def cd_save_item(self):
+ cd_countdown_list = self.findChild(ListWidget, 'countdown_list')
+ countdown_date = []
+ cd_text_custom = []
+
+ for i in range(cd_countdown_list.count()):
+ item = cd_countdown_list.item(i)
+ text = item.text().split(' - ')
+ countdown_date.append(text[0])
+ cd_text_custom.append(text[1])
+
+ Flyout.create(
+ icon=InfoBarIcon.SUCCESS,
+ title='保存成功',
+ content=f"已保存至 ./config.ini",
+ target=self.findChild(PrimaryPushButton, 'save_countdown'),
+ parent=self,
+ isClosable=True,
+ aniType=FlyoutAnimationType.PULL_UP
+ )
+
+ config_center.write_conf('Date', 'countdown_date', ','.join(countdown_date))
+ config_center.write_conf('Date', 'cd_text_custom', ','.join(cd_text_custom))
+
+ def setup_countdown_edit(self):
+ cd_load_item()
+ logger.debug(f"{countdown_dict}")
+ cd_set_button = self.findChild(ToolButton, 'set_button_cd')
+ cd_set_button.setIcon(fIcon.EDIT)
+ cd_set_button.setToolTip('编辑倒计日')
+ cd_set_button.installEventFilter(ToolTipFilter(cd_set_button, showDelay=300, position=ToolTipPosition.TOP))
+ cd_set_button.clicked.connect(self.cd_edit_item)
+
+ cd_clear_button = self.findChild(ToolButton, 'clear_button_cd')
+ cd_clear_button.setIcon(fIcon.DELETE)
+ cd_clear_button.setToolTip('删除倒计日')
+ cd_clear_button.installEventFilter(ToolTipFilter(cd_clear_button, showDelay=300, position=ToolTipPosition.TOP))
+ cd_clear_button.clicked.connect(self.cd_delete_item)
+
+ cd_add_button = self.findChild(ToolButton, 'add_button_cd')
+ cd_add_button.setIcon(fIcon.ADD)
+ cd_add_button.setToolTip('添加倒计日')
+ cd_add_button.installEventFilter(ToolTipFilter(cd_add_button, showDelay=300, position=ToolTipPosition.TOP))
+ cd_add_button.clicked.connect(self.cd_add_item)
+
+ cd_schedule_list = self.findChild(ListWidget, 'countdown_list')
+ cd_schedule_list.addItems([f"{date} - {countdown_dict[date]}" for date in countdown_dict])
+
+ cd_save_button = self.findChild(PrimaryPushButton, 'save_countdown')
+ cd_save_button.clicked.connect(self.cd_save_item)
+
+ cd_mode = self.findChild(ComboBox, 'countdown_mode')
+ cd_mode.addItems(list_.countdown_modes)
+ cd_mode.setCurrentIndex(int(config_center.read_conf('Date', 'countdown_custom_mode')))
+ cd_mode.currentIndexChanged.connect(
+ lambda: config_center.write_conf('Date', 'countdown_custom_mode', str(cd_mode.currentIndex())))
+
+ cd_upd_cd = self.findChild(SpinBox, 'countdown_upd_cd')
+ cd_upd_cd.setValue(int(config_center.read_conf('Date', 'countdown_upd_cd')))
+ cd_upd_cd.valueChanged.connect(
+ lambda: config_center.write_conf('Date', 'countdown_upd_cd', str(cd_upd_cd.value())))
+
+ def m_start_time_changed(self):
+ global morning_st
+ te_m_start_time = self.findChild(TimeEdit, 'morningStartTime')
+ unformatted_time = te_m_start_time.time()
+ h = unformatted_time.hour()
+ m = unformatted_time.minute()
+ morning_st = (h, m)
+
+ def a_start_time_changed(self):
+ global afternoon_st
+ te_m_start_time = self.findChild(TimeEdit, 'afternoonStartTime')
+ unformatted_time = te_m_start_time.time()
+ h = unformatted_time.hour()
+ m = unformatted_time.minute()
+ afternoon_st = (h, m)
+
+ def init_nav(self):
+ self.addSubInterface(self.spInterface, fIcon.HOME, '课表预览')
+ self.addSubInterface(self.teInterface, fIcon.DATE_TIME, '时间线编辑')
+ self.addSubInterface(self.seInterface, fIcon.EDUCATION, '课程表编辑')
+ self.addSubInterface(self.cdInterface, fIcon.CALENDAR, '倒计日编辑')
+ self.addSubInterface(self.cfInterface, fIcon.FOLDER, '配置文件')
+ self.navigationInterface.addSeparator()
+ self.addSubInterface(self.hdInterface, fIcon.QUESTION, '帮助')
+ self.addSubInterface(self.plInterface, fIcon.APPLICATION, '插件', NavigationItemPosition.BOTTOM)
+ self.navigationInterface.addSeparator(NavigationItemPosition.BOTTOM)
+ self.addSubInterface(self.ctInterface, fIcon.BRUSH, '自定义', NavigationItemPosition.BOTTOM)
+ self.addSubInterface(self.sdInterface, fIcon.RINGER, '提醒', NavigationItemPosition.BOTTOM)
+ self.addSubInterface(self.adInterface, fIcon.SETTING, '高级选项', NavigationItemPosition.BOTTOM)
+ self.addSubInterface(self.ifInterface, fIcon.INFO, '关于本产品', NavigationItemPosition.BOTTOM)
+
+ def init_window(self):
+ self.stackedWidget.setCurrentIndex(0) # 设置初始页面
+ self.load_all_item()
+ self.setMinimumWidth(700)
+ self.setMinimumHeight(400)
+ self.navigationInterface.setExpandWidth(250)
+ self.navigationInterface.setCollapsible(False)
+ self.setMicaEffectEnabled(True)
+
+ # 修复设置窗口在各个屏幕分辨率DPI下的窗口大小
+ screen_geometry = QApplication.primaryScreen().geometry()
+ screen_width = screen_geometry.width()
+ screen_height = screen_geometry.height()
+
+ width = int(screen_width * 0.6)
+ height = int(screen_height * 0.7)
+
+ self.move(int(screen_width / 2 - width / 2), 150)
+ self.resize(width, height)
+
+ self.setWindowTitle('Class Widgets - 设置')
+ self.setWindowIcon(QIcon(f'{base_directory}/img/logo/favicon-settings.ico'))
+
+ self.init_font() # 设置字体
+
+ def closeEvent(self, event):
+ self.closed.emit()
+ event.accept()
+
+
+def sp_get_class_num(): # 获取当前周课程数(未完成)
+ highest_count = 0
+ for timeline_ in get_timeline().keys():
+ timeline = get_timeline()[timeline_]
+ count = 0
+ for item_name, item_time in timeline.items():
+ if item_name.startswith('a'):
+ count += 1
+ if count > highest_count:
+ highest_count = count
+ return highest_count
+
+
+if __name__ == '__main__':
+ app = QApplication(sys.argv)
+ settings = SettingsMenu()
+ settings.show()
+ # settings.setMicaEffectEnabled(True)
+ sys.exit(app.exec())
diff --git a/network_thread.py b/network_thread.py
new file mode 100644
index 0000000..6968a10
--- /dev/null
+++ b/network_thread.py
@@ -0,0 +1,483 @@
+import json
+import os
+import shutil
+import zipfile # 解压插件zip
+from datetime import datetime
+
+import requests
+from PyQt5.QtCore import QThread, pyqtSignal, QEventLoop
+from loguru import logger
+from packaging.version import Version
+
+import conf
+import utils
+import weather_db as db
+from conf import base_directory
+from file import config_center
+
+headers = {"User-Agent": "Mozilla/5.0", "Cache-Control": "no-cache"} # 设置请求头
+# proxies = {"http": "http://127.0.0.1:10809", "https": "http://127.0.0.1:10809"} # 加速访问
+proxies = {"http": None, "https": None}
+
+MIRROR_PATH = f"{base_directory}/config/mirror.json"
+PLAZA_REPO_URL = "https://raw.githubusercontent.com/Class-Widgets/plugin-plaza/"
+PLAZA_REPO_DIR = "https://api.github.com/repos/Class-Widgets/plugin-plaza/contents/"
+threads = []
+
+# 读取镜像配置
+mirror_list = []
+try:
+ with open(MIRROR_PATH, 'r', encoding='utf-8') as file:
+ mirror_dict = json.load(file).get('gh_mirror')
+except Exception as e:
+ logger.error(f"读取镜像配置失败: {e}")
+
+for name in mirror_dict:
+ mirror_list.append(name)
+
+if config_center.read_conf('Plugin', 'mirror') not in mirror_list: # 如果当前配置不在镜像列表中,则设置为默认镜像
+ logger.warning(f"当前配置不在镜像列表中,设置为默认镜像: {mirror_list[0]}")
+ config_center.write_conf('Plugin', 'mirror', mirror_list[0])
+
+
+class getRepoFileList(QThread): # 获取仓库文件目录
+ repo_signal = pyqtSignal(dict)
+
+ def __init__(
+ self, url='https://raw.githubusercontent.com/Class-Widgets/plugin-plaza/main/Banner/banner.json'
+ ):
+ super().__init__()
+ self.download_url = url
+
+ def run(self):
+ try:
+ plugin_info_data = self.get_plugin_info()
+ self.repo_signal.emit(plugin_info_data)
+ except Exception as e:
+ logger.error(f"触发banner信息失败: {e}")
+
+ def get_plugin_info(self):
+ try:
+ mirror_url = mirror_dict[config_center.read_conf('Plugin', 'mirror')]
+ url = f"{mirror_url}{self.download_url}"
+ response = requests.get(url, proxies=proxies, headers=headers) # 禁用代理
+ if response.status_code == 200:
+ data = response.json()
+ return data
+ else:
+ logger.error(f"获取banner信息失败:{response.status_code}")
+ return {"error": response.status_code}
+ except Exception as e:
+ logger.error(f"获取banner信息失败:{e}")
+ return {"error": e}
+
+
+class getPluginInfo(QThread): # 获取插件信息(json)
+ repo_signal = pyqtSignal(dict)
+
+ def __init__(
+ self, url='https://raw.githubusercontent.com/Class-Widgets/plugin-plaza/main/Plugins/plugin_list.json'
+ ):
+ super().__init__()
+ self.download_url = url
+
+ def run(self):
+ try:
+ plugin_info_data = self.get_plugin_info()
+ self.repo_signal.emit(plugin_info_data)
+ except Exception as e:
+ logger.error(f"触发插件信息失败: {e}")
+
+ def get_plugin_info(self):
+ try:
+ mirror_url = mirror_dict[config_center.read_conf('Plugin', 'mirror')]
+ url = f"{mirror_url}{self.download_url}"
+ response = requests.get(url, proxies=proxies, headers=headers) # 禁用代理
+ if response.status_code == 200:
+ data = response.json()
+ return data
+ else:
+ logger.error(f"获取插件信息失败:{response.status_code}")
+ return {}
+ except Exception as e:
+ logger.error(f"获取插件信息失败:{e}")
+ return {}
+
+
+class getTags(QThread): # 获取插件标签(json)
+ repo_signal = pyqtSignal(dict)
+
+ def __init__(
+ self, url='https://raw.githubusercontent.com/Class-Widgets/plugin-plaza/main/Plugins/plaza_detail.json'
+ ):
+ super().__init__()
+ self.download_url = url
+
+ def run(self):
+ try:
+ plugin_info_data = self.get_plugin_info()
+ self.repo_signal.emit(plugin_info_data)
+ except Exception as e:
+ logger.error(f"触发Tag信息失败: {e}")
+
+ def get_plugin_info(self):
+ try:
+ mirror_url = mirror_dict[config_center.read_conf('Plugin', 'mirror')]
+ url = f"{mirror_url}{self.download_url}"
+ response = requests.get(url, proxies=proxies, headers=headers) # 禁用代理
+ if response.status_code == 200:
+ data = response.json()
+ return data
+ else:
+ logger.error(f"获取Tag信息失败:{response.status_code}")
+ return {}
+ except Exception as e:
+ logger.error(f"获取Tag信息失败:{e}")
+ return {}
+
+
+class getImg(QThread): # 获取图片
+ repo_signal = pyqtSignal(bytes)
+
+ def __init__(self, url='https://raw.githubusercontent.com/Class-Widgets/plugin-plaza/main/Banner/banner_1.png'):
+ super().__init__()
+ self.download_url = url
+
+ def run(self):
+ try:
+ banner_data = self.get_banner()
+ if banner_data is not None:
+ self.repo_signal.emit(banner_data)
+ else:
+ with open(f"{base_directory}/img/plaza/banner_pre.png", 'rb') as default_img: # 读取默认图片
+ self.repo_signal.emit(default_img.read())
+ except Exception as e:
+ logger.error(f"触发图片失败: {e}")
+
+ def get_banner(self):
+ try:
+ mirror_url = mirror_dict[config_center.read_conf('Plugin', 'mirror')]
+ url = f"{mirror_url}{self.download_url}"
+ response = requests.get(url, proxies=proxies, headers=headers)
+ if response.status_code == 200:
+ return response.content
+ else:
+ logger.error(f"获取图片失败:{response.status_code}")
+ return None
+ except Exception as e:
+ logger.error(f"获取图片失败:{e}")
+ return None
+
+
+class getReadme(QThread): # 获取README
+ html_signal = pyqtSignal(str)
+
+ def __init__(self, url='https://raw.githubusercontent.com/Class-Widgets/Class-Widgets/main/README.md'):
+ super().__init__()
+ self.download_url = url
+
+ def run(self):
+ try:
+ readme_data = self.get_readme()
+ self.html_signal.emit(readme_data)
+ except Exception as e:
+ logger.error(f"触发README失败: {e}")
+
+ def get_readme(self):
+ try:
+ mirror_url = mirror_dict[config_center.read_conf('Plugin', 'mirror')]
+ url = f"{mirror_url}{self.download_url}"
+ # print(url)
+ response = requests.get(url, proxies=proxies)
+ if response.status_code == 200:
+ return response.text
+ else:
+ logger.error(f"获取README失败:{response.status_code}")
+ return ''
+ except Exception as e:
+ logger.error(f"获取README失败:{e}")
+ return ''
+
+class getCity(QThread):
+
+ def __init__(self, url='https://qifu-api.baidubce.com/ip/local/geo/v1/district'):
+ super().__init__()
+ self.download_url = url
+
+ def run(self):
+ try:
+ city_data = self.get_city()
+ config_center.write_conf('Weather', 'city', db.search_code_by_name(city_data))
+ except Exception as e:
+ logger.error(f"获取城市失败: {e}")
+
+ def get_city(self):
+ try:
+ req = requests.get(self.download_url, proxies=proxies)
+ if req.status_code == 200:
+ data = req.json()
+ # {"code":"Success","data":{"continent":"","country":"中国","zipcode":"","owner":"","isp":"","adcode":"","prov":"","city":"","district":""},"ip":"45.192.96.246"}
+ if data['code'] == 'Success':
+ data = data['data']
+ logger.info(f"获取城市成功:{data['city']}, {data['district']}")
+ return (data['city'], data['district'])
+ else:
+ logger.error(f"获取城市失败:{data['message']}")
+ return ('', '')
+ else:
+ logger.error(f"获取城市失败:{req.status_code}")
+ return ('', '')
+
+ except Exception as e:
+ logger.error(f"获取城市失败:{e}")
+ return ('', '')
+
+class VersionThread(QThread): # 获取最新版本号
+ version_signal = pyqtSignal(dict)
+ _instance_running = False
+
+ def __init__(self):
+ super().__init__()
+ def run(self):
+ version = self.get_latest_version()
+ self.version_signal.emit(version)
+
+ @classmethod
+ def is_running(cls):
+ return cls._instance_running
+
+ @staticmethod
+ def get_latest_version():
+ url = "https://classwidgets.rinlit.cn/version.json"
+ try:
+ logger.info(f"正在获取版本信息")
+ response = requests.get(url, proxies=proxies, timeout=30)
+ logger.debug(f"更新请求响应: {response.status_code}")
+ if response.status_code == 200:
+ data = response.json()
+ return data
+ else:
+ logger.error(f"无法获取版本信息 错误代码:{response.status_code},响应内容: {response.text}")
+ return {'error': f"请求失败,错误代码:{response.status_code}"}
+ except requests.exceptions.RequestException as e:
+ logger.error(f"请求失败,错误详情:{str(e)}")
+ return {"error": f"请求失败\n{str(e)}"}
+
+
+class getDownloadUrl(QThread):
+ # 定义信号,通知下载进度或完成
+ geturl_signal = pyqtSignal(str)
+
+ def __init__(self, username, repo):
+ super().__init__()
+ self.username = username
+ self.repo = repo
+
+ def run(self):
+ try:
+ url = f"https://api.github.com/repos/{self.username}/{self.repo}/releases/latest"
+ response = requests.get(url, proxies=proxies)
+ if response.status_code == 200:
+ data = response.json()
+ for asset in data['assets']: # 遍历下载链接
+ if isinstance(asset, dict) and 'browser_download_url' in asset:
+ asset_url = asset['browser_download_url']
+ self.geturl_signal.emit(asset_url)
+ elif response.status_code == 403: # 触发API限制
+ logger.warning("到达Github API限制,请稍后再试")
+ response = requests.get('https://api.github.com/users/octocat', proxies=proxies)
+ reset_time = response.headers.get('X-RateLimit-Reset')
+ reset_time = datetime.fromtimestamp(int(reset_time))
+ self.geturl_signal.emit(f"ERROR: 由于请求次数过多,到达Github API限制,请在{reset_time.minute}分钟后再试")
+ else:
+ logger.error(f"网络连接错误:{response.status_code}")
+ except Exception as e:
+ logger.error(f"获取下载链接错误: {e}")
+ self.geturl_signal.emit(f"获取下载链接错误: {e}")
+
+
+class DownloadAndExtract(QThread): # 下载并解压插件
+ progress_signal = pyqtSignal(float) # 进度
+ status_signal = pyqtSignal(str) # 状态
+
+ def __init__(self, url, plugin_name='test_114'):
+ super().__init__()
+ self.download_url = url
+ print(self.download_url)
+ self.cache_dir = "cache"
+ self.plugin_name = plugin_name
+ self.extract_dir = conf.PLUGINS_DIR # 插件目录
+
+ def run(self):
+ try:
+ enabled_plugins = conf.load_plugin_config() # 加载启用的插件
+
+ os.makedirs(self.cache_dir, exist_ok=True)
+ os.makedirs(self.extract_dir, exist_ok=True)
+
+ zip_path = os.path.join(self.cache_dir, f'{self.plugin_name}.zip')
+
+ self.status_signal.emit("DOWNLOADING")
+ self.download_file(zip_path)
+ self.status_signal.emit("EXTRACTING")
+ self.extract_zip(zip_path)
+ os.remove(zip_path)
+ print(enabled_plugins)
+
+ if (
+ self.plugin_name not in enabled_plugins['enabled_plugins']
+ and config_center.read_conf('Plugin', 'auto_enable_plugin') == '1'
+ ):
+ logger.info(f"自动启用插件: {self.plugin_name}")
+ enabled_plugins['enabled_plugins'].append(self.plugin_name)
+ conf.save_plugin_config(enabled_plugins)
+
+ self.status_signal.emit("DONE")
+ except Exception as e:
+ self.status_signal.emit(f"错误: {e}")
+ logger.error(f"插件下载/解压失败: {e}")
+
+ def stop(self):
+ self._running = False
+ self.terminate()
+
+ def download_file(self, file_path):
+ # time.sleep(555) # 模拟下载时间
+ try:
+ self.download_url = mirror_dict[config_center.read_conf('Plugin', 'mirror')] + self.download_url
+ print(self.download_url)
+ response = requests.get(self.download_url, stream=True, proxies=proxies)
+ if response.status_code != 200:
+ logger.error(f"插件下载失败,错误代码: {response.status_code}")
+ self.status_signal.emit(f'ERROR: 网络连接错误:{response.status_code}')
+ return
+
+ total_size = int(response.headers.get('content-length', 0))
+ downloaded_size = 0
+
+ with open(file_path, 'wb') as file:
+ for chunk in response.iter_content(1024):
+ file.write(chunk)
+ downloaded_size += len(chunk)
+ progress = (downloaded_size / total_size) * 100 if total_size > 0 else 0 # 计算进度
+ self.progress_signal.emit(progress)
+ except Exception as e:
+ self.status_signal.emit(f'ERROR: {e}')
+ logger.error(f"插件下载错误: {e}")
+
+ def extract_zip(self, zip_path):
+ try:
+ with zipfile.ZipFile(zip_path, 'r') as zip_ref:
+ zip_ref.extractall(self.extract_dir)
+
+ for p_dir in os.listdir(self.extract_dir):
+ if p_dir.startswith(self.plugin_name) and len(p_dir) > len(self.plugin_name):
+ new_name = p_dir.rsplit('-', 1)[0]
+ if os.path.exists(os.path.join(self.extract_dir, new_name)):
+ shutil.copytree(
+ os.path.join(self.extract_dir, p_dir), os.path.join(self.extract_dir, new_name),
+ dirs_exist_ok=True)
+ shutil.rmtree(os.path.join(self.extract_dir, p_dir))
+ else:
+ os.rename(os.path.join(self.extract_dir, p_dir), os.path.join(self.extract_dir, new_name))
+ except Exception as e:
+ logger.error(f"解压失败: {e}")
+
+
+def check_update():
+ global threads
+
+ if VersionThread.is_running():
+ logger.debug("已存在版本检查线程在运行,跳过本检查")
+ return
+
+ # 清理已终止的线程
+ threads = [t for t in threads if t.isRunning()]
+
+ # 创建新的版本检查线程
+ version_thread = VersionThread()
+ threads.append(version_thread)
+ version_thread.version_signal.connect(check_version)
+ version_thread.start()
+
+
+def check_version(version): # 检查更新
+ global threads
+ for thread in threads:
+ thread.terminate()
+ threads = []
+ if 'error' in version:
+ utils.tray_icon.push_error_notification(
+ "检查更新失败!",
+ f"检查更新失败!\n{version['error']}"
+ )
+ return False
+
+ channel = int(config_center.read_conf("Other", "version_channel"))
+ server_version = version['version_release' if channel == 0 else 'version_beta']
+ local_version = config_center.read_conf("Other", "version")
+ logger.debug(f"服务端版本: {Version(server_version)},本地版本: {Version(local_version)}")
+ if Version(server_version) > Version(local_version):
+ utils.tray_icon.push_update_notification(f"新版本速递:{server_version}\n请在“设置”中了解更多。")
+
+class weatherReportThread(QThread): # 获取最新天气信息
+ weather_signal = pyqtSignal(dict)
+
+ def __init__(self):
+ super().__init__()
+
+ def run(self):
+ try:
+ weather_data = self.get_weather_data()
+ self.weather_signal.emit(weather_data)
+ except Exception as e:
+ logger.error(f"触发天气信息失败: {e}")
+ finally:
+ self.deleteLater()
+
+ @staticmethod
+ def get_weather_data():
+ location_key = config_center.read_conf('Weather', 'city')
+ if location_key == '0':
+ city_thread = getCity()
+ loop = QEventLoop()
+ city_thread.finished.connect(loop.quit)
+ city_thread.start()
+ loop.exec_() # 阻塞到完成
+ location_key = config_center.read_conf('Weather', 'city')
+ if location_key == '0' or not location_key:
+ location_key = 101010100
+ days = 1
+ key = config_center.read_conf('Weather', 'api_key')
+ url = db.get_weather_url().format(location_key=location_key, days=days, key=key)
+ alert_url = db.get_weather_alert_url()
+ try:
+ data_group = {'now': {}, 'alert': {}}
+ response_now = requests.get(url, proxies=proxies) # 禁用代理
+ if alert_url == 'NotSupported':
+ logger.warning(f"当前API不支持天气预警信息")
+ elif alert_url is None:
+ logger.warning(f"无单独天气预警信息API")
+ else:
+ alert_url = alert_url.format(location_key=location_key, key=key)
+ response_alert = requests.get(alert_url, proxies=proxies)
+
+ if response_alert.status_code == 200:
+ data_alert = response_alert.json()
+ data_group['alert'] = data_alert
+ else:
+ logger.error(f"获取天气预警信息失败:{response_alert.status_code}")
+
+ if response_now.status_code == 200:
+ data = response_now.json()
+ data_group['now'] = data
+ return data_group
+ else:
+ logger.error(f"获取天气信息失败:{response_now.status_code}")
+ return {'error': {'info': {'value': '错误', 'unit': response_now.status_code}}}
+ except requests.exceptions.RequestException as e: # 请求失败
+ logger.error(f"获取天气信息失败:{e}")
+ return {'error': {'info': {'value': '错误', 'unit': ''}}}
+ except Exception as e:
+ logger.error(f"获取天气信息失败:{e}")
+ return {'error': {'info': {'value': '错误', 'unit': ''}}}
diff --git a/play_audio.py b/play_audio.py
new file mode 100644
index 0000000..2d8e1b3
--- /dev/null
+++ b/play_audio.py
@@ -0,0 +1,99 @@
+import os
+import time
+
+import pygame
+import pygame.mixer
+from PyQt5.QtCore import QThread, pyqtSignal
+from loguru import logger
+
+import conf
+from file import config_center
+from generate_speech import TTSEngine
+
+sound_cache = {}
+
+
+class PlayAudio(QThread):
+ play_back_signal = pyqtSignal(bool)
+
+ def __init__(self, file_path: str, tts_delete_after: bool = False):
+ super().__init__()
+ self.file_path = file_path
+ self.tts_delete_after = tts_delete_after
+
+ def run(self):
+ play_audio(self.file_path, self.tts_delete_after)
+ self.play_back_signal.emit(True)
+
+
+def play_audio(file_path: str, tts_delete_after: bool = False):
+ # global sound # Removed global sound variable
+ sound = None # Use local variable
+ channel = None # Initialize channel
+ try:
+ if not os.path.exists(file_path):
+ raise FileNotFoundError(f"音频文件不存在: {file_path}")
+
+ if not pygame.mixer.get_init():
+ try:
+ pygame.mixer.init(frequency=44100, size=-16, channels=2, buffer=512)
+ except pygame.error:
+ logger.warning("标准 Mixer 初始化失败,尝试兼容模式...")
+ try:
+ pygame.mixer.init(frequency=22050, size=-16, channels=1, buffer=1024)
+ logger.info("使用兼容设置成功初始化 Mixer")
+ except pygame.error as e_fallback:
+ logger.error(f"Pygame mixer 初始化失败: {e_fallback}")
+ return
+
+ # 检查文件是否可读
+ if os.path.getsize(file_path) <= 0:
+ start_time = time.time()
+ while time.time() - start_time < 4:
+ if os.path.getsize(file_path) > 0:
+ break
+ time.sleep(0.1)
+ else:
+ raise IOError("音频文件写入超时")
+
+ try:
+ if file_path in sound_cache:
+ sound = sound_cache[file_path]
+ logger.debug(f'使用缓存音频: {file_path}')
+ else:
+ sound = pygame.mixer.Sound(file_path)
+ sound_cache[file_path] = sound
+ logger.debug(f'缓存音频: {file_path}')
+ except pygame.error as e_load:
+ logger.error(f"加载音频文件失败: {file_path} | 错误: {e_load}")
+ return
+
+ volume = int(config_center.read_conf('Audio', 'volume')) / 100
+ sound.set_volume(volume) # 设置Sound对象的音量
+ channel = sound.play()
+ if channel:
+ channel.set_volume(volume) # 设置Channel对象的音量
+ while channel.get_busy():
+ pygame.time.wait(100)
+ else:
+ logger.error(f"无法获取播放通道: {file_path}")
+
+ logger.debug(f'成功播放音频: {file_path}')
+
+ if tts_delete_after:
+ tts = TTSEngine()
+ tts.delete_audio_file(file_path)
+
+ except FileNotFoundError as e:
+ logger.error(f'音频文件未找到 | 路径: {file_path} | 错误: {str(e)}')
+ except IOError as e:
+ logger.error(f'音频文件读取错误或超时 | 路径: {file_path} | 错误: {str(e)}')
+ except pygame.error as e:
+ logger.error(f'Pygame 播放错误 | 路径: {file_path} | 错误: {str(e)}')
+ except Exception as e:
+ logger.error(f'未知播放失败 | 路径: {file_path} | 错误: {str(e)}')
+ finally:
+ if channel:
+ channel.stop()
+ if sound:
+ sound.stop()
diff --git a/plugin.py b/plugin.py
new file mode 100644
index 0000000..aca1ea7
--- /dev/null
+++ b/plugin.py
@@ -0,0 +1,124 @@
+import importlib
+import json
+from pathlib import Path
+import shutil
+
+from loguru import logger
+
+import conf
+
+
+class PluginLoader: # 插件加载器
+ def __init__(self, p_mgr=None):
+ self.plugins_settings = {}
+ self.plugins_name = []
+ self.plugins_dict = {}
+ self.manager = p_mgr
+
+ def set_manager(self, p_mgr):
+ self.manager = p_mgr
+
+ def load_plugins(self):
+ for folder in Path(conf.PLUGINS_DIR).iterdir():
+ if folder.is_dir() and (folder / 'plugin.json').exists():
+ self.plugins_name.append(folder.name) # 检测所有插件
+
+ if folder.name not in conf.load_plugin_config()['enabled_plugins']:
+ continue
+ relative_path = conf.PLUGINS_DIR.name
+ module_name = f"{relative_path}.{folder.name}"
+ try:
+ module = importlib.import_module(module_name)
+
+ if hasattr(module, 'Settings'): # 设置页
+ plugin_class = getattr(module, "Settings") # 获取 Plugin 类
+ # 实例化插件
+ self.plugins_settings[folder.name] = plugin_class(f'{conf.PLUGINS_DIR}/{folder.name}')
+
+ if self.manager and hasattr(module, 'Plugin'): # 插件入口
+ plugin_class = getattr(module, "Plugin") # 获取 Plugin 类
+ # 实例化插件
+ self.plugins_dict[folder.name] = plugin_class(
+ self.manager.get_app_contexts(folder.name), self.manager.method
+ )
+
+ logger.success(f"加载插件成功:{module_name}")
+ except (ImportError, FileNotFoundError) as e:
+ logger.warning(f"加载插件 {folder.name} 失败: {e}. 可能缺少文件或依赖项。将禁用此插件。")
+ plugin_config = conf.load_plugin_config()
+ if folder.name in plugin_config['enabled_plugins']:
+ plugin_config['enabled_plugins'].remove(folder.name)
+ conf.save_plugin_config(plugin_config)
+ if folder.name in self.plugins_name:
+ self.plugins_name.remove(folder.name)
+ continue
+ except Exception as e:
+ logger.error(f"加载插件 {folder.name} 时发生未知错误: {e}")
+ # 大部分情况一般不会影响运行
+ continue
+ return self.plugins_name
+
+ def run_plugins(self):
+ for plugin in self.plugins_dict.values():
+ plugin.execute()
+
+ def update_plugins(self):
+ for plugin in self.plugins_dict.values():
+ if hasattr(plugin, 'update'):
+ plugin.update(self.manager.get_app_contexts())
+
+ def delete_plugin(self, plugin_name):
+ plugin_dir = Path(conf.PLUGINS_DIR) / plugin_name
+ if not plugin_dir.is_dir():
+ logger.warning(f"插件目录 {plugin_dir} 不存在,无法删除。")
+ return False
+ widgets_to_remove = []
+ if widgets_to_remove:
+ try:
+ widget_config_path = Path(conf.base_directory) / 'config' / 'widget.json'
+ if widget_config_path.exists():
+ with open(widget_config_path, 'r', encoding='utf-8') as f:
+ widget_config = json.load(f)
+
+ original_widgets = widget_config.get('widgets', [])
+ # 过滤掉要移除的组件
+ widget_config['widgets'] = [w for w in original_widgets if w not in widgets_to_remove]
+
+ with open(widget_config_path, 'w', encoding='utf-8') as f:
+ json.dump(widget_config, f, ensure_ascii=False, indent=4)
+ logger.info(f"已从 config/widget.json 中移除插件 {plugin_name} 的关联组件: {widgets_to_remove}")
+ else:
+ logger.warning(f"主配置文件 config/widget.json 不存在,无法移除插件组件。")
+ except Exception as e:
+ logger.error(f"更新 config/widget.json 失败: {e}")
+
+ if plugin_name in self.plugins_dict:
+ del self.plugins_dict[plugin_name]
+ logger.info(f"已移除正在运行的插件实例: {plugin_name}")
+ if plugin_name in self.plugins_settings:
+ del self.plugins_settings[plugin_name]
+ logger.info(f"已移除插件设置实例: {plugin_name}")
+
+ plugin_config = conf.load_plugin_config()
+ if plugin_name in plugin_config.get('enabled_plugins', []):
+ plugin_config['enabled_plugins'].remove(plugin_name)
+ conf.save_plugin_config(plugin_config)
+ logger.info(f"已从启用插件列表中移除: {plugin_name}")
+
+ if plugin_name in self.plugins_name:
+ self.plugins_name.remove(plugin_name)
+
+ try:
+ shutil.rmtree(plugin_dir)
+ logger.success(f"插件 {plugin_name} 已成功删除。")
+ return True
+ except Exception as e:
+ logger.error(f"删除插件目录 {plugin_dir} 失败: {e}")
+ return False
+
+p_loader = PluginLoader()
+
+
+if __name__ == '__main__':
+ p_loader.load_plugins()
+ p_loader.run_plugins()
diff --git a/plugin_plaza.py b/plugin_plaza.py
new file mode 100644
index 0000000..fcf8822
--- /dev/null
+++ b/plugin_plaza.py
@@ -0,0 +1,801 @@
+import json
+import sys
+from datetime import datetime
+from random import shuffle
+
+from PyQt5 import uic
+from PyQt5.QtCore import QSize, Qt, QTimer, QUrl, QStringListModel, pyqtSignal
+from PyQt5.QtGui import QIcon, QPixmap, QDesktopServices
+from PyQt5.QtWidgets import QApplication, QHBoxLayout, QVBoxLayout, QGridLayout, QSpacerItem, QSizePolicy, QWidget, \
+ QScroller, QCompleter
+from loguru import logger
+from qfluentwidgets import MSFluentWindow, FluentIcon as fIcon, NavigationItemPosition, TitleLabel, \
+ ImageLabel, StrongBodyLabel, HyperlinkLabel, CaptionLabel, PrimaryPushButton, HorizontalFlipView, \
+ InfoBar, InfoBarPosition, SplashScreen, MessageBoxBase, TransparentToolButton, BodyLabel, \
+ PrimarySplitPushButton, RoundMenu, Action, PipsPager, TextBrowser, CardWidget, \
+ IndeterminateProgressRing, ComboBox, ProgressBar, SmoothScrollArea, SearchLineEdit, HyperlinkButton, \
+ MessageBox, SwitchButton, SubtitleLabel
+
+import conf
+import list_ as l
+import network_thread as nt
+from conf import base_directory
+from file import config_center
+from plugin import p_loader
+from utils import restart, calculate_size
+import platform
+from loguru import logger
+
+# 适配高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标识')
+
+CONF_PATH = f"{base_directory}/plugins/plugins_from_pp.json"
+PLAZA_REPO_URL = "https://raw.githubusercontent.com/Class-Widgets/plugin-plaza/"
+PLAZA_REPO_DIR = "https://api.github.com/repos/Class-Widgets/plugin-plaza/contents/Plugins"
+TEST_DOWNLOAD_LINK = "https://dldir1.qq.com/qqfile/qq/PCQQ9.7.17/QQ9.7.17.29225.exe"
+
+restart_tips_flag = False # 重启提示
+plugins_data = {} # 仓库插件信息
+local_plugins_version = {} # 本地插件版本
+download_progress = [] # 下载线程
+
+installed_plugins = [] # 已安装插件(通过PluginPlaza获取)
+tags = ['示例', '信息展示', '学习', '测试', '工具', '自动化'] # 测试用TAG
+recommend_plugins = ['cw-example-plugin'] # 推荐插件(通过PluginPlaza获取)
+search_items = []
+SELF_PLUGIN_VERSION = config_center.read_conf('Plugin', 'version') # 自身版本号
+SEARCH_FIELDS = ["name", "description", "tag", "author"] # 搜索字段
+
+
+class TagLink(HyperlinkButton): # 标签链接
+ def __init__(self, text, parent=None):
+ super().__init__(parent)
+ self.parent = parent
+ self.tag = text
+ self.setText(text)
+ self.setIcon(fIcon.SEARCH)
+
+ self.setFixedHeight(30)
+ self.clicked.connect(self.search_tag)
+
+ def search_tag(self):
+ self.parent.search_plugin.setText(self.tag)
+ self.parent.search_plugin.searchSignal.emit(self.tag) # 发射搜索信号
+
+
+class downloadProgressBar(InfoBar): # 下载进度条(创建下载进程)
+ def __init__(self, url=TEST_DOWNLOAD_LINK, branch='main', name="Test", parent=None):
+ global download_progress
+ self.p_name = url.split('/')[4] # repo
+ # user = url.split('/')[3]
+ self.name = name
+ self.url = f'{url}/archive/refs/heads/{branch}.zip'
+
+ super().__init__(icon=fIcon.DOWNLOAD,
+ title='',
+ content=f"正在下载 {name} (~ ̄▽ ̄)~)",
+ orient=Qt.Horizontal,
+ isClosable=False,
+ position=InfoBarPosition.TOP,
+ duration=-1,
+ parent=parent
+ )
+ self.setCustomBackgroundColor('white', '#202020')
+ self.bar = ProgressBar()
+ self.bar.setFixedWidth(300)
+ self.cancelBtn = HyperlinkLabel()
+ self.cancelBtn.setText("取消")
+ self.cancelBtn.clicked.connect(self.cancelDownload)
+ self.addWidget(self.bar)
+ self.addWidget(self.cancelBtn)
+
+ # 开始下载
+
+ download_progress.append(self.p_name)
+ self.download(self.url)
+
+ def download(self, url): # 接受下载连接并开始任务
+ self.download_thread = nt.DownloadAndExtract(url, self.p_name)
+ # self.download_thread = nt.DownloadAndExtract(TEST_DOWNLOAD_LINK, self.p_name)
+ self.download_thread.progress_signal.connect(lambda progress: self.bar.setValue(int(progress))) # 下载进度
+ self.download_thread.status_signal.connect(self.detect_status) # 判断状态
+ self.download_thread.start()
+
+ def cancelDownload(self):
+ global download_progress
+ download_progress.remove(self.p_name)
+ self.download_thread.stop()
+ self.download_thread.deleteLater()
+ self.close()
+
+ def detect_status(self, status):
+ if status == "DOWNLOADING":
+ self.content = f"正在下载 {self.name} (~ ̄▽ ̄)~)"
+ elif status == "EXTRACTING":
+ self.content = f"正在解压 {self.name} ( •̀ ω •́ )✧)"
+ elif status == "DONE":
+ self.download_finished()
+ elif status.startswith("ERROR"):
+ self.download_error(status[6:])
+ else:
+ pass
+
+ def download_finished(self):
+ global download_progress
+ download_progress.remove(self.p_name)
+ add2save_plugin(self.p_name) # 保存到配置
+ self.download_thread.finished.emit()
+ self.download_thread.deleteLater()
+
+ InfoBar.success(
+ title='下载成功!',
+ content=f"下载 {self.name} 成功!",
+ orient=Qt.Horizontal,
+ isClosable=True,
+ position=InfoBarPosition.TOP,
+ duration=5000,
+ parent=self.parent()
+ )
+ if not restart_tips_flag: # 重启提示
+ self.parent().restart_tips()
+ self.close()
+
+ def download_error(self, error_info):
+ global download_progress
+ download_progress.remove(self.p_name)
+ InfoBar.error(
+ title='下载失败(っ °Д °;)っ',
+ content=f"{error_info}",
+ orient=Qt.Horizontal,
+ isClosable=True,
+ position=InfoBarPosition.TOP,
+ duration=5000,
+ parent=self.parent()
+ )
+ self.close()
+
+
+def install_plugin(parent, p_name, data):
+ plugin_ver = str(data.get('plugin_ver'))
+ if plugin_ver != SELF_PLUGIN_VERSION: # 插件版本不匹配
+ if plugin_ver > SELF_PLUGIN_VERSION:
+ content = (f'此插件版本({plugin_ver})高于当前设备中 Class Widgets 兼容的插件版本({SELF_PLUGIN_VERSION});\n'
+ f'请更新 Class Widgets 后再尝试安装此插件。')
+ else:
+ content = (f'此插件版本({plugin_ver})低于当前设备中 Class Widgets 兼容的插件版本({SELF_PLUGIN_VERSION});\n'
+ f'可能是插件缺乏维护,请联系插件作者更新插件,或在社区(GitHub、QQ群)中提出问题。')
+
+ cc = MessageBox(
+ "本插件不兼容当前版本的 Class Widgets",
+ f"{content}\n\n不建议安装此插件,否则将出现不可预料(包括崩溃、闪退等故障)的问题。",
+ parent
+ ) # 兼容性检查窗口
+ cc.yesButton.setText("取消安装")
+ cc.cancelButton.setText("强制安装(不建议)")
+ if cc.exec(): # 取消安装
+ return False
+
+ if p_name not in download_progress: # 如果正在下载
+ url = data.get("url")
+ branch = data.get("branch")
+ title = data.get("name")
+
+ di = downloadProgressBar(
+ url=f"{url}",
+ branch=branch,
+ name=title,
+ parent=parent
+ )
+ di.show()
+ return True
+ return False
+
+
+class PluginDetailPage(MessageBoxBase): # 插件详情页面
+ def __init__(self, icon, title, content, tag, version, author, url, data=None, parent=None):
+ super().__init__(parent)
+ self.data = data
+ self.branch = data.get("branch")
+ self.title = title
+ self.parent = parent
+ self.url = url
+ self.p_name = url.split('/')[-1] # repo
+ author_url = '/'.join(url.rsplit('/', 2)[:-1])
+ self.init_ui()
+ self.download_readme()
+ scroll_area_widget = self.findChild(QVBoxLayout, 'verticalLayout_9')
+
+ self.iconWidget = self.findChild(ImageLabel, 'pluginIcon')
+ self.iconWidget.setImage(icon)
+ self.iconWidget.setFixedSize(100, 100)
+ self.iconWidget.setBorderRadius(8, 8, 8, 8)
+
+ self.titleLabel = self.findChild(TitleLabel, 'titleLabel') # 标题
+ self.titleLabel.setText(title)
+
+ self.contentLabel = self.findChild(CaptionLabel, 'descLabel') # 描述
+ self.contentLabel.setText(content)
+
+ self.tagLabel = self.findChild(HyperlinkLabel, 'tagButton') # tag
+ self.tagLabel.setText(tag)
+
+ self.versionLabel = self.findChild(BodyLabel, 'versionLabel') # 版本
+ self.versionLabel.setText(version)
+
+ self.authorLabel = self.findChild(HyperlinkLabel, 'authorButton') # 作者
+ self.authorLabel.setText(author)
+ self.authorLabel.setUrl(author_url)
+
+ self.openGitHub = self.findChild(TransparentToolButton, 'openGitHub') # 打开连接
+ self.openGitHub.setIcon(fIcon.LINK)
+ self.openGitHub.setIconSize(QSize(18, 18))
+ self.openGitHub.clicked.connect(lambda: QDesktopServices.openUrl(QUrl(url)))
+
+ self.installButton = self.findChild(PrimarySplitPushButton, 'installButton')
+ self.installButton.setText(" 安装 ")
+ self.installButton.setIcon(fIcon.DOWNLOAD)
+ self.installButton.clicked.connect(self.install)
+
+ if self.p_name in download_progress: # 如果正在下载
+ self.installButton.setText(" 安装中 ")
+ self.installButton.setEnabled(False)
+ if self.p_name in installed_plugins: # 如果已安装
+ self.installButton.setText(" 已安装 ")
+ self.installButton.setEnabled(False)
+
+ if self.p_name in local_plugins_version: # 如果本地版本低于仓库版本
+ print(local_plugins_version[self.p_name], version)
+ if local_plugins_version[self.p_name] < version:
+ self.installButton.setText("更新")
+ self.installButton.setIcon(fIcon.SYNC)
+ self.installButton.setEnabled(True)
+
+ menu = RoundMenu(parent=self.installButton)
+ menu.addActions([
+ Action(fIcon.DOWNLOAD, "为 Class Widgets 安装", triggered=self.install),
+ Action(fIcon.LINK, "下载到本地",
+ triggered=lambda: QDesktopServices.openUrl(QUrl(f"{url}/releases/latest")))
+ ])
+ self.installButton.setFlyout(menu)
+
+ self.readmePage = TextBrowser(self)
+ self.readmePage.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
+ self.readmePage.setReadOnly(True)
+ scroll_area_widget.addWidget(self.readmePage)
+
+ def install(self):
+ if install_plugin(self.parent, self.p_name, self.data):
+ self.installButton.setText(" 安装中 ")
+ self.installButton.setEnabled(False)
+
+ def download_readme(self):
+ def display_readme(markdown_text):
+ self.readmePage.setMarkdown(markdown_text)
+
+ if self.data is None:
+ self.download_thread = nt.getReadme(f"{replace_to_file_server(self.url)}/README.md")
+ else:
+ self.download_thread = nt.getReadme(f"{replace_to_file_server(self.url, self.data['branch'])}/README.md")
+ self.download_thread.html_signal.connect(display_readme)
+ self.download_thread.start()
+
+ def init_ui(self):
+ # 加载ui文件
+ self.temp_widget = QWidget()
+ uic.loadUi(f'{base_directory}/view/pp/plugin_detail.ui', self.temp_widget)
+ self.viewLayout.addWidget(self.temp_widget)
+ self.viewLayout.setContentsMargins(0, 0, 0, 0)
+ # 隐藏原有按钮
+ self.yesButton.hide()
+ self.cancelButton.hide()
+ self.buttonGroup.hide()
+
+ # 自定关闭按钮
+ self.closeButton = self.findChild(TransparentToolButton, 'closeButton')
+ self.closeButton.setIcon(fIcon.CLOSE)
+ self.closeButton.clicked.connect(self.close)
+
+ self.widget.setMinimumWidth(875)
+ self.widget.setMinimumHeight(625)
+
+
+class PluginCard_Horizontal(CardWidget): # 插件卡片(横向)
+ def __init__(
+ self, icon='img/plaza/plugin_pre.png', title='Plugin Name', content='Description...', tag='Unknown',
+ version='1.0.0', author="CW Support",
+ url="https://github.com/RinLit-233-shiroko/cw-example-plugin", data=None, parent=None):
+ super().__init__(parent)
+ self.icon = icon
+ self.title = title
+ self.plugin_ver = data.get('plugin_ver')
+ self.parent = parent
+ self.tag = tag
+ self.branch = data.get("branch")
+ self.url = url
+ self.p_name = url.split('/')[-1] # repo
+ self.data = data
+ author_url = '/'.join(self.url.rsplit('/', 2)[:-1])
+
+ self.iconWidget = ImageLabel(icon) # 插件图标
+ self.titleLabel = StrongBodyLabel(title, self) # 插件名
+ self.versionLabel = CaptionLabel(version, self) # 插件版本
+ self.authorLabel = HyperlinkLabel() # 插件作者
+ self.contentLabel = CaptionLabel(content, self) # 插件描述
+ self.installButton = PrimaryPushButton()
+
+ # layout
+ self.hBoxLayout = QHBoxLayout()
+ self.hBoxLayout_Title = QHBoxLayout()
+ self.hBoxLayout_Author = QHBoxLayout()
+ self.vBoxLayout = QVBoxLayout()
+
+ self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
+ self.setFixedHeight(110)
+ self.setMinimumWidth(250)
+ self.authorLabel.setText(author)
+ self.authorLabel.setUrl(author_url)
+ self.authorLabel.setSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Fixed)
+ self.iconWidget.setFixedSize(84, 84)
+ self.iconWidget.setBorderRadius(5, 5, 5, 5) # 圆角
+ self.contentLabel.setTextColor("#606060", "#d2d2d2")
+ self.contentLabel.setWordWrap(True)
+ self.versionLabel.setTextColor("#999999", "#999999")
+ self.titleLabel.setSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Fixed)
+
+ self.installButton.setText("安装")
+ self.installButton.setMaximumSize(100, 36)
+ self.installButton.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
+ self.installButton.setIcon(fIcon.DOWNLOAD)
+ self.installButton.clicked.connect(self.install)
+
+ if self.p_name in installed_plugins: # 如果已安装
+ self.installButton.setText("已安装")
+ self.installButton.setEnabled(False)
+
+ if self.p_name in local_plugins_version: # 如果本地版本低于仓库版本
+ print(local_plugins_version[self.p_name], version)
+ if local_plugins_version[self.p_name] < version:
+ self.installButton.setText("更新")
+ self.installButton.setIcon(fIcon.SYNC)
+ self.installButton.setEnabled(True)
+
+ self.hBoxLayout.setContentsMargins(20, 11, 11, 11)
+ self.hBoxLayout.setSpacing(15)
+ self.hBoxLayout.addWidget(self.iconWidget)
+
+ self.blank = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
+
+ self.vBoxLayout.setContentsMargins(0, 5, 0, 5)
+ self.vBoxLayout.setSpacing(0)
+ self.vBoxLayout.addLayout(self.hBoxLayout_Title)
+ self.vBoxLayout.addLayout(self.hBoxLayout_Author)
+ self.vBoxLayout.addItem(self.blank)
+ self.vBoxLayout.addWidget(self.contentLabel, 0, Qt.AlignmentFlag.AlignTop)
+ self.vBoxLayout.setAlignment(Qt.AlignmentFlag.AlignVCenter)
+
+ self.hBoxLayout.addLayout(self.vBoxLayout)
+ self.hBoxLayout.addWidget(self.installButton)
+ self.setLayout(self.hBoxLayout)
+
+ self.hBoxLayout_Title.setSpacing(12)
+ self.hBoxLayout_Title.addWidget(self.titleLabel, 0, Qt.AlignmentFlag.AlignVCenter)
+ self.hBoxLayout_Title.addWidget(self.versionLabel, 0, Qt.AlignmentFlag.AlignVCenter)
+
+ self.hBoxLayout_Author.addWidget(self.authorLabel, 0, Qt.AlignmentFlag.AlignLeft)
+
+ def install(self):
+ install_plugin(self.parent, self.p_name, self.data)
+
+ def set_img(self, img):
+ try:
+ self.icon = img
+ self.iconWidget.setImage(img)
+ self.iconWidget.setFixedSize(84, 84)
+ except Exception as e:
+ logger.error(f"设置插件图片失败: {e}")
+
+ def show_detail(self):
+ w = PluginDetailPage(
+ icon=self.icon, title=self.title, content=self.contentLabel.text(),
+ tag=self.tag, version=self.versionLabel.text(), author=self.authorLabel.text(),
+ url=self.url, data=self.data, parent=self.parent
+ )
+ w.exec()
+
+
+class PluginPlaza(MSFluentWindow):
+ closed = pyqtSignal()
+
+ def __init__(self):
+ super().__init__()
+ self.splashScreen = None
+ global installed_plugins
+ try:
+ with open(CONF_PATH, 'r', encoding='utf-8') as file:
+ installed_plugins = json.load(file).get('plugins')
+ # 校验
+ for plugin in installed_plugins:
+ if plugin not in p_loader.plugins_name:
+ logger.warning(f"已在插件广场安装的插件 {plugin} 未找到,可能已遭删除")
+ installed_plugins.remove(plugin)
+ except Exception as e:
+ logger.error(f"读取已安装的插件失败: {e}")
+ try:
+ self.homeInterface = uic.loadUi(f'{base_directory}/view/pp/home.ui') # 首页
+ self.homeInterface.setObjectName("homeInterface")
+ self.latestsInterface = uic.loadUi(f'{base_directory}/view/pp/latests.ui') # 最新更新
+ self.latestsInterface.setObjectName("latestInterface")
+ self.settingsInterface = uic.loadUi(f'{base_directory}/view/pp/settings.ui') # 设置
+ self.settingsInterface.setObjectName("settingsInterface")
+ self.searchInterface = uic.loadUi(f'{base_directory}/view/pp/search.ui') # 搜索
+ self.searchInterface.setObjectName("searchInterface")
+
+ load_local_plugins_version() # 加载本地插件版本
+ self.init_nav()
+ self.init_window()
+ self.get_pp_data()
+ self.get_banner_img()
+ except Exception as e:
+ logger.error(f'初始化插件广场时发生错误:{e}')
+
+ def load_all_interface(self):
+ self.setup_homeInterface()
+ self.setup_latestInterface()
+ self.setup_settingsInterface()
+ self.setup_searchInterface()
+
+ def setup_latestInterface(self): # 初始化最新更新
+ latest_scroll = self.latestsInterface.findChild(SmoothScrollArea, 'latest_scroll')
+ QScroller.grabGesture(latest_scroll.viewport(), QScroller.LeftMouseButtonGesture)
+
+ def setup_searchInterface(self): # 初始化搜索
+ search_scroll = self.searchInterface.findChild(SmoothScrollArea, 'search_scroll')
+
+ def search(keyword): # 搜索
+ if keyword == '/all':
+ return plugins_data
+
+ result = {}
+ for key, value in plugins_data.items():
+ if any(keyword.lower() in str(value.get(field, "")).lower() for field in SEARCH_FIELDS):
+ result[key] = value
+ return result
+
+ def clear_results():
+ for i in reversed(range(self.search_plugin_grid.count())):
+ widget = self.search_plugin_grid.itemAt(i).widget()
+ if widget:
+ widget.setParent(None) # 移除控件
+ widget.destroy() # 销毁控件
+
+ def search_plugins(): # 搜索插件
+ if not plugins_data:
+ return
+ clear_results()
+
+ if self.search_plugin.text():
+ def set_plugin_image(plugin_card, data):
+ pixmap = QPixmap()
+ pixmap.loadFromData(data)
+ plugin_card.set_img(pixmap)
+
+ keyword = self.search_plugin.text()
+ print(f'结果:{search(keyword)}') # 结果
+ plugin_num = 0 # 计数
+ for key, data in search(keyword).items():
+ plugin_card = PluginCard_Horizontal(title=data['name'], content=data['description'],
+ tag=data['tag'], version=data['version'], url=data['url'],
+ author=data['author'], data=data, parent=self)
+ plugin_card.clicked.connect(plugin_card.show_detail) # 点击事件
+
+ # 启动线程加载图片
+ image_thread = nt.getImg(f"{replace_to_file_server(data['url'], data['branch'])}/icon.png")
+ image_thread.repo_signal.connect(
+ lambda img_data, card=plugin_card: set_plugin_image(card, img_data))
+ image_thread.start()
+
+ self.search_plugin_grid.addWidget(plugin_card, plugin_num // 2, plugin_num % 2) # 排列
+ plugin_num += 1
+
+ self.search_plugin_grid = self.searchInterface.findChild(QGridLayout, 'search_plugin_grid') # 插件表格
+ self.tags_layout = self.searchInterface.findChild(QGridLayout, 'tags_layout') # tag 布局
+ self.search_plugin = self.searchInterface.findChild(SearchLineEdit, 'search_plugin')
+ self.search_plugin.searchSignal.connect(search_plugins)
+ self.search_plugin.returnPressed.connect(search_plugins)
+ self.search_plugin.clearSignal.connect(clear_results)
+ self.search_completer = QCompleter(search_items, self.search_plugin)
+ # 设置显示的选项数
+ self.search_completer.setMaxVisibleItems(10)
+ self.search_completer.setFilterMode(Qt.MatchContains) # 内容匹配
+ self.search_completer.setCaseSensitivity(Qt.CaseInsensitive) # 不区分大小写
+ self.search_completer.activated.connect(search_plugins)
+ self.search_plugin.setCompleter(self.search_completer)
+
+ QScroller.grabGesture(search_scroll.viewport(), QScroller.LeftMouseButtonGesture)
+
+ def setup_settingsInterface(self): # 初始化设置
+ # 选择代理
+ select_mirror = self.settingsInterface.findChild(ComboBox, 'select_proxy')
+ select_mirror.addItems(nt.mirror_list)
+ select_mirror.setCurrentIndex(nt.mirror_list.index(config_center.read_conf('Plugin', 'mirror')))
+ select_mirror.currentIndexChanged.connect(
+ lambda: config_center.write_conf('Plugin', 'mirror', select_mirror.currentText()))
+
+ # 开关自动启用插件
+ auto_enable_plugin = self.settingsInterface.findChild(SwitchButton, 'auto_enable_plugin')
+ auto_enable_plugin.setChecked(int(config_center.read_conf('Plugin', 'auto_enable_plugin')))
+ auto_enable_plugin.checkedChanged.connect(
+ lambda: config_center.write_conf('Plugin', 'auto_enable_plugin', int(auto_enable_plugin.isChecked()))
+ )
+
+ def setup_homeInterface(self): # 初始化首页
+ # 标题和副标题
+ home_scroll = self.homeInterface.findChild(SmoothScrollArea, 'home_scroll')
+ time_today_label = self.homeInterface.findChild(TitleLabel, 'time_today_label')
+ time_today_label.setText(f"{datetime.now().month}月{datetime.now().day}日 {l.week[datetime.now().weekday()]}")
+
+ # Banner
+ self.banner_view = self.homeInterface.findChild(HorizontalFlipView, 'banner_view')
+ self.banner_view.setAspectRatioMode(Qt.AspectRatioMode.KeepAspectRatio)
+ self.banner_view.setItemSize(QSize(900, 450)) # 设置图片大小(banner图片尺寸比)
+ self.banner_view.setBorderRadius(8)
+ self.banner_view.setSpacing(5)
+ self.banner_view.clicked.connect(self.open_banner_link)
+
+ self.auto_play_timer = QTimer(self) # 自动轮播
+ self.auto_play_timer.timeout.connect(lambda: self.switch_banners())
+ self.auto_play_timer.setInterval(2500)
+
+ # 翻页
+ self.banner_pager = self.homeInterface.findChild(PipsPager, 'banner_pager')
+ self.banner_pager.setVisibleNumber(5)
+ self.banner_pager.currentIndexChanged.connect(
+ lambda: (self.banner_view.scrollToIndex(self.banner_pager.currentIndex()),
+ self.auto_play_timer.stop(),
+ self.auto_play_timer.start(2500))
+ )
+ QScroller.grabGesture(home_scroll.viewport(), QScroller.LeftMouseButtonGesture)
+
+ def open_banner_link(self):
+ if not hasattr(self, 'img_list'):
+ QDesktopServices.openUrl(QUrl(
+ 'https://www.yuque.com/rinlit/class-widgets_help/ez4vv7tv8wikxc0s#Se2Bb'
+ ))
+ return False # 没有图片
+
+ if self.img_list[self.banner_view.currentIndex()] in self.banners_data:
+ if not self.banners_data[self.img_list[self.banner_view.currentIndex()]]['link']:
+ return False # 无链接
+ QDesktopServices.openUrl(QUrl(
+ self.banners_data[self.img_list[self.banner_view.currentIndex()]]['link']
+ ))
+
+ def set_tags_data(self, data):
+ global tags, search_items, recommend_plugins
+ rec_data = {}
+ if data:
+ tags = data.get('tags')
+ recommend_plugins = data.get('recommend_plugin')
+ shuffle(tags) # 随机
+ for tag in tags:
+ search_items.append(tag)
+ self.search_completer.setModel(QStringListModel(search_items)) # 设置搜索提示
+ tag_num = 0 # 计数
+ for tag in tags[:6]:
+ tag_link = TagLink(tag, self)
+ self.tags_layout.addWidget(tag_link, tag_num // 3, tag_num % 3) # 排列
+ tag_num += 1
+
+ for key, data in plugins_data.items():
+ if key in recommend_plugins:
+ rec_data[key] = data
+ self.load_plugins(rec_data, 'home')
+
+ def load_plugins(self, p_data, page):
+ global search_items
+
+ for plugin in p_data.values(): # 遍历插件数据
+ search_items.append(plugin['name'])
+ if plugin['author'] not in search_items:
+ search_items.append(plugin['author'])
+ self.search_completer.setModel(QStringListModel(search_items)) # 设置搜索提示
+
+ def set_plugin_image(plugin_card, data):
+ pixmap = QPixmap()
+ pixmap.loadFromData(data)
+ plugin_card.set_img(pixmap)
+
+ if page == 'latest':
+ self.plugin_grid = self.latestsInterface.findChild(QGridLayout, 'all_plugin_grid') # 插件表格
+ elif page == 'home':
+ self.plugin_grid = self.homeInterface.findChild(QGridLayout, 'rec_plugin_grid') # 插件表格
+ else:
+ self.plugin_grid = self.latestsInterface.findChild(QGridLayout, 'all_plugin_grid') # 插件表格
+ plugin_num = 0 # 计数
+
+ for plugin, data in p_data.items(): # 遍历插件数据
+ plugin_card = PluginCard_Horizontal(title=data['name'], content=data['description'],
+ tag=data['tag'], version=data['version'], url=data['url'],
+ author=data['author'], data=data, parent=self)
+ plugin_card.clicked.connect(plugin_card.show_detail) # 点击事件
+
+ # 启动线程加载图片
+ image_thread = nt.getImg(f"{replace_to_file_server(data['url'], data['branch'])}/icon.png")
+ image_thread.repo_signal.connect(lambda img_data, card=plugin_card: set_plugin_image(card, img_data))
+ image_thread.start()
+
+ self.plugin_grid.addWidget(plugin_card, plugin_num // 2, plugin_num % 2) # 排列
+ plugin_num += 1
+
+ self.homeInterface.findChild(IndeterminateProgressRing, 'load_plugin_progress').hide()
+
+ def get_banner_img(self):
+ def display_banner(data, index=0):
+ if index == 0:
+ self.auto_play_timer.start()
+ if data:
+ pixmap = QPixmap()
+ pixmap.loadFromData(data)
+ self.banner_view.setItemImage(index, pixmap)
+ self.splashScreen.hide()
+
+ def get_banner(data=dict):
+ try:
+ if 'error' not in data:
+ self.banners_data = data
+ self.img_list = self.img_links = list(data.keys())
+ self.img_links = [f'https://raw.githubusercontent.com/Class-Widgets/plugin-plaza/main/Banner/'
+ f'{img}.png' for img in self.img_links]
+ self.banner_pager.setPageNumber(len(data))
+ banner_placeholders = ["img/plaza/banner_pre.png" for _ in range(len(data))]
+ self.banner_view.addImages(banner_placeholders)
+ else:
+ error_info = data.get("error", "未知错误")
+ logger.error(f'PluginPlaza 无法联网,错误:{error_info}')
+ self.findChild(BodyLabel, 'tips').setText(f'错误原因:{error_info}')
+ self.banner_view.addImage("img/plaza/banner_network-failed.png")
+ self.banner_view.addImage("img/plaza/banner_network-failed.png")
+ self.splashScreen.hide()
+ self.homeInterface.findChild(SubtitleLabel, 'SubtitleLabel_3').hide() # 隐藏副标题
+ return
+
+ # 定义一个内部函数来启动下一个线程
+ def start_next_banner(index):
+ if index < len(data):
+ self.banner_thread = nt.getImg(self.img_links[index])
+ self.banner_thread.repo_signal.connect(lambda data: display_banner(data, index))
+ self.banner_thread.repo_signal.connect(lambda: start_next_banner(index + 1)) # 连接完成信号
+ self.banner_thread.start()
+
+ start_next_banner(0) # 启动第一个线程
+
+ except Exception as e:
+ logger.error(f"获取Banner失败:{e}")
+
+ self.banner_list_thread = nt.getRepoFileList()
+ self.banner_list_thread.repo_signal.connect(get_banner)
+ self.banner_list_thread.start()
+
+ def restart_tips(self):
+ global restart_tips_flag
+ restart_tips_flag = True
+ w = InfoBar.info(
+ title='需要重启',
+ content='若要应用插件配置,需重启 Class Widgets',
+ orient=Qt.Horizontal,
+ isClosable=True,
+ position=InfoBarPosition.TOP,
+ duration=-1,
+ parent=self
+ )
+ restart_btn = HyperlinkLabel('现在重启')
+ restart_btn.clicked.connect(restart)
+ w.addWidget(restart_btn)
+ w.show()
+
+ def get_pp_data(self):
+ global plugins_data
+ def callback(data):
+ global plugins_data
+ plugins_data = data # 保存插件数据
+ self.load_plugins(data, 'latest')
+ self.get_tags_data()
+
+ self.get_plugin_list_thread = nt.getPluginInfo()
+ self.get_plugin_list_thread.repo_signal.connect(callback)
+ self.get_plugin_list_thread.start()
+
+ def get_tags_data(self):
+ self.get_tags_list_thread = nt.getTags()
+ self.get_tags_list_thread.repo_signal.connect(self.set_tags_data)
+ self.get_tags_list_thread.start()
+
+ def switch_banners(self): # 切换Banner
+ if self.banner_view.currentIndex() == len(self.img_list) - 1:
+ self.banner_view.scrollToIndex(0)
+ self.banner_pager.setCurrentIndex(0)
+ else:
+ self.banner_view.scrollNext()
+ self.banner_pager.setCurrentIndex(self.banner_view.currentIndex())
+
+ def init_nav(self):
+ self.addSubInterface(self.homeInterface, fIcon.HOME, '首页', fIcon.HOME_FILL)
+ self.addSubInterface(self.latestsInterface, fIcon.LIBRARY, '分类', fIcon.LIBRARY_FILL)
+ self.addSubInterface(
+ self.searchInterface, fIcon.SEARCH, '搜索', position=NavigationItemPosition.BOTTOM
+ )
+ self.addSubInterface(
+ self.settingsInterface, fIcon.SETTING, '设置', fIcon.SETTING, position=NavigationItemPosition.BOTTOM
+ )
+
+ def init_window(self):
+ self.load_all_interface()
+ self.init_font()
+
+ self.setMinimumWidth(850)
+ self.setMinimumHeight(500)
+ self.setWindowTitle('插件广场')
+ self.setWindowIcon(QIcon(f'{base_directory}/img/pp_favicon.png'))
+
+ # 设置窗口大小
+ size, pos = calculate_size()
+
+ self.move(pos[0], pos[1])
+ self.resize(size[0], size[1])
+
+ # 启动屏幕
+ self.splashScreen = SplashScreen(self.windowIcon(), self)
+ self.splashScreen.setIconSize(QSize(102, 102))
+ self.show()
+
+ def init_font(self): # 设置字体
+ self.setStyleSheet("""QLabel {
+ font-family: 'Microsoft YaHei';
+ }""")
+
+ def closeEvent(self, event):
+ self.closed.emit()
+ event.accept()
+
+
+def add2save_plugin(p_name): # 保存已安装插件
+ global installed_plugins
+ installed_plugins.append(p_name)
+ try:
+ with open(CONF_PATH, 'r+', encoding='utf-8') as f:
+ if p_name not in json.load(f)['plugins']:
+ f.seek(0) # 指针指向开头
+ json.dump({"plugins": installed_plugins}, f, ensure_ascii=False, indent=4)
+ f.truncate() # 截断文件
+ except Exception as e:
+ logger.error(f"保存已安装插件失败:{e}")
+
+
+def replace_to_file_server(url, branch='main'):
+ return (f'{url.replace("https://github.com/", "https://raw.githubusercontent.com/")}'
+ f'/{branch}')
+
+
+def load_local_plugins_version():
+ global local_plugins_version
+ for plugin in installed_plugins:
+ try:
+ with open(f"plugins/{plugin}/plugin.json", 'r', encoding='utf-8') as f:
+ data = json.load(f)
+ local_plugins_version[plugin] = data['version']
+ except Exception as e:
+ logger.error(f"加载本地插件版本失败:{e}")
+ print(local_plugins_version)
+
+
+if __name__ == '__main__':
+ app = QApplication(sys.argv)
+ pp = PluginPlaza()
+ pp.show()
+ sys.exit(app.exec())
diff --git a/plugins/plugins_from_pp.json b/plugins/plugins_from_pp.json
new file mode 100644
index 0000000..80a6988
--- /dev/null
+++ b/plugins/plugins_from_pp.json
@@ -0,0 +1,5 @@
+{
+ "plugins": [
+
+ ]
+}
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..2fa5cbf
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,32 @@
+# ---------------------------------
+# System platform value
+# ---------------------------------
+# Windows "win32"
+# ---------------------------------
+certifi==2024.8.30
+charset-normalizer==3.3.2
+colorama==0.4.6
+configparser~=7.1.0
+darkdetect==0.8.0
+idna==3.10
+loguru~=0.7.2
+PyQt5~=5.15.11
+PyQt-Fluent-Widgets~=1.8.1
+pyqt5-frameless-window==0.4.3
+pyqt5-qt5~=5.15.2
+pyqt5-sip==12.15.0
+pyqt5-stubs==5.15.6.0
+pywin32==306; platform_system == "Windows"
+psutil==5.9.5
+requests==2.32.3
+urllib3==2.2.3
+win32-setctime==1.1.0
+PyQtWebEngine~=5.15.7
+Markdown~=3.7
+pygame~=2.6.1
+packaging~=24.2
+PyGetWindow~=0.0.9
+edge-tts~=7.0.0
+pyttsx3==2.98
+python-dateutil==2.8.2
+git+https://github.com/SmartTeachCN/pycses.git
diff --git a/tip_toast.py b/tip_toast.py
new file mode 100644
index 0000000..df9d6a8
--- /dev/null
+++ b/tip_toast.py
@@ -0,0 +1,460 @@
+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())
diff --git a/ui/default/dark/preview/widget-countdown-day.png b/ui/default/dark/preview/widget-countdown-day.png
new file mode 100644
index 0000000..6f3db4b
Binary files /dev/null and b/ui/default/dark/preview/widget-countdown-day.png differ
diff --git a/ui/default/dark/preview/widget-countdown.png b/ui/default/dark/preview/widget-countdown.png
new file mode 100644
index 0000000..46514c1
Binary files /dev/null and b/ui/default/dark/preview/widget-countdown.png differ
diff --git a/ui/default/dark/preview/widget-current-activity.png b/ui/default/dark/preview/widget-current-activity.png
new file mode 100644
index 0000000..9797024
Binary files /dev/null and b/ui/default/dark/preview/widget-current-activity.png differ
diff --git a/ui/default/dark/preview/widget-custom.png b/ui/default/dark/preview/widget-custom.png
new file mode 100644
index 0000000..c714428
Binary files /dev/null and b/ui/default/dark/preview/widget-custom.png differ
diff --git a/ui/default/dark/preview/widget-next-activity.png b/ui/default/dark/preview/widget-next-activity.png
new file mode 100644
index 0000000..42ede81
Binary files /dev/null and b/ui/default/dark/preview/widget-next-activity.png differ
diff --git a/ui/default/dark/preview/widget-time.png b/ui/default/dark/preview/widget-time.png
new file mode 100644
index 0000000..8748127
Binary files /dev/null and b/ui/default/dark/preview/widget-time.png differ
diff --git a/ui/default/dark/preview/widget-weather.png b/ui/default/dark/preview/widget-weather.png
new file mode 100644
index 0000000..02db73d
Binary files /dev/null and b/ui/default/dark/preview/widget-weather.png differ
diff --git a/ui/default/dark/toast-open_dialog.ui b/ui/default/dark/toast-open_dialog.ui
new file mode 100644
index 0000000..aa33d18
--- /dev/null
+++ b/ui/default/dark/toast-open_dialog.ui
@@ -0,0 +1,247 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 281
+ 112
+
+
+
+
+ 200
+ 0
+
+
+
+ Form
+
+
+
+ 8
+
+
+ 8
+
+
+ 8
+
+
+ 22
+
+ -
+
+
+ background-color: rgba(10, 10, 15, 245);
+border-radius: 38px
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+
+ 4
+
+
+ 10
+
+
+ 10
+
+
+ 10
+
+
+ 10
+
+ -
+
+ -
+
+
+ 12
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 58
+ 58
+
+
+
+
+ 58
+ 58
+
+
+
+
+ Microsoft YaHei
+ 12
+ 75
+ true
+
+
+
+ font-weight: bold;
+
+
+ false
+
+
+ %p
+
+
+ 0.000000000000000
+
+
+ 7
+
+
+
+ -
+
+
+ 0
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ Microsoft YaHei UI
+ 12
+ 75
+ true
+
+
+
+ color: rgba(188, 188, 188, 200);
+background-color: rgba(0,0,0,0);
+font-weight: bold;
+
+
+ 即将打开
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ Microsoft YaHei UI
+ 17
+ 75
+ true
+
+
+
+ color: rgba(255, 255, 255, 255);
+background-color: rgba(0,0,0,0);
+font-weight: bold
+
+
+ 测试
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
+
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ 取消
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ HyperlinkButton
+ PushButton
+
+
+
+ PushButton
+ QPushButton
+
+
+
+ ProgressBar
+ QProgressBar
+
+
+
+ ProgressRing
+ ProgressBar
+
+
+
+
+
+
diff --git a/ui/default/dark/widget-base.ui b/ui/default/dark/widget-base.ui
new file mode 100644
index 0000000..9be7802
--- /dev/null
+++ b/ui/default/dark/widget-base.ui
@@ -0,0 +1,146 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 200
+ 125
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 基本组件
+
+
+
+ 20
+
+
+ 8
+
+
+ 8
+
+
+ 8
+
+
+ 24
+
+ -
+
+
+ background-color: rgba(15, 18, 22, 255);
+border-radius: 8px
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+
+ 0
+
+
+ 14
+
+
+ 14
+
+ -
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ color: rgba(255, 255, 255, 150);
+font-weight: bold;
+background-color: rgba(255, 255, 255, 0);
+
+
+ Title
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ -
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ Microsoft YaHei UI
+ 21
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(255, 255, 255, 255);
+font-weight: bold;
+background-color: rgba(255, 255, 255, 0);
+
+
+ Content
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/default/dark/widget-countdown-day.ui b/ui/default/dark/widget-countdown-day.ui
new file mode 100644
index 0000000..4848652
--- /dev/null
+++ b/ui/default/dark/widget-countdown-day.ui
@@ -0,0 +1,114 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 200
+ 125
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 倒计日
+
+
+
+
+ 20
+ 20
+ 161
+ 31
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ color: rgba(255, 255, 255, 150);
+font-weight: bold;
+
+
+ 距离 中考 还有
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 20
+ 30
+ 161
+ 71
+
+
+
+
+ Microsoft YaHei UI
+ 22
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(255, 255, 255, 255);
+font-weight: bold
+
+
+ 300 天
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 10
+ 10
+ 181
+ 91
+
+
+
+ background-color: rgba(15, 18, 22, 255);
+border-radius: 8px
+
+
+
+
+
+ backgnd
+ countdown_custom_title
+ custom_countdown
+
+
+
+
diff --git a/ui/default/dark/widget-countdown.ui b/ui/default/dark/widget-countdown.ui
new file mode 100644
index 0000000..9d99548
--- /dev/null
+++ b/ui/default/dark/widget-countdown.ui
@@ -0,0 +1,156 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 200
+ 125
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 活动倒计时
+
+
+
+
+ 20
+ 90
+ 161
+ 5
+
+
+
+
+ 0
+ 5
+
+
+
+
+ 16777215
+ 6
+
+
+
+
+
+
+ Qt::Horizontal
+
+
+ true
+
+
+ 0.000000000000000
+
+
+
+
+
+ 20
+ 20
+ 161
+ 31
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ color: rgba(255, 255, 255, 150);
+font-weight: bold;
+
+
+ 倒计时
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 20
+ 40
+ 161
+ 51
+
+
+
+
+ Microsoft YaHei UI
+ 21
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(255, 255, 255, 255);
+font-weight: bold
+
+
+ 00:00
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 10
+ 10
+ 181
+ 91
+
+
+
+ background-color: rgba(15, 18, 22, 255);
+border-radius: 8px
+
+
+
+
+
+ backgnd
+ progressBar
+ activity_countdown_title
+ activity_countdown
+
+
+
+ ProgressBar
+ QProgressBar
+
+
+
+
+
+
diff --git a/ui/default/dark/widget-current-activity.ui b/ui/default/dark/widget-current-activity.ui
new file mode 100644
index 0000000..9cdcb40
--- /dev/null
+++ b/ui/default/dark/widget-current-activity.ui
@@ -0,0 +1,141 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 360
+ 125
+
+
+
+
+ 0
+ 100
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 当前活动
+
+
+
+
+ 10
+ 40
+ 341
+ 51
+
+
+
+
+ Microsoft YaHei UI
+ 22
+ 75
+ true
+
+
+
+ QPushButton {
+ qproperty-iconSize: 24px;
+ color: white;
+ border: none;
+ color: rgba(255, 255, 255, 255);
+ font-weight: bold
+ }
+
+
+
+ 测试
+
+
+
+ ../../../img/it.svg ../../../img/it.svg
+
+
+
+ 24
+ 24
+
+
+
+
+
+
+ 170
+ 40
+ 35
+ 35
+
+
+
+ background-color: rgb(0, 255, 127);
+border-radius:20px
+
+
+
+
+
+
+
+
+ 30
+ 0
+ 301
+ 71
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ color: rgba(255, 255, 255, 150);
+font-weight: bold;
+
+
+ 当前活动
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 10
+ 10
+ 341
+ 91
+
+
+
+ background-color: rgba(15, 18, 22, 255);
+border-radius: 8px
+
+
+
+
+
+ backgnd
+ blurEffect
+ sub_title
+ subject
+
+
+
+
diff --git a/ui/default/dark/widget-floating.ui b/ui/default/dark/widget-floating.ui
new file mode 100644
index 0000000..ebd3c78
--- /dev/null
+++ b/ui/default/dark/widget-floating.ui
@@ -0,0 +1,252 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 200
+ 145
+
+
+
+
+ 200
+ 0
+
+
+
+ Form
+
+
+
+ 8
+
+
+ 8
+
+
+ 8
+
+
+ 22
+
+ -
+
+
+ background-color: rgba(15, 18, 22, 255);
+border-radius: 8px
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+
+ 4
+
+
+ 16
+
+
+ 4
+
+
+ 16
+
+
+ 10
+
+ -
+
+ -
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::Maximum
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 50
+ 5
+
+
+
+ background-color: rgba(255, 255, 255, 100);
+border-radius: 2px
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::Maximum
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+ -
+
+
+ 12
+
+ -
+
+
+ 0
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ Microsoft YaHei UI
+ 22
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(255, 255, 255, 255);
+font-weight: bold
+
+
+ 测试
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ color: rgba(255, 255, 255, 150);
+font-weight: bold;
+
+
+ <0分钟
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 60
+ 60
+
+
+
+
+ 60
+ 60
+
+
+
+
+ Microsoft YaHei
+ 12
+ 75
+ true
+
+
+
+ font-weight: bold;
+
+
+ true
+
+
+ %p%
+
+
+ 0.000000000000000
+
+
+ 7
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ProgressBar
+ QProgressBar
+
+
+
+ ProgressRing
+ ProgressBar
+
+
+
+
+
+
diff --git a/ui/default/dark/widget-next-activity.ui b/ui/default/dark/widget-next-activity.ui
new file mode 100644
index 0000000..3708732
--- /dev/null
+++ b/ui/default/dark/widget-next-activity.ui
@@ -0,0 +1,114 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 290
+ 125
+
+
+
+
+ 0
+ 100
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 更多活动
+
+
+
+
+ 40
+ 20
+ 211
+ 31
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ color: rgba(255, 255, 255, 150);
+font-weight: bold;
+
+
+ 接下来
+
+
+ Qt::PlainText
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
+
+
+
+
+
+ 40
+ 40
+ 221
+ 51
+
+
+
+
+ Microsoft YaHei UI
+ 21
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(255, 255, 255, 255);
+font-weight: bold
+
+
+ 测试测试
+
+
+ Qt::PlainText
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
+
+
+
+
+
+ 10
+ 10
+ 271
+ 91
+
+
+
+ background-color: rgba(15, 18, 22, 255);
+border-radius: 8px
+
+
+
+
+
+ backgnd
+ next_subtitle
+ next_lesson_text
+
+
+
+
diff --git a/ui/default/dark/widget-time.ui b/ui/default/dark/widget-time.ui
new file mode 100644
index 0000000..73538f7
--- /dev/null
+++ b/ui/default/dark/widget-time.ui
@@ -0,0 +1,114 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 210
+ 125
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 时间
+
+
+
+
+ 20
+ 20
+ 161
+ 31
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ color: rgba(255, 255, 255, 150);
+font-weight: bold;
+
+
+ 2025 年 13 月
+
+
+ Qt::PlainText
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+
+
+ 30
+ 40
+ 151
+ 51
+
+
+
+
+ Microsoft YaHei UI
+ 21
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(255, 255, 255, 255);
+font-weight: bold
+
+
+ 32 日 周二
+
+
+ Qt::PlainText
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+
+
+ 10
+ 10
+ 191
+ 91
+
+
+
+ background-color: rgba(15, 18, 22, 255);
+border-radius: 8px
+
+
+
+
+
+ backgnd
+ date_text
+ day_text
+
+
+
+
diff --git a/ui/default/dark/widget-weather.ui b/ui/default/dark/widget-weather.ui
new file mode 100644
index 0000000..d7e3f88
--- /dev/null
+++ b/ui/default/dark/widget-weather.ui
@@ -0,0 +1,146 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 200
+ 125
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 天气
+
+
+
+
+ 20
+ 20
+ 161
+ 31
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ color: rgba(255, 255, 255, 185);
+font-weight: bold;
+
+
+ 当前城市
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 10
+ 10
+ 181
+ 91
+
+
+
+ background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:1, stop:0 rgba(40, 60, 110, 245), stop:1 rgba(75, 175, 245, 245));
+border-radius: 8px;
+border-image: url();
+
+
+
+
+
+
+
+
+ 30
+ 10
+ 141
+ 111
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+ ../../img/weather/0.svg
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ Microsoft YaHei UI
+ 21
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(255, 255, 255, 255);
+font-weight: bold
+
+
+ 114℉
+
+
+ Qt::PlainText
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
+
+
+
+
+
+ backgnd
+ current_city
+ horizontalLayoutWidget
+
+
+
+
diff --git a/ui/default/preview/widget-countdown-day.png b/ui/default/preview/widget-countdown-day.png
new file mode 100644
index 0000000..9e913a5
Binary files /dev/null and b/ui/default/preview/widget-countdown-day.png differ
diff --git a/ui/default/preview/widget-countdown.png b/ui/default/preview/widget-countdown.png
new file mode 100644
index 0000000..871b97c
Binary files /dev/null and b/ui/default/preview/widget-countdown.png differ
diff --git a/ui/default/preview/widget-current-activity.png b/ui/default/preview/widget-current-activity.png
new file mode 100644
index 0000000..53f590b
Binary files /dev/null and b/ui/default/preview/widget-current-activity.png differ
diff --git a/ui/default/preview/widget-custom.png b/ui/default/preview/widget-custom.png
new file mode 100644
index 0000000..827a24c
Binary files /dev/null and b/ui/default/preview/widget-custom.png differ
diff --git a/ui/default/preview/widget-next-activity.png b/ui/default/preview/widget-next-activity.png
new file mode 100644
index 0000000..f53fe1a
Binary files /dev/null and b/ui/default/preview/widget-next-activity.png differ
diff --git a/ui/default/preview/widget-time.png b/ui/default/preview/widget-time.png
new file mode 100644
index 0000000..5a5db31
Binary files /dev/null and b/ui/default/preview/widget-time.png differ
diff --git a/ui/default/preview/widget-weather.png b/ui/default/preview/widget-weather.png
new file mode 100644
index 0000000..74f0aed
Binary files /dev/null and b/ui/default/preview/widget-weather.png differ
diff --git a/ui/default/theme.json b/ui/default/theme.json
new file mode 100644
index 0000000..9044a9c
--- /dev/null
+++ b/ui/default/theme.json
@@ -0,0 +1,17 @@
+{
+ "name": "默认",
+ "support_dark_mode": true,
+ "default_theme": null,
+ "radius": "8px",
+ "spacing": -5,
+ "shadow": true,
+ "height": 125,
+ "widget_width": {
+ "widget-time.ui": 210,
+ "widget-countdown.ui": 200,
+ "widget-current-activity.ui": 360,
+ "widget-next-activity.ui": 290,
+ "widget-countdown-day.ui": 200,
+ "widget-weather.ui": 200
+ }
+}
diff --git a/ui/default/toast-open_dialog.ui b/ui/default/toast-open_dialog.ui
new file mode 100644
index 0000000..3c111c4
--- /dev/null
+++ b/ui/default/toast-open_dialog.ui
@@ -0,0 +1,247 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 281
+ 112
+
+
+
+
+ 200
+ 0
+
+
+
+ Form
+
+
+
+ 8
+
+
+ 8
+
+
+ 8
+
+
+ 22
+
+ -
+
+
+ background-color: rgba(245, 245, 255, 245);
+border-radius: 38px
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+
+ 4
+
+
+ 10
+
+
+ 10
+
+
+ 10
+
+
+ 10
+
+ -
+
+ -
+
+
+ 12
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 58
+ 58
+
+
+
+
+ 58
+ 58
+
+
+
+
+ Microsoft YaHei
+ 12
+ 75
+ true
+
+
+
+ font-weight: bold;
+
+
+ false
+
+
+ %p
+
+
+ 0.000000000000000
+
+
+ 7
+
+
+
+ -
+
+
+ 0
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ Microsoft YaHei UI
+ 12
+ 75
+ true
+
+
+
+ color: rgba(0, 0, 0, 90);
+background-color: rgba(0,0,0,0);
+font-weight: bold;
+
+
+ 即将打开
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ Microsoft YaHei UI
+ 17
+ 75
+ true
+
+
+
+ color: rgba(37, 37, 37, 255);
+background-color: rgba(0,0,0,0);
+font-weight: bold
+
+
+ 测试
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
+
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ 取消
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ HyperlinkButton
+ PushButton
+
+
+
+ PushButton
+ QPushButton
+
+
+
+ ProgressBar
+ QProgressBar
+
+
+
+ ProgressRing
+ ProgressBar
+
+
+
+
+
+
diff --git a/ui/default/widget-base.ui b/ui/default/widget-base.ui
new file mode 100644
index 0000000..eb8f7bc
--- /dev/null
+++ b/ui/default/widget-base.ui
@@ -0,0 +1,146 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 200
+ 125
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 基本组件
+
+
+
+ 22
+
+
+ 8
+
+
+ 8
+
+
+ 8
+
+
+ 24
+
+ -
+
+
+ background-color: rgba(242, 243, 245, 255);
+border-radius: 8px
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+
+ 0
+
+
+ 14
+
+
+ 14
+
+ -
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ color: rgba(0, 0, 0, 90);
+background-color: rgba(255, 255, 255, 0);
+font-weight: bold;
+
+
+ Title
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ -
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ Microsoft YaHei UI
+ 21
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(37, 37, 37, 255);
+font-weight: bold;
+background-color: rgba(255, 255, 255, 0);
+
+
+ Content
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/default/widget-countdown-day.ui b/ui/default/widget-countdown-day.ui
new file mode 100644
index 0000000..5f3e17b
--- /dev/null
+++ b/ui/default/widget-countdown-day.ui
@@ -0,0 +1,114 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 200
+ 125
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 倒计日
+
+
+
+
+ 20
+ 20
+ 161
+ 31
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ color: rgba(0, 0, 0, 90);
+font-weight: bold;
+
+
+ 距离 中考 还有
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 20
+ 30
+ 161
+ 71
+
+
+
+
+ Microsoft YaHei UI
+ 22
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(37, 37, 37, 255);
+font-weight: bold
+
+
+ 300 天
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 10
+ 10
+ 181
+ 91
+
+
+
+ background-color: rgba(242, 243, 245, 255);
+border-radius: 8px
+
+
+
+
+
+ backgnd
+ countdown_custom_title
+ custom_countdown
+
+
+
+
diff --git a/ui/default/widget-countdown.ui b/ui/default/widget-countdown.ui
new file mode 100644
index 0000000..81d7e1f
--- /dev/null
+++ b/ui/default/widget-countdown.ui
@@ -0,0 +1,156 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 200
+ 125
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 活动倒计时
+
+
+
+
+ 20
+ 90
+ 161
+ 5
+
+
+
+
+ 0
+ 5
+
+
+
+
+ 16777215
+ 6
+
+
+
+
+
+
+ Qt::Horizontal
+
+
+ true
+
+
+ 0.000000000000000
+
+
+
+
+
+ 20
+ 20
+ 161
+ 31
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ color: rgba(0, 0, 0, 90);
+font-weight: bold;
+
+
+ 倒计时
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 20
+ 40
+ 161
+ 51
+
+
+
+
+ Microsoft YaHei UI
+ 21
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(37, 37, 37, 255);
+font-weight: bold
+
+
+ 00:00
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 10
+ 10
+ 181
+ 91
+
+
+
+ background-color: rgba(242, 243, 245, 255);
+border-radius: 8px
+
+
+
+
+
+ backgnd
+ progressBar
+ activity_countdown_title
+ activity_countdown
+
+
+
+ ProgressBar
+ QProgressBar
+
+
+
+
+
+
diff --git a/ui/default/widget-current-activity.ui b/ui/default/widget-current-activity.ui
new file mode 100644
index 0000000..008c01e
--- /dev/null
+++ b/ui/default/widget-current-activity.ui
@@ -0,0 +1,136 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 360
+ 125
+
+
+
+
+ 0
+ 100
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 当前活动
+
+
+
+
+ 10
+ 40
+ 341
+ 51
+
+
+
+
+ Microsoft YaHei UI
+ 22
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(37, 37, 37, 255);
+font-weight: bold;
+
+
+ 测试
+
+
+
+ ../../../../../img/it.svg ../../../../../img/it.svg
+
+
+
+ 36
+ 26
+
+
+
+
+
+
+ 155
+ 35
+ 45
+ 45
+
+
+
+ background-color: rgb(0, 255, 127);
+border-radius:20px
+
+
+
+
+
+
+
+
+ 30
+ 0
+ 301
+ 71
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ color: rgba(0, 0, 0, 90);
+font-weight: bold;
+
+
+ 当前活动
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 10
+ 10
+ 341
+ 91
+
+
+
+ background-color: rgba(242, 243, 245, 255);
+border-radius: 8px
+
+
+
+
+
+ backgnd
+ blurEffect
+ sub_title
+ subject
+
+
+
+
diff --git a/ui/default/widget-floating.ui b/ui/default/widget-floating.ui
new file mode 100644
index 0000000..d5a5e9b
--- /dev/null
+++ b/ui/default/widget-floating.ui
@@ -0,0 +1,239 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 200
+ 145
+
+
+
+
+ 200
+ 0
+
+
+
+ Form
+
+
+
+ 8
+
+
+ 8
+
+
+ 8
+
+
+ 22
+
+ -
+
+
+ background-color: rgba(242, 243, 245, 255);
+border-radius: 8px
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+
+ 4
+
+
+ 16
+
+
+ 4
+
+
+ 16
+
+
+ 10
+
+ -
+
+ -
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::Maximum
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 50
+ 5
+
+
+
+ background-color:#503D3D3D;
+border-radius: 2px
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::Maximum
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+ -
+
+
+ 12
+
+ -
+
+
+ 0
+
+ -
+
+
+
+ Microsoft YaHei UI
+ 22
+ 75
+ true
+
+
+
+ color: rgba(37, 37, 37, 255);
+font-weight: bold
+
+
+ 测试
+
+
+
+ -
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ color: rgba(0, 0, 0, 90);
+font-weight: bold;
+
+
+ <0分钟
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 60
+ 60
+
+
+
+
+ 60
+ 60
+
+
+
+
+ Microsoft YaHei
+ 12
+ 75
+ true
+
+
+
+ font-weight: bold;
+
+
+ true
+
+
+ %p%
+
+
+ 0.000000000000000
+
+
+ 7
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ProgressBar
+ QProgressBar
+
+
+
+ ProgressRing
+ ProgressBar
+
+
+
+
+
+
diff --git a/ui/default/widget-next-activity.ui b/ui/default/widget-next-activity.ui
new file mode 100644
index 0000000..bdb0c53
--- /dev/null
+++ b/ui/default/widget-next-activity.ui
@@ -0,0 +1,114 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 290
+ 125
+
+
+
+
+ 0
+ 100
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 更多活动
+
+
+
+
+ 40
+ 20
+ 211
+ 31
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ color: rgba(0, 0, 0, 90);
+font-weight: bold;
+
+
+ 接下来
+
+
+ Qt::PlainText
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
+
+
+
+
+
+ 40
+ 40
+ 221
+ 51
+
+
+
+
+ Microsoft YaHei UI
+ 21
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(37, 37, 37, 255);
+font-weight: bold
+
+
+ 测试测试
+
+
+ Qt::PlainText
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
+
+
+
+
+
+ 10
+ 10
+ 271
+ 91
+
+
+
+ background-color: rgba(242, 243, 245, 255);
+border-radius: 8px
+
+
+
+
+
+ backgnd
+ next_subtitle
+ next_lesson_text
+
+
+
+
diff --git a/ui/default/widget-time.ui b/ui/default/widget-time.ui
new file mode 100644
index 0000000..065f5bc
--- /dev/null
+++ b/ui/default/widget-time.ui
@@ -0,0 +1,114 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 210
+ 125
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 时间
+
+
+
+
+ 20
+ 20
+ 161
+ 31
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ color: rgba(0, 0, 0, 90);
+font-weight: bold;
+
+
+ 2025 年 13 月
+
+
+ Qt::PlainText
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+
+
+ 30
+ 40
+ 151
+ 51
+
+
+
+
+ Microsoft YaHei UI
+ 21
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(37, 37, 37, 255);
+font-weight: bold
+
+
+ 32 日 周二
+
+
+ Qt::PlainText
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+
+
+ 10
+ 10
+ 191
+ 91
+
+
+
+ background-color: rgba(242, 243, 245, 255);
+border-radius: 8px
+
+
+
+
+
+ backgnd
+ date_text
+ day_text
+
+
+
+
diff --git a/ui/default/widget-weather.ui b/ui/default/widget-weather.ui
new file mode 100644
index 0000000..2e03073
--- /dev/null
+++ b/ui/default/widget-weather.ui
@@ -0,0 +1,146 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 200
+ 125
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 天气
+
+
+
+
+ 20
+ 20
+ 161
+ 31
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ color: rgba(255, 255, 255, 185);
+font-weight: bold;
+
+
+ 当前城市
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 10
+ 10
+ 181
+ 91
+
+
+
+ background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:1, stop:0 rgba(40, 60, 110, 245), stop:1 rgba(75, 175, 245, 245));
+border-radius: 8px;
+border-image: url();
+
+
+
+
+
+
+
+
+ 30
+ 10
+ 141
+ 111
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+ ../../img/weather/0.svg
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ Microsoft YaHei UI
+ 21
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(255, 255, 255, 255);
+font-weight: bold
+
+
+ 114℉
+
+
+ Qt::PlainText
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
+
+
+
+
+
+ backgnd
+ current_city
+ horizontalLayoutWidget
+
+
+
+
diff --git a/ui/hoshino/dark/preview/widget-countdown-day.png b/ui/hoshino/dark/preview/widget-countdown-day.png
new file mode 100644
index 0000000..6f3db4b
Binary files /dev/null and b/ui/hoshino/dark/preview/widget-countdown-day.png differ
diff --git a/ui/hoshino/dark/preview/widget-countdown.png b/ui/hoshino/dark/preview/widget-countdown.png
new file mode 100644
index 0000000..46514c1
Binary files /dev/null and b/ui/hoshino/dark/preview/widget-countdown.png differ
diff --git a/ui/hoshino/dark/preview/widget-current-activity.png b/ui/hoshino/dark/preview/widget-current-activity.png
new file mode 100644
index 0000000..9797024
Binary files /dev/null and b/ui/hoshino/dark/preview/widget-current-activity.png differ
diff --git a/ui/hoshino/dark/preview/widget-custom.png b/ui/hoshino/dark/preview/widget-custom.png
new file mode 100644
index 0000000..c714428
Binary files /dev/null and b/ui/hoshino/dark/preview/widget-custom.png differ
diff --git a/ui/hoshino/dark/preview/widget-next-activity.png b/ui/hoshino/dark/preview/widget-next-activity.png
new file mode 100644
index 0000000..42ede81
Binary files /dev/null and b/ui/hoshino/dark/preview/widget-next-activity.png differ
diff --git a/ui/hoshino/dark/preview/widget-time.png b/ui/hoshino/dark/preview/widget-time.png
new file mode 100644
index 0000000..8748127
Binary files /dev/null and b/ui/hoshino/dark/preview/widget-time.png differ
diff --git a/ui/hoshino/dark/preview/widget-weather.png b/ui/hoshino/dark/preview/widget-weather.png
new file mode 100644
index 0000000..02db73d
Binary files /dev/null and b/ui/hoshino/dark/preview/widget-weather.png differ
diff --git a/ui/hoshino/dark/widget-base.ui b/ui/hoshino/dark/widget-base.ui
new file mode 100644
index 0000000..378eb81
--- /dev/null
+++ b/ui/hoshino/dark/widget-base.ui
@@ -0,0 +1,146 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 200
+ 125
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 基本组件
+
+
+
+ 20
+
+
+ 8
+
+
+ 8
+
+
+ 8
+
+
+ 24
+
+ -
+
+
+ background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:0, stop:0 rgba(50, 25, 35, 255), stop:1 rgba(15, 18, 22, 255));
+border-radius: 8px
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+
+ 0
+
+
+ 14
+
+
+ 14
+
+ -
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ color: rgba(255, 255, 255, 150);
+font-weight: bold;
+background-color: rgba(255, 255, 255, 0);
+
+
+ Title
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ -
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ Microsoft YaHei UI
+ 21
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(255, 255, 255, 255);
+font-weight: bold;
+background-color: rgba(255, 255, 255, 0);
+
+
+ Content
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/hoshino/dark/widget-countdown-day.ui b/ui/hoshino/dark/widget-countdown-day.ui
new file mode 100644
index 0000000..63a4869
--- /dev/null
+++ b/ui/hoshino/dark/widget-countdown-day.ui
@@ -0,0 +1,134 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 200
+ 125
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 倒计日
+
+
+
+
+ 20
+ 20
+ 161
+ 31
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ color: rgba(255, 255, 255, 150);
+font-weight: bold;
+
+
+ 距离 中考 还有
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 20
+ 30
+ 161
+ 71
+
+
+
+
+ Microsoft YaHei UI
+ 22
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(255, 255, 255, 255);
+font-weight: bold
+
+
+ 300 天
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 10
+ 10
+ 181
+ 91
+
+
+
+ background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:0, stop:0 rgba(50, 25, 35, 255), stop:1 rgba(15, 18, 22, 255));
+border-radius: 8px
+
+
+
+
+
+
+
+
+ 10
+ 10
+ 91
+ 91
+
+
+
+
+
+
+ ../img/2.png
+
+
+ true
+
+
+ backgnd
+ img
+ countdown_custom_title
+ custom_countdown
+
+
+
+
diff --git a/ui/hoshino/dark/widget-countdown.ui b/ui/hoshino/dark/widget-countdown.ui
new file mode 100644
index 0000000..130e961
--- /dev/null
+++ b/ui/hoshino/dark/widget-countdown.ui
@@ -0,0 +1,176 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 200
+ 125
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 活动倒计时
+
+
+
+
+ 20
+ 90
+ 161
+ 5
+
+
+
+
+ 0
+ 5
+
+
+
+
+ 16777215
+ 6
+
+
+
+
+
+
+ Qt::Horizontal
+
+
+ true
+
+
+ 0.000000000000000
+
+
+
+
+
+ 20
+ 20
+ 161
+ 31
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ color: rgba(255, 255, 255, 150);
+font-weight: bold;
+
+
+ 倒计时
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 20
+ 40
+ 161
+ 51
+
+
+
+
+ Microsoft YaHei UI
+ 21
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(255, 255, 255, 255);
+font-weight: bold
+
+
+ 00:00
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 10
+ 10
+ 181
+ 91
+
+
+
+ background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:0, stop:0 rgba(50, 25, 35, 255), stop:1 rgba(15, 18, 22, 255));
+border-radius: 8px
+
+
+
+
+
+
+
+
+ 100
+ 10
+ 91
+ 91
+
+
+
+
+
+
+ ../img/5.png
+
+
+ true
+
+
+ backgnd
+ img
+ progressBar
+ activity_countdown_title
+ activity_countdown
+
+
+
+ ProgressBar
+ QProgressBar
+
+
+
+
+
+
diff --git a/ui/hoshino/dark/widget-current-activity.ui b/ui/hoshino/dark/widget-current-activity.ui
new file mode 100644
index 0000000..5d19263
--- /dev/null
+++ b/ui/hoshino/dark/widget-current-activity.ui
@@ -0,0 +1,161 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 360
+ 125
+
+
+
+
+ 0
+ 100
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 当前活动
+
+
+
+
+ 10
+ 40
+ 341
+ 51
+
+
+
+
+ Microsoft YaHei UI
+ 22
+ 75
+ true
+
+
+
+ QPushButton {
+ qproperty-iconSize: 24px;
+ color: white;
+ border: none;
+ color: rgba(255, 255, 255, 255);
+ font-weight: bold
+ }
+
+
+
+ 测试
+
+
+
+ ../../../img/it.svg ../../../img/it.svg
+
+
+
+ 24
+ 24
+
+
+
+
+
+
+ 170
+ 40
+ 35
+ 35
+
+
+
+ background-color: rgb(0, 255, 127);
+border-radius:20px
+
+
+
+
+
+
+
+
+ 30
+ 0
+ 301
+ 71
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ color: rgba(255, 255, 255, 150);
+font-weight: bold;
+
+
+ 当前活动
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 10
+ 10
+ 341
+ 91
+
+
+
+ background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:0, stop:0 rgba(50, 25, 35, 255), stop:1 rgba(15, 18, 22, 255));
+border-radius: 8px
+
+
+
+
+
+
+
+
+ 30
+ 10
+ 91
+ 91
+
+
+
+
+
+
+ ../img/1.png
+
+
+ true
+
+
+ backgnd
+ img
+ blurEffect
+ sub_title
+ subject
+
+
+
+
diff --git a/ui/hoshino/dark/widget-next-activity.ui b/ui/hoshino/dark/widget-next-activity.ui
new file mode 100644
index 0000000..e8cc344
--- /dev/null
+++ b/ui/hoshino/dark/widget-next-activity.ui
@@ -0,0 +1,134 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 290
+ 125
+
+
+
+
+ 0
+ 100
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 更多活动
+
+
+
+
+ 40
+ 20
+ 211
+ 31
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ color: rgba(255, 255, 255, 150);
+font-weight: bold;
+
+
+ 接下来
+
+
+ Qt::PlainText
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
+
+
+
+
+
+ 40
+ 40
+ 221
+ 51
+
+
+
+
+ Microsoft YaHei UI
+ 21
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(255, 255, 255, 255);
+font-weight: bold
+
+
+ 测试测试
+
+
+ Qt::PlainText
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
+
+
+
+
+
+ 10
+ 10
+ 271
+ 91
+
+
+
+ background-color: qlineargradient(spread:pad, x1:1, y1:1, x2:0, y2:0, stop:0 rgba(50, 25, 35, 255), stop:1 rgba(15, 18, 22, 255));
+border-radius: 8px
+
+
+
+
+
+
+
+
+ 190
+ 10
+ 91
+ 91
+
+
+
+
+
+
+ ../img/4.png
+
+
+ true
+
+
+ backgnd
+ img
+ next_subtitle
+ next_lesson_text
+
+
+
+
diff --git a/ui/hoshino/dark/widget-time.ui b/ui/hoshino/dark/widget-time.ui
new file mode 100644
index 0000000..5aecfbe
--- /dev/null
+++ b/ui/hoshino/dark/widget-time.ui
@@ -0,0 +1,134 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 210
+ 125
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 时间
+
+
+
+
+ 20
+ 20
+ 161
+ 31
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ color: rgba(255, 255, 255, 150);
+font-weight: bold;
+
+
+ 2025 年 13 月
+
+
+ Qt::PlainText
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+
+
+ 30
+ 40
+ 151
+ 51
+
+
+
+
+ Microsoft YaHei UI
+ 21
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(255, 255, 255, 255);
+font-weight: bold
+
+
+ 32 日 周二
+
+
+ Qt::PlainText
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+
+
+ 10
+ 10
+ 191
+ 91
+
+
+
+ background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:0, stop:0 rgba(50, 25, 35, 255), stop:1 rgba(15, 18, 22, 255));
+border-radius: 8px
+
+
+
+
+
+
+
+
+ 10
+ 10
+ 91
+ 91
+
+
+
+
+
+
+ ../img/1.png
+
+
+ true
+
+
+ backgnd
+ img
+ date_text
+ day_text
+
+
+
+
diff --git a/ui/hoshino/dark/widget-weather.ui b/ui/hoshino/dark/widget-weather.ui
new file mode 100644
index 0000000..d96986a
--- /dev/null
+++ b/ui/hoshino/dark/widget-weather.ui
@@ -0,0 +1,166 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 200
+ 125
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 天气
+
+
+
+
+ 20
+ 20
+ 161
+ 31
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ color: rgba(255, 255, 255, 185);
+font-weight: bold;
+
+
+ 当前城市
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 10
+ 10
+ 181
+ 91
+
+
+
+ background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:1, stop:0 rgba(40, 60, 110, 245), stop:1 rgba(75, 175, 245, 245));
+border-radius: 8px;
+border-image: url();
+
+
+
+
+
+
+
+
+ 30
+ 10
+ 141
+ 111
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+ ../../img/weather/0.svg
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ Microsoft YaHei UI
+ 21
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(255, 255, 255, 255);
+font-weight: bold
+
+
+ 114℉
+
+
+ Qt::PlainText
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
+
+
+
+
+
+
+
+
+ 100
+ 10
+ 91
+ 91
+
+
+
+
+
+
+ img/4.png
+
+
+ true
+
+
+ backgnd
+ img
+ current_city
+ horizontalLayoutWidget
+
+
+
+
diff --git a/ui/hoshino/img/1.png b/ui/hoshino/img/1.png
new file mode 100644
index 0000000..024275d
Binary files /dev/null and b/ui/hoshino/img/1.png differ
diff --git a/ui/hoshino/img/2.png b/ui/hoshino/img/2.png
new file mode 100644
index 0000000..5e8e8e3
Binary files /dev/null and b/ui/hoshino/img/2.png differ
diff --git a/ui/hoshino/img/3.png b/ui/hoshino/img/3.png
new file mode 100644
index 0000000..2d39423
Binary files /dev/null and b/ui/hoshino/img/3.png differ
diff --git a/ui/hoshino/img/4.png b/ui/hoshino/img/4.png
new file mode 100644
index 0000000..74af677
Binary files /dev/null and b/ui/hoshino/img/4.png differ
diff --git a/ui/hoshino/img/5.png b/ui/hoshino/img/5.png
new file mode 100644
index 0000000..494d566
Binary files /dev/null and b/ui/hoshino/img/5.png differ
diff --git a/ui/hoshino/preview/widget-countdown-day.png b/ui/hoshino/preview/widget-countdown-day.png
new file mode 100644
index 0000000..9e913a5
Binary files /dev/null and b/ui/hoshino/preview/widget-countdown-day.png differ
diff --git a/ui/hoshino/preview/widget-countdown.png b/ui/hoshino/preview/widget-countdown.png
new file mode 100644
index 0000000..871b97c
Binary files /dev/null and b/ui/hoshino/preview/widget-countdown.png differ
diff --git a/ui/hoshino/preview/widget-current-activity.png b/ui/hoshino/preview/widget-current-activity.png
new file mode 100644
index 0000000..53f590b
Binary files /dev/null and b/ui/hoshino/preview/widget-current-activity.png differ
diff --git a/ui/hoshino/preview/widget-custom.png b/ui/hoshino/preview/widget-custom.png
new file mode 100644
index 0000000..827a24c
Binary files /dev/null and b/ui/hoshino/preview/widget-custom.png differ
diff --git a/ui/hoshino/preview/widget-next-activity.png b/ui/hoshino/preview/widget-next-activity.png
new file mode 100644
index 0000000..f53fe1a
Binary files /dev/null and b/ui/hoshino/preview/widget-next-activity.png differ
diff --git a/ui/hoshino/preview/widget-time.png b/ui/hoshino/preview/widget-time.png
new file mode 100644
index 0000000..5a5db31
Binary files /dev/null and b/ui/hoshino/preview/widget-time.png differ
diff --git a/ui/hoshino/preview/widget-weather.png b/ui/hoshino/preview/widget-weather.png
new file mode 100644
index 0000000..74f0aed
Binary files /dev/null and b/ui/hoshino/preview/widget-weather.png differ
diff --git a/ui/hoshino/theme.json b/ui/hoshino/theme.json
new file mode 100644
index 0000000..2351187
--- /dev/null
+++ b/ui/hoshino/theme.json
@@ -0,0 +1,17 @@
+{
+ "name": "小鸟游星野",
+ "support_dark_mode": true,
+ "default_theme": null,
+ "radius": "8px",
+ "spacing": -5,
+ "shadow": true,
+ "height": 125,
+ "widget_width": {
+ "widget-time.ui": 210,
+ "widget-countdown.ui": 200,
+ "widget-current-activity.ui": 360,
+ "widget-next-activity.ui": 290,
+ "widget-countdown-day.ui": 200,
+ "widget-weather.ui": 200
+ }
+}
diff --git a/ui/hoshino/widget-base.ui b/ui/hoshino/widget-base.ui
new file mode 100644
index 0000000..1db5fe2
--- /dev/null
+++ b/ui/hoshino/widget-base.ui
@@ -0,0 +1,146 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 200
+ 125
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 基本组件
+
+
+
+ 22
+
+
+ 8
+
+
+ 8
+
+
+ 8
+
+
+ 24
+
+ -
+
+
+ background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:0, stop:0 rgba(255, 225, 245, 255), stop:1 rgba(255, 255, 255, 255));
+border-radius: 8px
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+
+ 0
+
+
+ 14
+
+
+ 14
+
+ -
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ color: rgba(0, 0, 0, 90);
+background-color: rgba(255, 255, 255, 0);
+font-weight: bold;
+
+
+ Title
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ -
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ Microsoft YaHei UI
+ 21
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(37, 37, 37, 255);
+font-weight: bold;
+background-color: rgba(255, 255, 255, 0);
+
+
+ Content
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/hoshino/widget-countdown-day.ui b/ui/hoshino/widget-countdown-day.ui
new file mode 100644
index 0000000..3c38442
--- /dev/null
+++ b/ui/hoshino/widget-countdown-day.ui
@@ -0,0 +1,134 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 200
+ 125
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 倒计日
+
+
+
+
+ 20
+ 20
+ 161
+ 31
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ color: rgba(0, 0, 0, 90);
+font-weight: bold;
+
+
+ 距离 中考 还有
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 20
+ 30
+ 161
+ 71
+
+
+
+
+ Microsoft YaHei UI
+ 22
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(37, 37, 37, 255);
+font-weight: bold
+
+
+ 300 天
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 10
+ 10
+ 181
+ 91
+
+
+
+ background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:0, stop:0 rgba(255, 225, 245, 255), stop:1 rgba(255, 255, 255, 255));
+border-radius: 8px
+
+
+
+
+
+
+
+
+ 10
+ 10
+ 91
+ 91
+
+
+
+
+
+
+ img/2.png
+
+
+ true
+
+
+ backgnd
+ img
+ countdown_custom_title
+ custom_countdown
+
+
+
+
diff --git a/ui/hoshino/widget-countdown.ui b/ui/hoshino/widget-countdown.ui
new file mode 100644
index 0000000..80d0050
--- /dev/null
+++ b/ui/hoshino/widget-countdown.ui
@@ -0,0 +1,176 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 200
+ 125
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 活动倒计时
+
+
+
+
+ 20
+ 90
+ 161
+ 5
+
+
+
+
+ 0
+ 5
+
+
+
+
+ 16777215
+ 6
+
+
+
+
+
+
+ Qt::Horizontal
+
+
+ true
+
+
+ 0.000000000000000
+
+
+
+
+
+ 20
+ 20
+ 161
+ 31
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ color: rgba(0, 0, 0, 90);
+font-weight: bold;
+
+
+ 倒计时
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 20
+ 40
+ 161
+ 51
+
+
+
+
+ Microsoft YaHei UI
+ 21
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(37, 37, 37, 255);
+font-weight: bold
+
+
+ 00:00
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 10
+ 10
+ 181
+ 91
+
+
+
+ background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:0, stop:0 rgba(255, 225, 245, 255), stop:1 rgba(255, 255, 255, 255));
+border-radius: 8px
+
+
+
+
+
+
+
+
+ 10
+ 10
+ 91
+ 91
+
+
+
+
+
+
+ img/3.png
+
+
+ true
+
+
+ backgnd
+ img
+ progressBar
+ activity_countdown_title
+ activity_countdown
+
+
+
+ ProgressBar
+ QProgressBar
+
+
+
+
+
+
diff --git a/ui/hoshino/widget-current-activity.ui b/ui/hoshino/widget-current-activity.ui
new file mode 100644
index 0000000..39ab625
--- /dev/null
+++ b/ui/hoshino/widget-current-activity.ui
@@ -0,0 +1,156 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 360
+ 125
+
+
+
+
+ 0
+ 100
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 当前活动
+
+
+
+
+ 10
+ 40
+ 341
+ 51
+
+
+
+
+ Microsoft YaHei UI
+ 22
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(37, 37, 37, 255);
+font-weight: bold;
+
+
+ 测试
+
+
+
+ ../../../../../img/it.svg ../../../../../img/it.svg
+
+
+
+ 36
+ 26
+
+
+
+
+
+
+ 155
+ 35
+ 45
+ 45
+
+
+
+ background-color: rgb(0, 255, 127);
+border-radius:20px
+
+
+
+
+
+
+
+
+ 30
+ 0
+ 301
+ 71
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ color: rgba(0, 0, 0, 90);
+font-weight: bold;
+
+
+ 当前活动
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 10
+ 10
+ 341
+ 91
+
+
+
+ background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:0, stop:0 rgba(255, 225, 245, 255), stop:1 rgba(255, 255, 255, 255));
+border-radius: 8px
+
+
+
+
+
+
+
+
+ 30
+ 10
+ 91
+ 91
+
+
+
+
+
+
+ img/1.png
+
+
+ true
+
+
+ backgnd
+ img
+ blurEffect
+ sub_title
+ subject
+
+
+
+
diff --git a/ui/hoshino/widget-next-activity.ui b/ui/hoshino/widget-next-activity.ui
new file mode 100644
index 0000000..7207876
--- /dev/null
+++ b/ui/hoshino/widget-next-activity.ui
@@ -0,0 +1,134 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 290
+ 125
+
+
+
+
+ 0
+ 100
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 更多活动
+
+
+
+
+ 40
+ 20
+ 211
+ 31
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ color: rgba(0, 0, 0, 90);
+font-weight: bold;
+
+
+ 接下来
+
+
+ Qt::PlainText
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
+
+
+
+
+
+ 40
+ 40
+ 221
+ 51
+
+
+
+
+ Microsoft YaHei UI
+ 21
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(37, 37, 37, 255);
+font-weight: bold
+
+
+ 测试测试
+
+
+ Qt::PlainText
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
+
+
+
+
+
+ 10
+ 10
+ 271
+ 91
+
+
+
+ background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:1, stop:0 rgba(255, 225, 245, 255), stop:1 rgba(255, 255, 255, 255));
+border-radius: 8px
+
+
+
+
+
+
+
+
+ 190
+ 10
+ 91
+ 91
+
+
+
+
+
+
+ img/4.png
+
+
+ true
+
+
+ backgnd
+ img
+ next_subtitle
+ next_lesson_text
+
+
+
+
diff --git a/ui/hoshino/widget-time.ui b/ui/hoshino/widget-time.ui
new file mode 100644
index 0000000..c73146b
--- /dev/null
+++ b/ui/hoshino/widget-time.ui
@@ -0,0 +1,134 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 210
+ 125
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 时间
+
+
+
+
+ 20
+ 20
+ 161
+ 31
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ color: rgba(0, 0, 0, 90);
+font-weight: bold;
+
+
+ 2025 年 13 月
+
+
+ Qt::PlainText
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+
+
+ 30
+ 40
+ 151
+ 51
+
+
+
+
+ Microsoft YaHei UI
+ 21
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(37, 37, 37, 255);
+font-weight: bold
+
+
+ 32 日 周二
+
+
+ Qt::PlainText
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+
+
+ 10
+ 10
+ 191
+ 91
+
+
+
+ background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:0, stop:0 rgba(255, 225, 245, 255), stop:1 rgba(255, 255, 255, 255));
+border-radius: 8px
+
+
+
+
+
+
+
+
+ 10
+ 10
+ 91
+ 91
+
+
+
+
+
+
+ img/1.png
+
+
+ true
+
+
+ backgnd
+ img
+ date_text
+ day_text
+
+
+
+
diff --git a/ui/hoshino/widget-weather.ui b/ui/hoshino/widget-weather.ui
new file mode 100644
index 0000000..d96986a
--- /dev/null
+++ b/ui/hoshino/widget-weather.ui
@@ -0,0 +1,166 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 200
+ 125
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 天气
+
+
+
+
+ 20
+ 20
+ 161
+ 31
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ color: rgba(255, 255, 255, 185);
+font-weight: bold;
+
+
+ 当前城市
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 10
+ 10
+ 181
+ 91
+
+
+
+ background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:1, stop:0 rgba(40, 60, 110, 245), stop:1 rgba(75, 175, 245, 245));
+border-radius: 8px;
+border-image: url();
+
+
+
+
+
+
+
+
+ 30
+ 10
+ 141
+ 111
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+ ../../img/weather/0.svg
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ Microsoft YaHei UI
+ 21
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(255, 255, 255, 255);
+font-weight: bold
+
+
+ 114℉
+
+
+ Qt::PlainText
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
+
+
+
+
+
+
+
+
+ 100
+ 10
+ 91
+ 91
+
+
+
+
+
+
+ img/4.png
+
+
+ true
+
+
+ backgnd
+ img
+ current_city
+ horizontalLayoutWidget
+
+
+
+
diff --git a/ui/minimize/dark/preview/widget-countdown-day.png b/ui/minimize/dark/preview/widget-countdown-day.png
new file mode 100644
index 0000000..5aab367
Binary files /dev/null and b/ui/minimize/dark/preview/widget-countdown-day.png differ
diff --git a/ui/minimize/dark/preview/widget-countdown.png b/ui/minimize/dark/preview/widget-countdown.png
new file mode 100644
index 0000000..2bdaabc
Binary files /dev/null and b/ui/minimize/dark/preview/widget-countdown.png differ
diff --git a/ui/minimize/dark/preview/widget-current-activity.png b/ui/minimize/dark/preview/widget-current-activity.png
new file mode 100644
index 0000000..291dc43
Binary files /dev/null and b/ui/minimize/dark/preview/widget-current-activity.png differ
diff --git a/ui/minimize/dark/preview/widget-custom.png b/ui/minimize/dark/preview/widget-custom.png
new file mode 100644
index 0000000..5f054e2
Binary files /dev/null and b/ui/minimize/dark/preview/widget-custom.png differ
diff --git a/ui/minimize/dark/preview/widget-next-activity.png b/ui/minimize/dark/preview/widget-next-activity.png
new file mode 100644
index 0000000..c95f734
Binary files /dev/null and b/ui/minimize/dark/preview/widget-next-activity.png differ
diff --git a/ui/minimize/dark/preview/widget-time.png b/ui/minimize/dark/preview/widget-time.png
new file mode 100644
index 0000000..99bfd95
Binary files /dev/null and b/ui/minimize/dark/preview/widget-time.png differ
diff --git a/ui/minimize/dark/preview/widget-weather.png b/ui/minimize/dark/preview/widget-weather.png
new file mode 100644
index 0000000..c85ce87
Binary files /dev/null and b/ui/minimize/dark/preview/widget-weather.png differ
diff --git a/ui/minimize/dark/widget-base.ui b/ui/minimize/dark/widget-base.ui
new file mode 100644
index 0000000..96d9df7
--- /dev/null
+++ b/ui/minimize/dark/widget-base.ui
@@ -0,0 +1,147 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 160
+ 117
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 125
+
+
+
+ 基本组件
+
+
+
+ 22
+
+
+ 8
+
+
+ 8
+
+
+ 8
+
+
+ 24
+
+ -
+
+
+ background-color: rgba(15, 18, 22, 255);
+border-radius: 8px
+
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+
+ 0
+
+
+ 12
+
+
+ 12
+
+ -
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ Microsoft YaHei UI
+ 13
+ 75
+ true
+
+
+
+ color: rgba(255, 255, 255, 150);
+font-weight: bold;
+background-color: rgba(255, 255, 255, 0);
+
+
+ Title
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ -
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ Microsoft YaHei UI
+ 19
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(255, 255, 255, 255);
+font-weight: bold;
+background-color: rgba(255, 255, 255, 0);
+
+
+ Content
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/minimize/dark/widget-countdown-day.ui b/ui/minimize/dark/widget-countdown-day.ui
new file mode 100644
index 0000000..e0afb7b
--- /dev/null
+++ b/ui/minimize/dark/widget-countdown-day.ui
@@ -0,0 +1,115 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 180
+ 110
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 倒计日
+
+
+
+
+ 10
+ 10
+ 161
+ 41
+
+
+
+
+ Microsoft YaHei UI
+ 13
+ 75
+ true
+
+
+
+ color: rgba(255, 255, 255, 150);
+font-weight: bold;
+
+
+ 距离 中考 还有
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 20
+ 30
+ 141
+ 61
+
+
+
+
+ Microsoft YaHei UI
+ 19
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(255, 255, 255, 255);
+font-weight: bold
+
+
+ 300 天
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 10
+ 10
+ 161
+ 81
+
+
+
+ background-color: rgba(15, 18, 22, 255);
+border-radius: 8px
+
+
+
+
+
+
+ backgnd
+ countdown_custom_title
+ custom_countdown
+
+
+
+
diff --git a/ui/minimize/dark/widget-countdown.ui b/ui/minimize/dark/widget-countdown.ui
new file mode 100644
index 0000000..59652f1
--- /dev/null
+++ b/ui/minimize/dark/widget-countdown.ui
@@ -0,0 +1,166 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 170
+ 114
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 活动倒计时
+
+
+
+
+ 20
+ 80
+ 131
+ 6
+
+
+
+
+ 0
+ 5
+
+
+
+
+ 16777215
+ 6
+
+
+
+
+
+
+ 75
+
+
+ false
+
+
+ Qt::Horizontal
+
+
+ QProgressBar::BottomToTop
+
+
+ true
+
+
+ 75.000000000000000
+
+
+
+
+
+ 10
+ 10
+ 151
+ 41
+
+
+
+
+ Microsoft YaHei UI
+ 13
+ 75
+ true
+
+
+
+ color: rgba(255, 255, 255, 150);
+font-weight: bold;
+
+
+ 倒计时
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 20
+ 30
+ 131
+ 61
+
+
+
+
+ Microsoft YaHei UI
+ 19
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(255, 255, 255, 255);
+font-weight: bold
+
+
+ 00:00
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 10
+ 10
+ 150
+ 81
+
+
+
+ background-color: rgba(15, 18, 22, 255);
+border-radius: 8px
+
+
+
+
+
+
+ backgnd
+ progressBar
+ activity_countdown_title
+ activity_countdown
+
+
+
+ ProgressBar
+ QProgressBar
+
+
+
+
+
+
diff --git a/ui/minimize/dark/widget-current-activity.ui b/ui/minimize/dark/widget-current-activity.ui
new file mode 100644
index 0000000..33a8008
--- /dev/null
+++ b/ui/minimize/dark/widget-current-activity.ui
@@ -0,0 +1,137 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 290
+ 110
+
+
+
+
+ 0
+ 100
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 当前活动
+
+
+
+
+ 20
+ 30
+ 251
+ 61
+
+
+
+
+ Microsoft YaHei UI
+ 19
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(255, 255, 255, 255);
+font-weight: bold
+
+
+ 测试
+
+
+
+ ../../../../../img/it.svg ../../../../../img/it.svg
+
+
+
+ 36
+ 26
+
+
+
+
+
+
+ 130
+ 40
+ 25
+ 25
+
+
+
+ background-color: rgb(0, 255, 127);
+border-radius:20px
+
+
+
+
+
+
+
+
+ 30
+ 10
+ 231
+ 51
+
+
+
+
+ Microsoft YaHei UI
+ 13
+ 75
+ true
+
+
+
+ color: rgba(255, 255, 255, 150);
+font-weight: bold;
+
+
+ 当前活动
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 10
+ 10
+ 271
+ 81
+
+
+
+ background-color: rgba(15, 18, 22, 255);
+border-radius: 8px
+
+
+
+
+
+
+ backgnd
+ blurEffect
+ sub_title
+ subject
+
+
+
+
diff --git a/ui/minimize/dark/widget-next-activity.ui b/ui/minimize/dark/widget-next-activity.ui
new file mode 100644
index 0000000..a81261e
--- /dev/null
+++ b/ui/minimize/dark/widget-next-activity.ui
@@ -0,0 +1,115 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 240
+ 110
+
+
+
+
+ 0
+ 100
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 更多活动
+
+
+
+
+ 20
+ 10
+ 201
+ 41
+
+
+
+
+ Microsoft YaHei UI
+ 13
+ 75
+ true
+
+
+
+ color: rgba(255, 255, 255, 150);
+font-weight: bold;
+
+
+ 接下来
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 20
+ 30
+ 201
+ 61
+
+
+
+
+ Microsoft YaHei UI
+ 19
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(255, 255, 255, 255);
+font-weight: bold
+
+
+ 一 二 三 四 五
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 10
+ 10
+ 221
+ 81
+
+
+
+ background-color: rgba(15, 18, 22, 255);
+border-radius: 8px
+
+
+
+
+
+
+ backgnd
+ next_subtitle
+ next_lesson_text
+
+
+
+
diff --git a/ui/minimize/dark/widget-time.ui b/ui/minimize/dark/widget-time.ui
new file mode 100644
index 0000000..a7001f6
--- /dev/null
+++ b/ui/minimize/dark/widget-time.ui
@@ -0,0 +1,115 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 170
+ 110
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 时间
+
+
+
+
+ 10
+ 10
+ 151
+ 41
+
+
+
+
+ Microsoft YaHei UI
+ 13
+ 75
+ true
+
+
+
+ color: rgba(255, 255, 255, 150);
+font-weight: bold;
+
+
+ 2025 年 13 月
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 20
+ 30
+ 131
+ 61
+
+
+
+
+ Microsoft YaHei UI
+ 18
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(255, 255, 255, 255);
+font-weight: bold
+
+
+ 32 日 周二
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 10
+ 10
+ 151
+ 81
+
+
+
+ background-color: rgba(15, 18, 22, 255);
+border-radius: 8px
+
+
+
+
+
+
+ backgnd
+ date_text
+ day_text
+
+
+
+
diff --git a/ui/minimize/dark/widget-weather.ui b/ui/minimize/dark/widget-weather.ui
new file mode 100644
index 0000000..3926088
--- /dev/null
+++ b/ui/minimize/dark/widget-weather.ui
@@ -0,0 +1,147 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 180
+ 110
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 天气
+
+
+
+
+ 10
+ 10
+ 161
+ 41
+
+
+
+
+ Microsoft YaHei UI
+ 13
+ 75
+ true
+
+
+
+ color: rgba(255, 255, 255, 185);
+font-weight: bold;
+
+
+ 当前城市
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 10
+ 10
+ 161
+ 81
+
+
+
+ background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:1, stop:0 rgba(40, 60, 110, 245), stop:1 rgba(75, 175, 245, 245));
+border-radius: 8px;
+border-image: url();
+
+
+
+
+
+
+
+
+
+ 30
+ 30
+ 121
+ 61
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+ ../../img/weather/0.svg
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ Microsoft YaHei UI
+ 19
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(255, 255, 255, 255);
+font-weight: bold
+
+
+ 114℉
+
+
+ Qt::PlainText
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
+
+
+
+
+
+ backgnd
+ current_city
+ horizontalLayoutWidget
+
+
+
+
diff --git a/ui/minimize/preview/widget-countdown-day.png b/ui/minimize/preview/widget-countdown-day.png
new file mode 100644
index 0000000..926a7bc
Binary files /dev/null and b/ui/minimize/preview/widget-countdown-day.png differ
diff --git a/ui/minimize/preview/widget-countdown.png b/ui/minimize/preview/widget-countdown.png
new file mode 100644
index 0000000..4311e13
Binary files /dev/null and b/ui/minimize/preview/widget-countdown.png differ
diff --git a/ui/minimize/preview/widget-current-activity.png b/ui/minimize/preview/widget-current-activity.png
new file mode 100644
index 0000000..6cbd734
Binary files /dev/null and b/ui/minimize/preview/widget-current-activity.png differ
diff --git a/ui/minimize/preview/widget-custom.png b/ui/minimize/preview/widget-custom.png
new file mode 100644
index 0000000..b98d32d
Binary files /dev/null and b/ui/minimize/preview/widget-custom.png differ
diff --git a/ui/minimize/preview/widget-next-activity.png b/ui/minimize/preview/widget-next-activity.png
new file mode 100644
index 0000000..331dbfc
Binary files /dev/null and b/ui/minimize/preview/widget-next-activity.png differ
diff --git a/ui/minimize/preview/widget-time.png b/ui/minimize/preview/widget-time.png
new file mode 100644
index 0000000..2152f61
Binary files /dev/null and b/ui/minimize/preview/widget-time.png differ
diff --git a/ui/minimize/preview/widget-weather.png b/ui/minimize/preview/widget-weather.png
new file mode 100644
index 0000000..92941fe
Binary files /dev/null and b/ui/minimize/preview/widget-weather.png differ
diff --git a/ui/minimize/theme.json b/ui/minimize/theme.json
new file mode 100644
index 0000000..619e619
--- /dev/null
+++ b/ui/minimize/theme.json
@@ -0,0 +1,17 @@
+{
+ "name": "小尺寸",
+ "support_dark_mode": true,
+ "default_theme": null,
+ "radius": "8px",
+ "spacing": -9,
+ "height": 110,
+ "shadow": true,
+ "widget_width": {
+ "widget-time.ui": 170,
+ "widget-countdown.ui": 170,
+ "widget-current-activity.ui": 290,
+ "widget-next-activity.ui": 240,
+ "widget-countdown-day.ui": 180,
+ "widget-weather.ui": 180
+ }
+}
diff --git a/ui/minimize/widget-base.ui b/ui/minimize/widget-base.ui
new file mode 100644
index 0000000..dc39f72
--- /dev/null
+++ b/ui/minimize/widget-base.ui
@@ -0,0 +1,146 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 160
+ 110
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 125
+
+
+
+ 基本组件
+
+
+
+ 22
+
+
+ 8
+
+
+ 8
+
+
+ 8
+
+
+ 24
+
+ -
+
+
+ background-color: rgba(242, 243, 245, 255);
+border-radius: 8px
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+
+ 0
+
+
+ 12
+
+
+ 12
+
+ -
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ Microsoft YaHei UI
+ 13
+ 75
+ true
+
+
+
+ color: rgba(0, 0, 0, 90);
+background-color: rgba(255, 255, 255, 0);
+font-weight: bold;
+
+
+ Title
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ -
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ Microsoft YaHei UI
+ 19
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(37, 37, 37, 255);
+font-weight: bold;
+background-color: rgba(255, 255, 255, 0);
+
+
+ Content
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/minimize/widget-countdown-day.ui b/ui/minimize/widget-countdown-day.ui
new file mode 100644
index 0000000..aab27d0
--- /dev/null
+++ b/ui/minimize/widget-countdown-day.ui
@@ -0,0 +1,115 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 180
+ 110
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 倒计日
+
+
+
+
+ 10
+ 10
+ 161
+ 41
+
+
+
+
+ Microsoft YaHei UI
+ 13
+ 75
+ true
+
+
+
+ color: rgba(0, 0, 0, 90);
+font-weight: bold;
+
+
+ 距离 中考 还有
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 20
+ 30
+ 141
+ 61
+
+
+
+
+ Microsoft YaHei UI
+ 19
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(37, 37, 37, 255);
+font-weight: bold
+
+
+ 300 天
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 10
+ 10
+ 161
+ 81
+
+
+
+ background-color: rgba(242, 243, 245, 255);
+border-radius: 8px
+
+
+
+
+
+
+ backgnd
+ countdown_custom_title
+ custom_countdown
+
+
+
+
diff --git a/ui/minimize/widget-countdown.ui b/ui/minimize/widget-countdown.ui
new file mode 100644
index 0000000..eaa38f9
--- /dev/null
+++ b/ui/minimize/widget-countdown.ui
@@ -0,0 +1,165 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 170
+ 110
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 活动倒计时
+
+
+
+
+ 20
+ 80
+ 131
+ 6
+
+
+
+
+ 0
+ 5
+
+
+
+
+ 16777215
+ 6
+
+
+
+
+
+
+ 75
+
+
+ false
+
+
+ Qt::Horizontal
+
+
+ QProgressBar::BottomToTop
+
+
+ true
+
+
+ 75.000000000000000
+
+
+
+
+
+ 10
+ 10
+ 151
+ 41
+
+
+
+
+ Microsoft YaHei UI
+ 13
+ 75
+ true
+
+
+
+ color: rgba(0, 0, 0, 90);
+font-weight: bold;
+
+
+ 倒计时
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 20
+ 30
+ 131
+ 61
+
+
+
+
+ Microsoft YaHei UI
+ 19
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(37, 37, 37, 255);
+font-weight: bold
+
+
+ 00:00
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 10
+ 10
+ 151
+ 81
+
+
+
+ background-color: rgba(242, 243, 245, 255);
+border-radius: 8px
+
+
+
+
+
+ backgnd
+ progressBar
+ activity_countdown_title
+ activity_countdown
+
+
+
+ ProgressBar
+ QProgressBar
+
+
+
+
+
+
diff --git a/ui/minimize/widget-current-activity.ui b/ui/minimize/widget-current-activity.ui
new file mode 100644
index 0000000..8f34ddf
--- /dev/null
+++ b/ui/minimize/widget-current-activity.ui
@@ -0,0 +1,137 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 290
+ 110
+
+
+
+
+ 0
+ 100
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 当前活动
+
+
+
+
+ 20
+ 30
+ 251
+ 61
+
+
+
+
+ Microsoft YaHei UI
+ 19
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(37, 37, 37, 255);
+font-weight: bold;
+
+
+ 测试
+
+
+
+ ../../../../../img/it.svg ../../../../../img/it.svg
+
+
+
+ 36
+ 26
+
+
+
+
+
+
+ 130
+ 35
+ 25
+ 25
+
+
+
+ background-color: rgb(0, 255, 127);
+border-radius:20px
+
+
+
+
+
+
+
+
+ 30
+ 10
+ 231
+ 41
+
+
+
+
+ Microsoft YaHei UI
+ 13
+ 75
+ true
+
+
+
+ color: rgba(0, 0, 0, 90);
+font-weight: bold;
+
+
+ 当前活动
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 10
+ 10
+ 271
+ 81
+
+
+
+ background-color: rgba(242, 243, 245, 255);
+border-radius: 8px
+
+
+
+
+
+
+ backgnd
+ blurEffect
+ sub_title
+ subject
+
+
+
+
diff --git a/ui/minimize/widget-next-activity.ui b/ui/minimize/widget-next-activity.ui
new file mode 100644
index 0000000..2d7a01a
--- /dev/null
+++ b/ui/minimize/widget-next-activity.ui
@@ -0,0 +1,115 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 240
+ 110
+
+
+
+
+ 0
+ 100
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 更多活动
+
+
+
+
+ 20
+ 10
+ 201
+ 41
+
+
+
+
+ Microsoft YaHei UI
+ 13
+ 75
+ true
+
+
+
+ color: rgba(0, 0, 0, 90);
+font-weight: bold;
+
+
+ 接下来
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 20
+ 30
+ 201
+ 61
+
+
+
+
+ Microsoft YaHei UI
+ 19
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(37, 37, 37, 255);
+font-weight: bold
+
+
+ 一 二 三 四 五
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 10
+ 10
+ 221
+ 81
+
+
+
+ background-color: rgba(242, 243, 245, 255);
+border-radius: 8px
+
+
+
+
+
+
+ backgnd
+ next_subtitle
+ next_lesson_text
+
+
+
+
diff --git a/ui/minimize/widget-time.ui b/ui/minimize/widget-time.ui
new file mode 100644
index 0000000..5cda201
--- /dev/null
+++ b/ui/minimize/widget-time.ui
@@ -0,0 +1,115 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 170
+ 110
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 时间
+
+
+
+
+ 10
+ 10
+ 151
+ 41
+
+
+
+
+ Microsoft YaHei UI
+ 13
+ 75
+ true
+
+
+
+ color: rgba(0, 0, 0, 90);
+font-weight: bold;
+
+
+ 2025 年 13 月
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 20
+ 30
+ 131
+ 61
+
+
+
+
+ Microsoft YaHei UI
+ 18
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(37, 37, 37, 255);
+font-weight: bold
+
+
+ 32 日 周二
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 10
+ 10
+ 151
+ 81
+
+
+
+ background-color: rgba(242, 243, 245, 255);
+border-radius: 8px
+
+
+
+
+
+
+ backgnd
+ date_text
+ day_text
+
+
+
+
diff --git a/ui/minimize/widget-weather.ui b/ui/minimize/widget-weather.ui
new file mode 100644
index 0000000..3926088
--- /dev/null
+++ b/ui/minimize/widget-weather.ui
@@ -0,0 +1,147 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 180
+ 110
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 天气
+
+
+
+
+ 10
+ 10
+ 161
+ 41
+
+
+
+
+ Microsoft YaHei UI
+ 13
+ 75
+ true
+
+
+
+ color: rgba(255, 255, 255, 185);
+font-weight: bold;
+
+
+ 当前城市
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 10
+ 10
+ 161
+ 81
+
+
+
+ background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:1, stop:0 rgba(40, 60, 110, 245), stop:1 rgba(75, 175, 245, 245));
+border-radius: 8px;
+border-image: url();
+
+
+
+
+
+
+
+
+
+ 30
+ 30
+ 121
+ 61
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+ ../../img/weather/0.svg
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ Microsoft YaHei UI
+ 19
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(255, 255, 255, 255);
+font-weight: bold
+
+
+ 114℉
+
+
+ Qt::PlainText
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
+
+
+
+
+
+ backgnd
+ current_city
+ horizontalLayoutWidget
+
+
+
+
diff --git a/ui/shiroko/dark/preview/widget-countdown-day.png b/ui/shiroko/dark/preview/widget-countdown-day.png
new file mode 100644
index 0000000..6f3db4b
Binary files /dev/null and b/ui/shiroko/dark/preview/widget-countdown-day.png differ
diff --git a/ui/shiroko/dark/preview/widget-countdown.png b/ui/shiroko/dark/preview/widget-countdown.png
new file mode 100644
index 0000000..46514c1
Binary files /dev/null and b/ui/shiroko/dark/preview/widget-countdown.png differ
diff --git a/ui/shiroko/dark/preview/widget-current-activity.png b/ui/shiroko/dark/preview/widget-current-activity.png
new file mode 100644
index 0000000..9797024
Binary files /dev/null and b/ui/shiroko/dark/preview/widget-current-activity.png differ
diff --git a/ui/shiroko/dark/preview/widget-custom.png b/ui/shiroko/dark/preview/widget-custom.png
new file mode 100644
index 0000000..c714428
Binary files /dev/null and b/ui/shiroko/dark/preview/widget-custom.png differ
diff --git a/ui/shiroko/dark/preview/widget-next-activity.png b/ui/shiroko/dark/preview/widget-next-activity.png
new file mode 100644
index 0000000..42ede81
Binary files /dev/null and b/ui/shiroko/dark/preview/widget-next-activity.png differ
diff --git a/ui/shiroko/dark/preview/widget-time.png b/ui/shiroko/dark/preview/widget-time.png
new file mode 100644
index 0000000..8748127
Binary files /dev/null and b/ui/shiroko/dark/preview/widget-time.png differ
diff --git a/ui/shiroko/dark/preview/widget-weather.png b/ui/shiroko/dark/preview/widget-weather.png
new file mode 100644
index 0000000..02db73d
Binary files /dev/null and b/ui/shiroko/dark/preview/widget-weather.png differ
diff --git a/ui/shiroko/dark/widget-base.ui b/ui/shiroko/dark/widget-base.ui
new file mode 100644
index 0000000..957bf43
--- /dev/null
+++ b/ui/shiroko/dark/widget-base.ui
@@ -0,0 +1,146 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 200
+ 125
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 基本组件
+
+
+
+ 20
+
+
+ 8
+
+
+ 8
+
+
+ 8
+
+
+ 24
+
+ -
+
+
+ background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:0, stop:0 rgba(0, 30, 50, 255), stop:1 rgba(15, 18, 22, 255));
+border-radius: 8px
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+
+ 0
+
+
+ 14
+
+
+ 14
+
+ -
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ color: rgba(255, 255, 255, 150);
+font-weight: bold;
+background-color: rgba(255, 255, 255, 0);
+
+
+ Title
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ -
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ Microsoft YaHei UI
+ 21
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(255, 255, 255, 255);
+font-weight: bold;
+background-color: rgba(255, 255, 255, 0);
+
+
+ Content
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/shiroko/dark/widget-countdown-day.ui b/ui/shiroko/dark/widget-countdown-day.ui
new file mode 100644
index 0000000..1ba06bb
--- /dev/null
+++ b/ui/shiroko/dark/widget-countdown-day.ui
@@ -0,0 +1,134 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 200
+ 125
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 倒计日
+
+
+
+
+ 20
+ 20
+ 161
+ 31
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ color: rgba(255, 255, 255, 150);
+font-weight: bold;
+
+
+ 距离 中考 还有
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 20
+ 30
+ 161
+ 71
+
+
+
+
+ Microsoft YaHei UI
+ 22
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(255, 255, 255, 255);
+font-weight: bold
+
+
+ 300 天
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 10
+ 10
+ 181
+ 91
+
+
+
+ background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:0, stop:0 rgba(0, 30, 50, 255), stop:1 rgba(15, 18, 22, 255));
+border-radius: 8px
+
+
+
+
+
+
+
+
+ 10
+ 10
+ 91
+ 91
+
+
+
+
+
+
+ ../img/2.png
+
+
+ true
+
+
+ backgnd
+ img
+ countdown_custom_title
+ custom_countdown
+
+
+
+
diff --git a/ui/shiroko/dark/widget-countdown.ui b/ui/shiroko/dark/widget-countdown.ui
new file mode 100644
index 0000000..35eaa02
--- /dev/null
+++ b/ui/shiroko/dark/widget-countdown.ui
@@ -0,0 +1,176 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 200
+ 125
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 活动倒计时
+
+
+
+
+ 20
+ 90
+ 161
+ 5
+
+
+
+
+ 0
+ 5
+
+
+
+
+ 16777215
+ 6
+
+
+
+
+
+
+ Qt::Horizontal
+
+
+ true
+
+
+ 0.000000000000000
+
+
+
+
+
+ 20
+ 20
+ 161
+ 31
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ color: rgba(255, 255, 255, 150);
+font-weight: bold;
+
+
+ 倒计时
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 20
+ 40
+ 161
+ 51
+
+
+
+
+ Microsoft YaHei UI
+ 21
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(255, 255, 255, 255);
+font-weight: bold
+
+
+ 00:00
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 10
+ 10
+ 181
+ 91
+
+
+
+ background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:0, stop:0 rgba(0, 30, 50, 255), stop:1 rgba(15, 18, 22, 255));
+border-radius: 8px
+
+
+
+
+
+
+
+
+ 100
+ 10
+ 91
+ 91
+
+
+
+
+
+
+ ../img/5.png
+
+
+ true
+
+
+ backgnd
+ img
+ progressBar
+ activity_countdown_title
+ activity_countdown
+
+
+
+ ProgressBar
+ QProgressBar
+
+
+
+
+
+
diff --git a/ui/shiroko/dark/widget-current-activity.ui b/ui/shiroko/dark/widget-current-activity.ui
new file mode 100644
index 0000000..f753021
--- /dev/null
+++ b/ui/shiroko/dark/widget-current-activity.ui
@@ -0,0 +1,161 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 360
+ 125
+
+
+
+
+ 0
+ 100
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 当前活动
+
+
+
+
+ 10
+ 40
+ 341
+ 51
+
+
+
+
+ Microsoft YaHei UI
+ 22
+ 75
+ true
+
+
+
+ QPushButton {
+ qproperty-iconSize: 24px;
+ color: white;
+ border: none;
+ color: rgba(255, 255, 255, 255);
+ font-weight: bold
+ }
+
+
+
+ 测试
+
+
+
+ ../../../img/it.svg ../../../img/it.svg
+
+
+
+ 24
+ 24
+
+
+
+
+
+
+ 170
+ 40
+ 35
+ 35
+
+
+
+ background-color: rgb(0, 255, 127);
+border-radius:20px
+
+
+
+
+
+
+
+
+ 30
+ 0
+ 301
+ 71
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ color: rgba(255, 255, 255, 150);
+font-weight: bold;
+
+
+ 当前活动
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 10
+ 10
+ 341
+ 91
+
+
+
+ background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:0, stop:0 rgba(0, 30, 50, 255), stop:1 rgba(15, 18, 22, 255));
+border-radius: 8px
+
+
+
+
+
+
+
+
+ 30
+ 10
+ 91
+ 91
+
+
+
+
+
+
+ ../img/1.png
+
+
+ true
+
+
+ backgnd
+ img
+ blurEffect
+ sub_title
+ subject
+
+
+
+
diff --git a/ui/shiroko/dark/widget-next-activity.ui b/ui/shiroko/dark/widget-next-activity.ui
new file mode 100644
index 0000000..12257b6
--- /dev/null
+++ b/ui/shiroko/dark/widget-next-activity.ui
@@ -0,0 +1,134 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 290
+ 125
+
+
+
+
+ 0
+ 100
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 更多活动
+
+
+
+
+ 40
+ 20
+ 211
+ 31
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ color: rgba(255, 255, 255, 150);
+font-weight: bold;
+
+
+ 接下来
+
+
+ Qt::PlainText
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
+
+
+
+
+
+ 40
+ 40
+ 221
+ 51
+
+
+
+
+ Microsoft YaHei UI
+ 21
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(255, 255, 255, 255);
+font-weight: bold
+
+
+ 测试测试
+
+
+ Qt::PlainText
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
+
+
+
+
+
+ 10
+ 10
+ 271
+ 91
+
+
+
+ background-color: qlineargradient(spread:pad, x1:1, y1:1, x2:0, y2:0, stop:0 rgba(0, 30, 50, 255), stop:1 rgba(15, 18, 22, 255));
+border-radius: 8px
+
+
+
+
+
+
+
+
+ 190
+ 10
+ 91
+ 91
+
+
+
+
+
+
+ ../img/4.png
+
+
+ true
+
+
+ backgnd
+ img
+ next_subtitle
+ next_lesson_text
+
+
+
+
diff --git a/ui/shiroko/dark/widget-time.ui b/ui/shiroko/dark/widget-time.ui
new file mode 100644
index 0000000..8a24023
--- /dev/null
+++ b/ui/shiroko/dark/widget-time.ui
@@ -0,0 +1,134 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 210
+ 125
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 时间
+
+
+
+
+ 20
+ 20
+ 161
+ 31
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ color: rgba(255, 255, 255, 150);
+font-weight: bold;
+
+
+ 2025 年 13 月
+
+
+ Qt::PlainText
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+
+
+ 30
+ 40
+ 151
+ 51
+
+
+
+
+ Microsoft YaHei UI
+ 21
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(255, 255, 255, 255);
+font-weight: bold
+
+
+ 32 日 周二
+
+
+ Qt::PlainText
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+
+
+ 10
+ 10
+ 191
+ 91
+
+
+
+ background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:0, stop:0 rgba(0, 30, 50, 255), stop:1 rgba(15, 18, 22, 255));
+border-radius: 8px
+
+
+
+
+
+
+
+
+ 10
+ 10
+ 91
+ 91
+
+
+
+
+
+
+ ../img/1.png
+
+
+ true
+
+
+ backgnd
+ img
+ date_text
+ day_text
+
+
+
+
diff --git a/ui/shiroko/dark/widget-weather.ui b/ui/shiroko/dark/widget-weather.ui
new file mode 100644
index 0000000..d96986a
--- /dev/null
+++ b/ui/shiroko/dark/widget-weather.ui
@@ -0,0 +1,166 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 200
+ 125
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 天气
+
+
+
+
+ 20
+ 20
+ 161
+ 31
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ color: rgba(255, 255, 255, 185);
+font-weight: bold;
+
+
+ 当前城市
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 10
+ 10
+ 181
+ 91
+
+
+
+ background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:1, stop:0 rgba(40, 60, 110, 245), stop:1 rgba(75, 175, 245, 245));
+border-radius: 8px;
+border-image: url();
+
+
+
+
+
+
+
+
+ 30
+ 10
+ 141
+ 111
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+ ../../img/weather/0.svg
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ Microsoft YaHei UI
+ 21
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(255, 255, 255, 255);
+font-weight: bold
+
+
+ 114℉
+
+
+ Qt::PlainText
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
+
+
+
+
+
+
+
+
+ 100
+ 10
+ 91
+ 91
+
+
+
+
+
+
+ img/4.png
+
+
+ true
+
+
+ backgnd
+ img
+ current_city
+ horizontalLayoutWidget
+
+
+
+
diff --git a/ui/shiroko/img/1.png b/ui/shiroko/img/1.png
new file mode 100644
index 0000000..50344e0
Binary files /dev/null and b/ui/shiroko/img/1.png differ
diff --git a/ui/shiroko/img/2.png b/ui/shiroko/img/2.png
new file mode 100644
index 0000000..8ae3a7d
Binary files /dev/null and b/ui/shiroko/img/2.png differ
diff --git a/ui/shiroko/img/3.png b/ui/shiroko/img/3.png
new file mode 100644
index 0000000..fad35ea
Binary files /dev/null and b/ui/shiroko/img/3.png differ
diff --git a/ui/shiroko/img/4.png b/ui/shiroko/img/4.png
new file mode 100644
index 0000000..9a5e22a
Binary files /dev/null and b/ui/shiroko/img/4.png differ
diff --git a/ui/shiroko/img/5.png b/ui/shiroko/img/5.png
new file mode 100644
index 0000000..c5a5a7e
Binary files /dev/null and b/ui/shiroko/img/5.png differ
diff --git a/ui/shiroko/preview/widget-countdown-day.png b/ui/shiroko/preview/widget-countdown-day.png
new file mode 100644
index 0000000..9e913a5
Binary files /dev/null and b/ui/shiroko/preview/widget-countdown-day.png differ
diff --git a/ui/shiroko/preview/widget-countdown.png b/ui/shiroko/preview/widget-countdown.png
new file mode 100644
index 0000000..871b97c
Binary files /dev/null and b/ui/shiroko/preview/widget-countdown.png differ
diff --git a/ui/shiroko/preview/widget-current-activity.png b/ui/shiroko/preview/widget-current-activity.png
new file mode 100644
index 0000000..53f590b
Binary files /dev/null and b/ui/shiroko/preview/widget-current-activity.png differ
diff --git a/ui/shiroko/preview/widget-custom.png b/ui/shiroko/preview/widget-custom.png
new file mode 100644
index 0000000..827a24c
Binary files /dev/null and b/ui/shiroko/preview/widget-custom.png differ
diff --git a/ui/shiroko/preview/widget-next-activity.png b/ui/shiroko/preview/widget-next-activity.png
new file mode 100644
index 0000000..f53fe1a
Binary files /dev/null and b/ui/shiroko/preview/widget-next-activity.png differ
diff --git a/ui/shiroko/preview/widget-time.png b/ui/shiroko/preview/widget-time.png
new file mode 100644
index 0000000..5a5db31
Binary files /dev/null and b/ui/shiroko/preview/widget-time.png differ
diff --git a/ui/shiroko/preview/widget-weather.png b/ui/shiroko/preview/widget-weather.png
new file mode 100644
index 0000000..74f0aed
Binary files /dev/null and b/ui/shiroko/preview/widget-weather.png differ
diff --git a/ui/shiroko/theme.json b/ui/shiroko/theme.json
new file mode 100644
index 0000000..fb3a07a
--- /dev/null
+++ b/ui/shiroko/theme.json
@@ -0,0 +1,17 @@
+{
+ "name": "砂狼白子",
+ "support_dark_mode": true,
+ "default_theme": null,
+ "radius": "8px",
+ "spacing": -5,
+ "shadow": true,
+ "height": 125,
+ "widget_width": {
+ "widget-time.ui": 210,
+ "widget-countdown.ui": 200,
+ "widget-current-activity.ui": 360,
+ "widget-next-activity.ui": 290,
+ "widget-countdown-day.ui": 200,
+ "widget-weather.ui": 200
+ }
+}
diff --git a/ui/shiroko/widget-base.ui b/ui/shiroko/widget-base.ui
new file mode 100644
index 0000000..749f335
--- /dev/null
+++ b/ui/shiroko/widget-base.ui
@@ -0,0 +1,146 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 200
+ 125
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 基本组件
+
+
+
+ 22
+
+
+ 8
+
+
+ 8
+
+
+ 8
+
+
+ 24
+
+ -
+
+
+ background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:0, stop:0 rgba(203, 239, 249, 255), stop:1 rgba(255, 255, 255, 255));
+border-radius: 8px
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+
+ 0
+
+
+ 14
+
+
+ 14
+
+ -
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ color: rgba(0, 0, 0, 90);
+background-color: rgba(255, 255, 255, 0);
+font-weight: bold;
+
+
+ Title
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ -
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ Microsoft YaHei UI
+ 21
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(37, 37, 37, 255);
+font-weight: bold;
+background-color: rgba(255, 255, 255, 0);
+
+
+ Content
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/shiroko/widget-countdown-day.ui b/ui/shiroko/widget-countdown-day.ui
new file mode 100644
index 0000000..79d0b71
--- /dev/null
+++ b/ui/shiroko/widget-countdown-day.ui
@@ -0,0 +1,134 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 200
+ 125
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 倒计日
+
+
+
+
+ 20
+ 20
+ 161
+ 31
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ color: rgba(0, 0, 0, 90);
+font-weight: bold;
+
+
+ 距离 中考 还有
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 20
+ 30
+ 161
+ 71
+
+
+
+
+ Microsoft YaHei UI
+ 22
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(37, 37, 37, 255);
+font-weight: bold
+
+
+ 300 天
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 10
+ 10
+ 181
+ 91
+
+
+
+ background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:0, stop:0 rgba(203, 239, 249, 255), stop:1 rgba(255, 255, 255, 255));
+border-radius: 8px
+
+
+
+
+
+
+
+
+ 10
+ 10
+ 91
+ 91
+
+
+
+
+
+
+ img/2.png
+
+
+ true
+
+
+ backgnd
+ img
+ countdown_custom_title
+ custom_countdown
+
+
+
+
diff --git a/ui/shiroko/widget-countdown.ui b/ui/shiroko/widget-countdown.ui
new file mode 100644
index 0000000..8705468
--- /dev/null
+++ b/ui/shiroko/widget-countdown.ui
@@ -0,0 +1,176 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 200
+ 125
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 活动倒计时
+
+
+
+
+ 20
+ 90
+ 161
+ 5
+
+
+
+
+ 0
+ 5
+
+
+
+
+ 16777215
+ 6
+
+
+
+
+
+
+ Qt::Horizontal
+
+
+ true
+
+
+ 0.000000000000000
+
+
+
+
+
+ 20
+ 20
+ 161
+ 31
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ color: rgba(0, 0, 0, 90);
+font-weight: bold;
+
+
+ 倒计时
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 20
+ 40
+ 161
+ 51
+
+
+
+
+ Microsoft YaHei UI
+ 21
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(37, 37, 37, 255);
+font-weight: bold
+
+
+ 00:00
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 10
+ 10
+ 181
+ 91
+
+
+
+ background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:0, stop:0 rgba(203, 239, 249, 255), stop:1 rgba(255, 255, 255, 255));
+border-radius: 8px
+
+
+
+
+
+
+
+
+ 10
+ 10
+ 91
+ 91
+
+
+
+
+
+
+ img/3.png
+
+
+ true
+
+
+ backgnd
+ img
+ progressBar
+ activity_countdown_title
+ activity_countdown
+
+
+
+ ProgressBar
+ QProgressBar
+
+
+
+
+
+
diff --git a/ui/shiroko/widget-current-activity.ui b/ui/shiroko/widget-current-activity.ui
new file mode 100644
index 0000000..46569cf
--- /dev/null
+++ b/ui/shiroko/widget-current-activity.ui
@@ -0,0 +1,156 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 360
+ 125
+
+
+
+
+ 0
+ 100
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 当前活动
+
+
+
+
+ 10
+ 40
+ 341
+ 51
+
+
+
+
+ Microsoft YaHei UI
+ 22
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(37, 37, 37, 255);
+font-weight: bold;
+
+
+ 测试
+
+
+
+ ../../../../../img/it.svg ../../../../../img/it.svg
+
+
+
+ 36
+ 26
+
+
+
+
+
+
+ 155
+ 35
+ 45
+ 45
+
+
+
+ background-color: rgb(0, 255, 127);
+border-radius:20px
+
+
+
+
+
+
+
+
+ 30
+ 0
+ 301
+ 71
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ color: rgba(0, 0, 0, 90);
+font-weight: bold;
+
+
+ 当前活动
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 10
+ 10
+ 341
+ 91
+
+
+
+ background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:0, stop:0 rgba(203, 239, 249, 255), stop:1 rgba(255, 255, 255, 255));
+border-radius: 8px
+
+
+
+
+
+
+
+
+ 30
+ 10
+ 91
+ 91
+
+
+
+
+
+
+ img/1.png
+
+
+ true
+
+
+ backgnd
+ img
+ blurEffect
+ sub_title
+ subject
+
+
+
+
diff --git a/ui/shiroko/widget-next-activity.ui b/ui/shiroko/widget-next-activity.ui
new file mode 100644
index 0000000..be56055
--- /dev/null
+++ b/ui/shiroko/widget-next-activity.ui
@@ -0,0 +1,134 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 290
+ 125
+
+
+
+
+ 0
+ 100
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 更多活动
+
+
+
+
+ 40
+ 20
+ 211
+ 31
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ color: rgba(0, 0, 0, 90);
+font-weight: bold;
+
+
+ 接下来
+
+
+ Qt::PlainText
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
+
+
+
+
+
+ 40
+ 40
+ 221
+ 51
+
+
+
+
+ Microsoft YaHei UI
+ 21
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(37, 37, 37, 255);
+font-weight: bold
+
+
+ 测试测试
+
+
+ Qt::PlainText
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
+
+
+
+
+
+ 10
+ 10
+ 271
+ 91
+
+
+
+ background-color: qlineargradient(spread:pad, x1:1, y1:1, x2:0, y2:0, stop:0 rgba(203, 239, 249, 255), stop:1 rgba(255, 255, 255, 255));
+border-radius: 8px
+
+
+
+
+
+
+
+
+ 180
+ 10
+ 91
+ 91
+
+
+
+
+
+
+ img/4.png
+
+
+ true
+
+
+ backgnd
+ img
+ next_subtitle
+ next_lesson_text
+
+
+
+
diff --git a/ui/shiroko/widget-time.ui b/ui/shiroko/widget-time.ui
new file mode 100644
index 0000000..20f7b63
--- /dev/null
+++ b/ui/shiroko/widget-time.ui
@@ -0,0 +1,134 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 210
+ 125
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 时间
+
+
+
+
+ 20
+ 20
+ 161
+ 31
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ color: rgba(0, 0, 0, 90);
+font-weight: bold;
+
+
+ 2025 年 13 月
+
+
+ Qt::PlainText
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+
+
+ 30
+ 40
+ 151
+ 51
+
+
+
+
+ Microsoft YaHei UI
+ 21
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(37, 37, 37, 255);
+font-weight: bold
+
+
+ 32 日 周二
+
+
+ Qt::PlainText
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+
+
+ 10
+ 10
+ 191
+ 91
+
+
+
+ background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:0, stop:0 rgba(203, 239, 249, 255), stop:1 rgba(255, 255, 255, 255));
+border-radius: 8px
+
+
+
+
+
+
+
+
+ 10
+ 10
+ 91
+ 91
+
+
+
+
+
+
+ img/1.png
+
+
+ true
+
+
+ backgnd
+ img
+ date_text
+ day_text
+
+
+
+
diff --git a/ui/shiroko/widget-weather.ui b/ui/shiroko/widget-weather.ui
new file mode 100644
index 0000000..d96986a
--- /dev/null
+++ b/ui/shiroko/widget-weather.ui
@@ -0,0 +1,166 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 200
+ 125
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ 天气
+
+
+
+
+ 20
+ 20
+ 161
+ 31
+
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ true
+
+
+
+ color: rgba(255, 255, 255, 185);
+font-weight: bold;
+
+
+ 当前城市
+
+
+ Qt::PlainText
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 10
+ 10
+ 181
+ 91
+
+
+
+ background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:1, stop:0 rgba(40, 60, 110, 245), stop:1 rgba(75, 175, 245, 245));
+border-radius: 8px;
+border-image: url();
+
+
+
+
+
+
+
+
+ 30
+ 10
+ 141
+ 111
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+ ../../img/weather/0.svg
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ Microsoft YaHei UI
+ 21
+ 75
+ true
+
+
+
+ border: none;
+color: rgba(255, 255, 255, 255);
+font-weight: bold
+
+
+ 114℉
+
+
+ Qt::PlainText
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
+
+
+
+
+
+
+
+
+ 100
+ 10
+ 91
+ 91
+
+
+
+
+
+
+ img/4.png
+
+
+ true
+
+
+ backgnd
+ img
+ current_city
+ horizontalLayoutWidget
+
+
+
+
diff --git a/utils.py b/utils.py
new file mode 100644
index 0000000..82749ac
--- /dev/null
+++ b/utils.py
@@ -0,0 +1,266 @@
+import os
+import sys
+import psutil
+
+from PyQt5.QtGui import QIcon
+from PyQt5.QtWidgets import QSystemTrayIcon, QApplication
+from loguru import logger
+from PyQt5.QtCore import QSharedMemory, QTimer, QObject, pyqtSignal
+import darkdetect
+import datetime as dt
+
+from file import base_directory, config_center
+import signal
+
+share = QSharedMemory('ClassWidgets')
+_stop_in_progress = False
+
+def restart():
+ logger.debug('重启程序')
+ app = QApplication.instance()
+ if app:
+ try:
+ signal.signal(signal.SIGTERM, signal.SIG_DFL)
+ signal.signal(signal.SIGINT, signal.SIG_DFL)
+ except (AttributeError, ValueError):
+ pass
+ app.quit()
+ app.processEvents()
+
+ if share.isAttached():
+ share.detach() # 释放共享内存
+ os.execl(sys.executable, sys.executable, *sys.argv)
+
+def stop(status=0):
+ global share, update_timer, _stop_in_progress
+ if _stop_in_progress:
+ return
+ _stop_in_progress = True
+
+ logger.debug('退出程序...')
+
+ if 'update_timer' in globals() and update_timer:
+ try:
+ update_timer.stop()
+ update_timer = None
+ except Exception as e:
+ logger.warning(f"停止全局更新定时器时出错: {e}")
+
+ app = QApplication.instance()
+ if app:
+ try:
+ signal.signal(signal.SIGTERM, signal.SIG_DFL)
+ signal.signal(signal.SIGINT, signal.SIG_DFL)
+ except (AttributeError, ValueError):
+ pass
+ app.quit()
+ try:
+ current_pid = os.getpid()
+ parent = psutil.Process(current_pid)
+ children = parent.children(recursive=True)
+ if children:
+ logger.debug(f"尝试终止 {len(children)} 个子进程...")
+ for child in children:
+ try:
+ logger.debug(f"终止子进程 {child.pid}...")
+ child.terminate()
+ except psutil.NoSuchProcess:
+ logger.debug(f"子进程 {child.pid} 已不存在.")
+ continue
+ except psutil.AccessDenied:
+ logger.warning(f"无权限终止子进程 {child.pid}.")
+ continue
+ except Exception as e:
+ logger.warning(f"终止子进程 {child.pid} 时出错: {e}")
+
+ gone, alive = psutil.wait_procs(children, timeout=1.5)
+ if alive:
+ logger.warning(f"{len(alive)} 个子进程未在规定时间内终止,将强制终止...")
+ for p in alive:
+ try:
+ logger.debug(f"强制终止子进程 {p.pid}...")
+ p.kill()
+ except psutil.NoSuchProcess:
+ logger.debug(f"子进程 {p.pid} 在强制终止前已消失.")
+ except Exception as e:
+ logger.error(f"强制终止子进程 {p.pid} 失败: {e}")
+ except psutil.NoSuchProcess:
+ logger.warning("无法获取当前进程信息,跳过子进程终止。")
+ except Exception as e:
+ logger.error(f"终止子进程时出现意外错误: {e}")
+
+ if 'share' in globals() and share:
+ try:
+ if share.isAttached():
+ share.detach()
+ logger.debug("共享内存已分离")
+ except Exception as e:
+ logger.error(f"分离共享内存时出错: {e}")
+
+ logger.debug(f"程序退出({status})")
+ if not app:
+ os._exit(status)
+
+def calculate_size(p_w=0.6, p_h=0.7): # 计算尺寸
+ screen_geometry = QApplication.primaryScreen().geometry()
+ screen_width = screen_geometry.width()
+ screen_height = screen_geometry.height()
+
+ width = int(screen_width * p_w)
+ height = int(screen_height * p_h)
+
+ return (width, height), (int(screen_width / 2 - width / 2), 150)
+
+def update_tray_tooltip():
+ """更新托盘文字"""
+ if hasattr(sys.modules[__name__], 'tray_icon'):
+ tray_instance = getattr(sys.modules[__name__], 'tray_icon')
+ if tray_instance is not None:
+ schedule_name_from_conf = config_center.read_conf('General', 'schedule')
+ if schedule_name_from_conf:
+ try:
+ schedule_display_name = schedule_name_from_conf
+ if schedule_display_name.endswith('.json'):
+ schedule_display_name = schedule_display_name[:-5]
+ tray_instance.setToolTip(f'Class Widgets - "{schedule_display_name}"')
+ logger.info(f'托盘文字更新: "Class Widgets - {schedule_display_name}"')
+ except Exception as e:
+ logger.error(f"更新托盘提示时发生错误: {e}")
+ else:
+ tray_instance.setToolTip("Class Widgets - 未加载课表")
+ logger.info(f'托盘文字更新: "Class Widgets - 未加载课表"')
+
+class DarkModeWatcher(QObject):
+ darkModeChanged = pyqtSignal(bool) # 发出暗黑模式变化信号
+ def __init__(self, interval=500, parent=None):
+ super().__init__(parent)
+ self._isDarkMode = darkdetect.isDark() # 初始状态
+ self._timer = QTimer(self)
+ self._timer.timeout.connect(self._checkTheme)
+ self._timer.start(interval) # 轮询间隔(毫秒)
+
+ def _checkTheme(self):
+ currentMode = darkdetect.isDark()
+ if currentMode != self._isDarkMode:
+ self._isDarkMode = currentMode
+ self.darkModeChanged.emit(currentMode) # 发出变化信号
+
+ def isDark(self):
+ """返回当前是否暗黑模式"""
+ return self._isDarkMode
+
+ def stop(self):
+ """停止监听"""
+ self._timer.stop()
+
+ def start(self, interval=None):
+ """开始监听"""
+ if interval:
+ self._timer.setInterval(interval)
+ self._timer.start()
+
+
+class TrayIcon(QSystemTrayIcon):
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.setIcon(QIcon(f"{base_directory}/img/logo/favicon.png"))
+
+ def push_update_notification(self, text=''):
+ self.setIcon(QIcon(f"{base_directory}/img/logo/favicon-update.png")) # tray
+ self.showMessage(
+ "发现 Class Widgets 新版本!",
+ text,
+ QIcon(f"{base_directory}/img/logo/favicon-update.png"),
+ 5000
+ )
+
+ def push_error_notification(self, title='检查更新失败!', text=''):
+ self.setIcon(QIcon(f"{base_directory}/img/logo/favicon-update.png")) # tray
+ self.showMessage(
+ title,
+ text,
+ QIcon(f"{base_directory}/img/logo/favicon-error.ico"),
+ 5000
+ )
+
+
+class UnionUpdateTimer(QObject):
+ """
+ 统一更新计时器
+ """
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.timer = QTimer(self)
+ self.timer.timeout.connect(self._on_timeout)
+ self.callbacks = [] # 存储所有的回调函数
+ self._is_running = False
+
+ def _on_timeout(self): # 超时
+ app = QApplication.instance()
+ if not app or app.closingDown():
+ if self.timer.isActive():
+ self.timer.stop()
+ return
+
+ # 使用最初的备份列表,防止遍历时修改
+ callbacks_copy = self.callbacks[:]
+ for callback in callbacks_copy:
+ if callback in self.callbacks:
+ try:
+ callback()
+ except RuntimeError as e:
+ logger.error(f"回调调用错误 (可能对象已删除): {e}")
+ try:
+ self.callbacks.remove(callback)
+ except ValueError:
+ pass
+ except Exception as e:
+ logger.error(f"执行回调时发生未知错误: {e}")
+ if self._is_running:
+ self._schedule_next()
+
+ def _schedule_next(self):
+ now = dt.datetime.now()
+ next_tick = now.replace(microsecond=0) + dt.timedelta(seconds=1)
+ delay = max(0, int((next_tick - now).total_seconds() * 1000))
+ self.timer.start(delay)
+
+ def add_callback(self, callback):
+ if callback not in self.callbacks:
+ self.callbacks.append(callback)
+ if not self._is_running:
+ self.start()
+
+ def remove_callback(self, callback):
+ try:
+ self.callbacks.remove(callback)
+ except ValueError:
+ pass
+ # if not self.callbacks and self._is_running:
+ # self.stop() # 删除定时器
+
+ def remove_all_callbacks(self):
+ self.callbacks = []
+ # self.stop() # 删除定时器
+
+ def start(self):
+ if not self._is_running:
+ logger.debug("启动 UnionUpdateTimer...")
+ self._is_running = True
+ self._schedule_next()
+
+ def stop(self):
+ self._is_running = False
+ if self.timer:
+ try:
+ if self.timer.isActive():
+ self.timer.stop()
+ except RuntimeError as e:
+ logger.warning(f"停止 QTimer 时发生运行时错误: {e}")
+ except Exception as e:
+ logger.error(f"停止 QTimer 时发生未知错误: {e}")
+
+
+tray_icon = None
+update_timer = UnionUpdateTimer()
diff --git a/view/extra_menu.ui b/view/extra_menu.ui
new file mode 100644
index 0000000..8e53ece
--- /dev/null
+++ b/view/extra_menu.ui
@@ -0,0 +1,372 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 764
+ 683
+
+
+
+ Form
+
+
+
+ 18
+
+
+ 24
+
+
+ 24
+
+
+ 24
+
+
+ 24
+
+ -
+
+
+ 额外选项
+
+
+
+ -
+
+
+ background: transparent; border: none
+
+
+ true
+
+
+
+
+ 0
+ 0
+ 716
+ 525
+
+
+
+ -
+
+
+ 调休
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+ 设置调休星期
+
+
+
+ -
+
+
+ 将替换当前调休日的课程表为选定星期
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+
+ -
+
+
+ 0
+
+ -
+
+
+ 换课
+
+
+
+ -
+
+
+ 临时替换当天的课程,重启后失效
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 275
+
+
+
+ QAbstractScrollArea::AdjustToContents
+
+
+
+ -
+
+
+ 5
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 课程/活动
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 自定义课程
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ false
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+ 16
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ *所有更改在重启后重置
+
+
+
+ -
+
+
+ 浏览更多设置
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 150
+ 0
+
+
+
+ 保存
+
+
+
+
+
+
+
+
+
+ ComboBox
+ QPushButton
+
+
+
+ HyperlinkButton
+ PushButton
+
+
+
+ PushButton
+ QPushButton
+
+
+
+ PrimaryPushButton
+ PushButton
+
+
+
+ ToolButton
+ QToolButton
+
+
+
+ CardWidget
+ QFrame
+
+ 1
+
+
+ SmoothScrollArea
+ QScrollArea
+
+ 1
+
+
+ CaptionLabel
+ QLabel
+
+
+
+ BodyLabel
+ QLabel
+
+
+
+ StrongBodyLabel
+ QLabel
+
+
+
+ SubtitleLabel
+ QLabel
+
+
+
+ TitleLabel
+ QLabel
+
+
+
+ LineEdit
+ QLineEdit
+
+
+
+ ListWidget
+ QListWidget
+
+
+
+
+
+
diff --git a/view/menu/about.ui b/view/menu/about.ui
new file mode 100644
index 0000000..025c7b7
--- /dev/null
+++ b/view/menu/about.ui
@@ -0,0 +1,593 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 821
+ 843
+
+
+
+ Form
+
+
+
+ 18
+
+
+ 24
+
+
+ 24
+
+
+ 24
+
+
+ 24
+
+ -
+
+
+ 关于
+
+
+
+ -
+
+
+ background: transparent; border: none
+
+
+ true
+
+
+
+
+ 0
+ 0
+ 773
+ 716
+
+
+
+
+ 14
+
+ -
+
+
+ 48
+
+
+ 0
+
+
+ 48
+
+
+ 0
+
+ -
+
+ -
+
+
+ true
+
+
+
+ 0
+ 0
+
+
+
+
+ 128
+ 128
+
+
+
+
+
+
+
+
+
+ ../../img/Logo.png
+
+
+ true
+
+
+ Qt::AlignCenter
+
+
+ true
+
+
+
+
+
+ -
+
+
+ Class Widgets
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ 版本:获取失败!
+
+
+ Qt::AlignCenter
+
+
+ true
+
+
+
+ -
+
+
+ Class Widgets 是一款能显示当前课程的桌面组件App。其提供了直观的图形化课程表编辑和美观的桌面组件。
+
+
+ Qt::AlignCenter
+
+
+ true
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+ -
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 360
+ 16777215
+
+
+
+ 此项目的 Github
+
+
+ false
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 360
+ 16777215
+
+
+
+ 我的 哔哩哔哩 主页
+
+
+
+
+
+ -
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 360
+ 16777215
+
+
+
+ 查看开放源代码许可
+
+
+ false
+
+
+
+
+
+ -
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 360
+ 16777215
+
+
+
+ 鸣谢
+
+
+ false
+
+
+
+
+
+
+
+ -
+
+
+ 3
+
+
+ 12
+
+ -
+
+ -
+
+
+ 更新
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 检查更新
+
+
+
+
+
+ -
+
+
+
+ 0
+ 70
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+ 选择更新通道
+
+
+
+ -
+
+
+ 将会获取选定更新通道的版本
+
+
+ false
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 70
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 启动 Class Widgets 时自动检查更新
+
+
+
+ -
+
+
+ 若启用,Class Widgets 将在启动时联网检查选定的更新通道中是否有最新版本更新。
+
+
+ true
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+ 启用
+
+
+ 禁用
+
+
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 106
+
+
+
+
+
+
+
+
+ -
+
+
+ Copyright © 2025 RinLit, All Rights Reversed.
+
+
+ Qt::AlignCenter
+
+
+
+ 127
+ 127
+ 127
+
+
+
+
+ 185
+ 185
+ 185
+
+
+
+
+
+
+
+
+ ComboBox
+ QPushButton
+
+
+
+ PushButton
+ QPushButton
+
+
+
+ PrimaryPushButton
+ PushButton
+
+
+
+ SwitchButton
+ QWidget
+
+
+
+ CardWidget
+ QFrame
+
+ 1
+
+
+ SmoothScrollArea
+ QScrollArea
+
+ 1
+
+
+ CaptionLabel
+ QLabel
+
+
+
+ BodyLabel
+ QLabel
+
+
+
+ StrongBodyLabel
+ QLabel
+
+
+
+ SubtitleLabel
+ QLabel
+
+
+
+ TitleLabel
+ QLabel
+
+
+
+
+
+
diff --git a/view/menu/advance.ui b/view/menu/advance.ui
new file mode 100644
index 0000000..0e25181
--- /dev/null
+++ b/view/menu/advance.ui
@@ -0,0 +1,1701 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 765
+ 1331
+
+
+
+
+ 0
+ 0
+
+
+
+ Form
+
+
+
+ 18
+
+
+ 24
+
+
+ 24
+
+
+ 24
+
+
+ 0
+
+ -
+
+
+ 高级选项
+
+
+
+ -
+
+
+ background: transparent; border: none
+
+
+ true
+
+
+
+
+ 0
+ 0
+ 717
+ 1812
+
+
+
+ -
+
+
+ 3
+
+ -
+
+
+ 课程
+
+
+
+ -
+
+
+
+ 0
+ 70
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+ 时差偏移
+
+
+
+ -
+
+
+ 修正系统时间与学校铃声的时差,学校铃声慢于系统时间为正值,反之为负
+
+
+ false
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ -114514
+
+
+ 114514
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 70
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+ 选择课程表配置
+
+
+
+ -
+
+
+ 课程表配置将存储于 本软件根目录\config\schedule 下
+
+
+ false
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 70
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 启用 单/双 周课表
+
+
+
+ -
+
+
+ 若要启用此选项,需设定开学日期以计算
+
+
+ false
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+ 启用
+
+
+ 关闭
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 70
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 选取开学日期
+
+
+
+ -
+
+
+ 将用于计算单/双周,开学日期需设置为开学第一周第一天(即周一)
+
+
+ false
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+ 选取开学日期
+
+
+
+
+
+
+
+
+ -
+
+ -
+
+
+ 外观
+
+
+
+ -
+
+
+ 隐藏方式
+
+
+
+ -
+
+
+ 隐藏方式将会修改单击隐藏和自动隐藏的行为,可按需更改(重启后生效)
+
+
+ false
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+ -
+
+
+ 6
+
+
+ 12
+
+ -
+
+
+ 9
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 200
+ 135
+
+
+
+
+ 16777215
+ 135
+
+
+
+ ../../img/settings/default.png
+
+
+ true
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 默认
+
+
+
+
+
+ -
+
+
+ 9
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 200
+ 135
+
+
+
+
+ 16777215
+ 135
+
+
+
+ ../../img/settings/hide_all.png
+
+
+ true
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 全部隐藏
+
+
+
+
+
+ -
+
+
+ 9
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 200
+ 135
+
+
+
+
+ 16777215
+ 135
+
+
+
+ ../../img/settings/floating.png
+
+
+ true
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 最小化为浮窗(推荐)
+
+
+
+
+
+
+
+ -
+
+
+ 其他
+
+
+
+ -
+
+
+
+ 0
+ 70
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 自动隐藏
+
+
+
+ -
+
+
+ 选择你需要的自动隐藏方式
+
+
+ false
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+ -
+
+
+ 什么是灵活隐藏?
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+ 0
+ 70
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 特定课程不自动隐藏
+
+
+
+ -
+
+
+ 若启用,在遇到下方设置的特定课程时不会自动隐藏,以英文逗号分隔
+
+
+ false
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+ 启用
+
+
+ 禁用
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 70
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 不自动隐藏的课程
+
+
+
+ -
+
+
+ 配合 特定课程不自动隐藏 使用
+
+
+ false
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 70
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+ 0
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 允许点击或触摸小组件
+
+
+
+ -
+
+
+ 允许通过点击或触摸小组件方式控制小组件
+若启用,单击小组件可显示或隐藏小组件,右键小组件可打开额外选项
+若禁用,点击小组件等同于点击小组件后方的窗口
+* 重启后生效
+
+
+ false
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+ 启用
+
+
+ 禁用
+
+
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 70
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+ 0
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 自定义缩放
+
+
+
+ -
+
+
+ 更改自定义缩放系数百分比(重启后生效)
+*不建议使用 180% 以上的值,这可能会导致显示异常
+
+
+ false
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 100
+
+
+ 200
+
+
+ 100
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 70
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+ 置顶/置底小组件
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 更改小组件的窗口状态(重启后生效)
+*开启“置底”功能时,将会禁用“单击隐藏小组件”
+
+
+ false
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+ 0
+ 70
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+ 边距大小
+
+
+
+ -
+
+
+ 设定桌面组件离屏幕边缘的大小(单位:px)
+
+
+ false
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 130
+ 33
+
+
+
+ -15
+
+
+
+
+
+
+
+
+ -
+
+ -
+
+
+ 启动
+
+
+
+ -
+
+
+
+ 0
+ 70
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 开机自启动
+
+
+
+ -
+
+
+ 启用
+
+
+ 禁用
+
+
+
+
+
+
+
+
+ -
+
+
+ 3
+
+ -
+
+
+ 其他
+
+
+
+ -
+
+
+
+ 0
+ 70
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 安全模式
+
+
+
+ -
+
+
+ 若启用,Class Widgets 将在程序崩溃时自动忽略,并不再弹出窗口;以免影响教学任务。
+
+
+ true
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+ 启用
+
+
+ 禁用
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 70
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 禁用日志
+
+
+
+ -
+
+
+ 若启用,应用将不再会保存日志到本地
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+ 启用
+
+
+ 禁用
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 70
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 清空日志
+
+
+
+ -
+
+
+ 将会清空 软件根目录下log. 的所有内容
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+ 清空日志
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 70
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 允许程序多开
+
+
+
+ -
+
+
+ 程序多开后可能出现未知的问题,请谨慎使用
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+ 不允许
+
+
+ 允许
+
+
+ 不允许
+
+
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 25
+
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+ 5
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+
+
+
+
+ ComboBox
+ QPushButton
+
+
+
+ PushButton
+ QPushButton
+
+
+
+ SwitchButton
+ QWidget
+
+
+
+ RadioButton
+ QRadioButton
+
+
+
+ Slider
+ QSlider
+
+
+
+ CardWidget
+ QFrame
+
+ 1
+
+
+ SmoothScrollArea
+ QScrollArea
+
+ 1
+
+
+ CalendarPicker
+ QPushButton
+
+
+
+ CaptionLabel
+ QLabel
+
+
+
+ StrongBodyLabel
+ QLabel
+
+
+
+ SubtitleLabel
+ QLabel
+
+
+
+ TitleLabel
+ QLabel
+
+
+
+ HyperlinkLabel
+ QPushButton
+
+
+
+ LineEdit
+ QLineEdit
+
+
+
+ SpinBox
+ QSpinBox
+
+
+
+
+
+
diff --git a/view/menu/configs.ui b/view/menu/configs.ui
new file mode 100644
index 0000000..4a5c8d8
--- /dev/null
+++ b/view/menu/configs.ui
@@ -0,0 +1,540 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 721
+ 814
+
+
+
+ Form
+
+
+
+ 18
+
+
+ 24
+
+
+ 24
+
+
+ 24
+
+
+ 24
+
+ -
+
+
+ 配置文件
+
+
+
+ -
+
+
+ background: transparent; border: none
+
+
+ true
+
+
+
+
+ 0
+ 0
+ 673
+ 709
+
+
+
+ -
+
+
+ 3
+
+ -
+
+
+ 课程表
+
+
+
+ -
+
+
+
+ 0
+ 60
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 导入 Class Widgets 课程表
+
+
+
+ -
+
+
+ 需导入从其他 Class Widgets 导出的课程表
+
+
+ true
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+ 导入课程表
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 60
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 导出 Class Widgets 课程表
+
+
+
+ -
+
+
+ 将当前使用的课程表文件 (.json) 导出
+
+
+ true
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+ 导出课程表
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 60
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 管理 Class Widgets 课程表
+
+
+
+ -
+
+
+ 打开 Class Widgets 课程表文件夹
+
+
+ true
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+ 使用“资源管理器”打开
+
+
+
+
+
+
+
+
+ -
+
+
+ 3
+
+ -
+
+
+ 通用课程表交换格式(CSES)
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 什么是 CSES?
+
+
+
+ -
+
+
+
+ 0
+ 60
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 导入 CSES 格式的课程表
+
+
+
+ -
+
+
+ 需导入从其他支持的软件导出的 CSES 格式的课程表
+注意:由 CSES 格式转换的 Class Widgets 课程表可读性可能降低
+
+
+ true
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+ 导入 CSES 文件
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 60
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 导出 Class Widgets 课程表
+
+
+
+ -
+
+
+ 将当前使用的课程表文件 (.yaml) 导出为 CSES 格式
+注意:生成的 CSES 课程表可读性可能降低
+
+
+ true
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+ 导出 CSES 文件
+
+
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+
+
+
+
+ HyperlinkButton
+ PushButton
+
+
+
+ PushButton
+ QPushButton
+
+
+
+ CardWidget
+ QFrame
+
+ 1
+
+
+ SmoothScrollArea
+ QScrollArea
+
+ 1
+
+
+ CaptionLabel
+ QLabel
+
+
+
+ StrongBodyLabel
+ QLabel
+
+
+
+ SubtitleLabel
+ QLabel
+
+
+
+ TitleLabel
+ QLabel
+
+
+
+
+
+
diff --git a/view/menu/countdown_custom_edit.ui b/view/menu/countdown_custom_edit.ui
new file mode 100644
index 0000000..46f506c
--- /dev/null
+++ b/view/menu/countdown_custom_edit.ui
@@ -0,0 +1,296 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 648
+ 627
+
+
+
+ Form
+
+
+
+ 18
+
+
+ 24
+
+
+ 24
+
+
+ 24
+
+
+ 24
+
+ -
+
+
+ 自定义倒计时编辑
+
+
+
+ -
+
+ -
+
+
+ 自定义倒计时
+
+
+
+
+
+ -
+
+
+ 12
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Qt::IgnoreAction
+
+
+ true
+
+
+
+
+
+ -
+
+
+ 5
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 自定义文本
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 选择日期
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 150
+ 0
+
+
+
+ 选定一个日期
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+ -
+
+
+ 2
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 倒计时模式
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 轮播间隔(秒)
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::Expanding
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 150
+ 0
+
+
+
+ 保存
+
+
+
+
+
+
+
+
+
+ ComboBox
+ QPushButton
+
+
+
+ PushButton
+ QPushButton
+
+
+
+ PrimaryPushButton
+ PushButton
+
+
+
+ ToolButton
+ QToolButton
+
+
+
+ CalendarPicker
+ QPushButton
+
+
+
+ BodyLabel
+ QLabel
+
+
+
+ SubtitleLabel
+ QLabel
+
+
+
+ TitleLabel
+ QLabel
+
+
+
+ LineEdit
+ QLineEdit
+
+
+
+ SpinBox
+ QSpinBox
+
+
+
+ ListWidget
+ QListWidget
+
+
+
+
+
+
diff --git a/view/menu/custom.ui b/view/menu/custom.ui
new file mode 100644
index 0000000..9a32910
--- /dev/null
+++ b/view/menu/custom.ui
@@ -0,0 +1,1227 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 706
+ 1191
+
+
+
+
+ 0
+ 0
+
+
+
+ Form
+
+
+
+ 18
+
+
+ 24
+
+
+ 24
+
+
+ 24
+
+
+ 12
+
+ -
+
+
+ 自定义
+
+
+
+ -
+
+
+ 0
+
+
+ QLayout::SetFixedSize
+
+
+
+ -
+
+
+ background: transparent; border: none
+
+
+ true
+
+
+
+
+ 0
+ 0
+ 658
+ 1036
+
+
+
+ -
+
+
+ *对小组件的显示、隐藏和拖拽操作将在重启后生效。
+
+
+ Qt::AlignCenter
+
+
+ true
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+ QAbstractScrollArea::AdjustToContents
+
+
+ QAbstractItemView::DragDrop
+
+
+ Qt::MoveAction
+
+
+ QListView::LeftToRight
+
+
+ true
+
+
+ QListView::Adjust
+
+
+ QListView::Batched
+
+
+
+ -
+
+
+ 10
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 选择小组件
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ 添加
+
+
+
+ -
+
+
+ 移除
+
+
+
+
+
+ -
+
+
+ 小组件
+
+
+
+ -
+
+
+ 3
+
+ -
+
+
+ 主题
+
+
+ 18
+
+
+
+ -
+
+
+ 打开“主题”文件夹
+
+
+
+ -
+
+
+
+ 0
+ 70
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+ 主题
+
+
+
+ -
+
+
+ 将用于更改小组件的样式(重启后生效)
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 70
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+ 颜色模式
+
+
+
+ -
+
+
+ 将改变应用的浅/深色外观
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 70
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+ 小组件透明度
+
+
+
+ -
+
+
+ 更改小组件在屏幕上显示的透明度
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 100
+ 0
+
+
+
+
+ 200
+ 16777215
+
+
+
+ Qt::Horizontal
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 70
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+ 上课时主题色
+
+
+
+ -
+
+
+ 将用于设置窗口、进度条和提醒弹窗 (为了提醒弹窗可读性,请不要设置过浅的颜色)
+
+
+ true
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 更改颜色
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 70
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+ 下课时主题色
+
+
+
+ -
+
+
+ 将用于设置窗口、进度条和提醒弹窗 (为了提醒弹窗可读性,请不要设置过浅的颜色)
+
+
+ true
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 更改颜色
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 70
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+ 浮窗时间颜色
+
+
+
+ -
+
+
+ 将用于设置浮窗时间颜色 (为了时间的可读性,请不要设置过浅的颜色&过高的透明度)
+
+
+ true
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 更改颜色
+
+
+
+
+
+
+
+
+ -
+
+
+ 3
+
+ -
+
+
+ 倒计时模糊
+
+
+ 18
+
+
+
+ -
+
+
+
+ 0
+ 70
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+ 模糊主组件倒计时
+
+
+
+ -
+
+
+ 将会以“< x 分钟”的形式模糊地显示倒计时
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+ 关
+
+
+ 开
+
+
+ 关
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 70
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+ 模糊浮窗倒计时
+
+
+
+ -
+
+
+ 将会以“< x 分钟”的形式模糊地显示倒计时
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+ 开
+
+
+ 开
+
+
+ 关
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+
+
+
+ -
+
+
+ 3
+
+ -
+
+
+ 天气
+
+
+ 18
+
+
+
+ -
+
+
+ *在 高德天气/腾讯天气 和 小米天气/和风天气 间切换后,需要重新选择城市
+
+
+ true
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+ -
+
+
+
+ 0
+ 70
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+ 选择城市
+
+
+
+ -
+
+
+ 将会用于获得天气数据
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 选择一个城市
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 70
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+ 设置天气源
+
+
+
+ -
+
+
+ 将会影响“天气”小组件的天气数据源
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 70
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+ 天气源 API Key
+
+
+
+ -
+
+
+ 部分天气源可能需要设置 Key 才能正常使用,可在“帮助”页找到各个天气源获得 Key 的方法。
+
+
+ true
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 275
+ 33
+
+
+
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+ -
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 150
+ 0
+
+
+
+ 应用
+
+
+
+
+
+
+
+
+
+ ComboBox
+ QPushButton
+
+
+
+ PushButton
+ QPushButton
+
+
+
+ PrimaryPushButton
+ PushButton
+
+
+
+ SwitchButton
+ QWidget
+
+
+
+ Slider
+ QSlider
+
+
+
+ CardWidget
+ QFrame
+
+ 1
+
+
+ SmoothScrollArea
+ QScrollArea
+
+ 1
+
+
+ CaptionLabel
+ QLabel
+
+
+
+ BodyLabel
+ QLabel
+
+
+
+ StrongBodyLabel
+ QLabel
+
+
+
+ SubtitleLabel
+ QLabel
+
+
+
+ TitleLabel
+ QLabel
+
+
+
+ HyperlinkLabel
+ QPushButton
+
+
+
+ LineEdit
+ QLineEdit
+
+
+
+ ListWidget
+ QListWidget
+
+
+
+
+
+
diff --git a/view/menu/help.ui b/view/menu/help.ui
new file mode 100644
index 0000000..b05a997
--- /dev/null
+++ b/view/menu/help.ui
@@ -0,0 +1,361 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 721
+ 817
+
+
+
+ Form
+
+
+
+ 18
+
+
+ 24
+
+
+ 24
+
+
+ 24
+
+
+ 24
+
+ -
+
+
+ 6
+
+
+ 6
+
+ -
+
+
+ 3
+
+ -
+
+
+ 帮助文档
+
+
+
+ -
+
+
+ 需连接到互联网
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 在浏览器中浏览
+
+
+
+
+
+ -
+
+
+ background: transparent; border: none
+
+
+ true
+
+
+
+
+ 0
+ 0
+ 673
+ 688
+
+
+
+
+ 12
+
+
+ 14
+
+
+ 9
+
+
+ 16
+
+ -
+
+
+ 6
+
+ -
+
+
+ 猜你想问
+
+
+
+ -
+
+
+ 3
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 如何设置课程表?
+
+
+
+ https://www.yuque.com/rinlit/class-widgets_help/swg86btkivirtnrl
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 如何切换主题?
+
+
+
+ https://www.yuque.com/rinlit/class-widgets_help/lg0p91q2mg4yertn
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 软件的时间与铃声不符怎么办?如何设置时差偏移?
+
+
+
+ https://www.yuque.com/rinlit/class-widgets_help/vlk3plggb8edvub4#mHfUX
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 怎么快速设置调休日和换课?
+
+
+
+ https://www.yuque.com/rinlit/class-widgets_help/gc4epffu7g5bf9os
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 如何开发插件?
+
+
+
+ https://www.yuque.com/rinlit/cw-docs-dev
+
+
+
+
+
+
+
+
+ -
+
+
+ 6
+
+ -
+
+
+ 社区
+
+
+
+ -
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 我们的Q群
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ GitHub Discussion
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Discord
+
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 25
+
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+ 5
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+
+
+
+
+ HyperlinkButton
+ PushButton
+
+
+
+ PushButton
+ QPushButton
+
+
+
+ SmoothScrollArea
+ QScrollArea
+
+ 1
+
+
+ CaptionLabel
+ QLabel
+
+
+
+ SubtitleLabel
+ QLabel
+
+
+
+ TitleLabel
+ QLabel
+
+
+
+
+
+
diff --git a/view/menu/plugin_mgr.ui b/view/menu/plugin_mgr.ui
new file mode 100644
index 0000000..f817ff3
--- /dev/null
+++ b/view/menu/plugin_mgr.ui
@@ -0,0 +1,569 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 791
+ 1388
+
+
+
+ Form
+
+
+
+ 18
+
+
+ 24
+
+
+ 24
+
+
+ 24
+
+
+ 24
+
+ -
+
+
+ 插件
+
+
+
+ -
+
+
+ background: transparent; border: none
+
+
+ true
+
+
+
+
+ 0
+ 0
+ 743
+ 1283
+
+
+
+ -
+
+
+ 3
+
+ -
+
+ -
+
+
+ 插件管理器
+
+
+
+ -
+
+
+ *对插件的任意操作将在重启后生效。
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+ true
+
+
+
+ 96
+ 96
+ 96
+
+
+
+
+ 210
+ 210
+ 210
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 70
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+ 在“插件广场”中检查更新
+
+
+
+ -
+
+
+ 将跳转至“插件广场”以检查插件的更新状态
+
+
+ true
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 在“插件广场”检查
+
+
+
+
+
+
+ -
+
+
+ 3
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 150
+
+
+
+ FluentLabelBase {
+ color: black;
+}
+
+HyperlinkLabel {
+ color: #009faa;
+ border: none;
+ background-color: transparent;
+ text-align: left;
+ padding: 0;
+ margin: 0;
+}
+
+HyperlinkLabel[underline=true] {
+ text-decoration: underline;
+}
+
+HyperlinkLabel[underline=false] {
+ text-decoration: none;
+}
+
+HyperlinkLabel:hover {
+ color: #007780;
+}
+
+HyperlinkLabel:pressed {
+ color: #00a7b3;
+}
+FluentLabelBase{color:#7d000000}
+
+
+ 还未添加任何插件
+
+
+ Qt::AlignCenter
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+ 14
+
+
+
+
+
+
+
+ -
+
+
+ 3
+
+ -
+
+
+ 添加插件
+
+
+
+ -
+
+
+
+ 0
+ 70
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+ 管理插件文件夹
+
+
+
+ -
+
+
+ 可在此文件夹添加、删除和修改您所安装的插件
+
+
+ true
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 使用“资源管理器”打开
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 70
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+ 在“插件广场”中寻找
+
+
+
+ -
+
+
+ 将跳转至“插件广场”
+
+
+ true
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 打开“插件广场”
+
+
+
+
+
+
+
+
+ -
+
+
+ 3
+
+ -
+
+
+ 自动化
+
+
+
+ -
+
+
+
+ 0
+ 70
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+ 插件自动化执行延迟
+
+
+
+ -
+
+
+ 当插件执行自动化操作时,需等待的时间(单位:秒)
+
+
+ true
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+
+
+
+
+ PushButton
+ QPushButton
+
+
+
+ CardWidget
+ QFrame
+
+ 1
+
+
+ SmoothScrollArea
+ QScrollArea
+
+ 1
+
+
+ CaptionLabel
+ QLabel
+
+
+
+ StrongBodyLabel
+ QLabel
+
+
+
+ SubtitleLabel
+ QLabel
+
+
+
+ TitleLabel
+ QLabel
+
+
+
+ SpinBox
+ QSpinBox
+
+
+
+
+
+
diff --git a/view/menu/preview.ui b/view/menu/preview.ui
new file mode 100644
index 0000000..873a79e
--- /dev/null
+++ b/view/menu/preview.ui
@@ -0,0 +1,221 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 715
+ 636
+
+
+
+ Form
+
+
+
+ 18
+
+
+ 24
+
+
+ 24
+
+
+ 24
+
+
+ 24
+
+ -
+
+
+ 课程表
+
+
+
+ -
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 预览
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ QTableView {
+ background: transparent;
+ outline: none;
+ border: none;
+ /* font: 13px 'Segoe UI', 'Microsoft YaHei'; */
+ selection-background-color: transparent;
+ alternate-background-color: transparent;
+}
+
+QTableView[isBorderVisible=true] {
+ border: none;
+}
+
+QTableView::item {
+ background: transparent;
+ border: 0px;
+ padding-left: 16px;
+ padding-right: 16px;
+ height: 35px;
+}
+
+
+QTableView::indicator {
+ width: 18px;
+ height: 18px;
+ border-radius: 5px;
+ border: none;
+ background-color: transparent;
+}
+
+
+QHeaderView {
+ background-color: transparent;
+}
+
+QHeaderView::section {
+ background-color: transparent;
+ color: rgb(96, 96, 96);
+ padding-left: 5px;
+ padding-right: 5px;
+ border: 1px solid rgba(0, 0, 0, 15);
+ font: 13px 'Segoe UI', 'Microsoft YaHei', 'PingFang SC';
+}
+
+QHeaderView::section:horizontal {
+ border-left: none;
+ height: 33px;
+}
+
+QTableView[isBorderVisible=true] QHeaderView::section:horizontal {
+ border-top: none;
+}
+
+QHeaderView::section:horizontal:last {
+ border-right: none;
+}
+
+QHeaderView::section:vertical {
+ border-top: none;
+}
+
+QHeaderView::section:checked {
+ background-color: transparent;
+}
+
+QHeaderView::down-arrow {
+ subcontrol-origin: padding;
+ subcontrol-position: center right;
+ margin-right: 6px;
+ image: url(:/qfluentwidgets/images/table_view/Down_black.svg);
+}
+
+QHeaderView::up-arrow {
+ subcontrol-origin: padding;
+ subcontrol-position: center right;
+ margin-right: 6px;
+ image: url(:/qfluentwidgets/images/table_view/Up_black.svg);
+}
+
+QTableCornerButton::section {
+ background-color: transparent;
+ border: 1px solid rgba(0, 0, 0, 15);
+}
+
+QTableCornerButton::section:pressed {
+ background-color: rgba(0, 0, 0, 12);
+}
+
+
+ QAbstractScrollArea::AdjustToContents
+
+
+ QAbstractItemView::NoEditTriggers
+
+
+ QAbstractItemView::SingleSelection
+
+
+ QAbstractItemView::SelectItems
+
+
+ Qt::ElideMiddle
+
+
+ false
+
+
+ 50
+
+
+ 50
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+ ComboBox
+ QPushButton
+
+
+
+ SubtitleLabel
+ QLabel
+
+
+
+ TitleLabel
+ QLabel
+
+
+
+ TableWidget
+ QTableWidget
+
+
+
+
+
+
diff --git a/view/menu/schedule_edit.ui b/view/menu/schedule_edit.ui
new file mode 100644
index 0000000..c07c1ca
--- /dev/null
+++ b/view/menu/schedule_edit.ui
@@ -0,0 +1,463 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 662
+ 627
+
+
+
+ Form
+
+
+
+ 18
+
+
+ 24
+
+
+ 24
+
+
+ 24
+
+
+ 24
+
+ -
+
+
+ 课程表编辑
+
+
+
+ -
+
+ -
+
+
+ 课程表
+
+
+
+
+
+ -
+
+
+ 3
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 选择星期
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 60
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 选择单/双周课表
+
+
+
+ -
+
+
+ 若要启用双周课表,请在“高级选项”中 启用单双周课表和设置开学日期
+
+
+ true
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+ -
+
+
+ 复制单周课表
+
+
+
+
+
+
+
+
+ -
+
+
+ 12
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Qt::IgnoreAction
+
+
+ true
+
+
+
+ -
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 快速添加课程
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ false
+
+
+ false
+
+
+ false
+
+
+ QAbstractItemView::NoDragDrop
+
+
+ Qt::IgnoreAction
+
+
+ false
+
+
+ QListView::Static
+
+
+ true
+
+
+ QListView::Adjust
+
+
+ QListView::SinglePass
+
+
+ QListView::IconMode
+
+
+
+ -
+
+
+ 下一天
+
+
+
+
+
+
+
+
+ -
+
+
+ 5
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 课程/活动
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 自定义课程
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ false
+
+
+
+ -
+
+
+ -
+
+
+
+
+ -
+
+
+ 2
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 150
+ 0
+
+
+
+ 保存
+
+
+
+
+
+
+
+
+
+ ComboBox
+ QPushButton
+
+
+
+ PushButton
+ QPushButton
+
+
+
+ PrimaryPushButton
+ PushButton
+
+
+
+ ToolButton
+ QToolButton
+
+
+
+ CardWidget
+ QFrame
+
+ 1
+
+
+ ElevatedCardWidget
+ SimpleCardWidget
+
+ 1
+
+
+ SimpleCardWidget
+ CardWidget
+
+ 1
+
+
+ CaptionLabel
+ QLabel
+
+
+
+ BodyLabel
+ QLabel
+
+
+
+ StrongBodyLabel
+ QLabel
+
+
+
+ SubtitleLabel
+ QLabel
+
+
+
+ TitleLabel
+ QLabel
+
+
+
+ LineEdit
+ QLineEdit
+
+
+
+ ListWidget
+ QListWidget
+
+
+
+
+
+
diff --git a/view/menu/sound.ui b/view/menu/sound.ui
new file mode 100644
index 0000000..5eb47c8
--- /dev/null
+++ b/view/menu/sound.ui
@@ -0,0 +1,837 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 745
+ 773
+
+
+
+
+ 0
+ 0
+
+
+
+ Form
+
+
+
+ 18
+
+
+ 24
+
+
+ 24
+
+
+ 24
+
+
+ 0
+
+ -
+
+
+ 上下课提醒
+
+
+
+ -
+
+
+ background: transparent; border: none
+
+
+ true
+
+
+
+
+ 0
+ 0
+ 697
+ 823
+
+
+
+ -
+
+
+ 3
+
+ -
+
+
+
+ 0
+ 70
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 启用上课提醒
+
+
+
+ -
+
+
+ 启用后将在上课时弹窗且发出提示音提醒
+
+
+ true
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+ 启用
+
+
+ 禁用
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 70
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 启用下课提醒
+
+
+
+ -
+
+
+ 启用后将在下课时弹窗且发出提示音提醒
+
+
+ true
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+ 启用
+
+
+ 禁用
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 70
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 启用放学提醒
+
+
+
+ -
+
+
+ 启用后将在放学时弹窗且发出提示音提醒
+
+
+ true
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+ 启用
+
+
+ 禁用
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 70
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 启用预备铃
+
+
+
+ -
+
+
+ 启用后将在预备铃时弹窗且发出提示音提醒
+
+
+ true
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+ 启用
+
+
+ 禁用
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 70
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 预备铃
+
+
+
+ -
+
+
+ 在正式上课前发出预备铃(输入提前的分钟数,若为0则禁用)
+
+
+ true
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+
+ 130
+ 33
+
+
+
+ 9
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 70
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+ 音量
+
+
+
+ -
+
+
+ 将调整提醒声音的音量大小
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 200
+ 0
+
+
+
+ Qt::Horizontal
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 70
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 是否置顶
+
+
+
+ -
+
+
+ 启用后将在提醒时置顶弹窗
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+ 启用
+
+
+ 禁用
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 70
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 启用强调特效
+
+
+
+ -
+
+
+ 启用后弹出提醒弹窗同时会有水波强调及模糊淡入淡出效果
+*可能影响性能
+
+
+ true
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+ 启用
+
+
+ 禁用
+
+
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 25
+
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+ 5
+
+
+
+ -
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 预览
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+
+
+
+
+ PushButton
+ QPushButton
+
+
+
+ PrimaryPushButton
+ PushButton
+
+
+
+ PrimaryDropDownPushButton
+ PrimaryPushButton
+
+
+
+ SwitchButton
+ QWidget
+
+
+
+ Slider
+ QSlider
+
+
+
+ CardWidget
+ QFrame
+
+ 1
+
+
+ SmoothScrollArea
+ QScrollArea
+
+ 1
+
+
+ CaptionLabel
+ QLabel
+
+
+
+ StrongBodyLabel
+ QLabel
+
+
+
+ TitleLabel
+ QLabel
+
+
+
+ SpinBox
+ QSpinBox
+
+
+
+
+
+
diff --git a/view/menu/subject.ui b/view/menu/subject.ui
new file mode 100644
index 0000000..9337725
--- /dev/null
+++ b/view/menu/subject.ui
@@ -0,0 +1,121 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 662
+ 627
+
+
+
+ Form
+
+
+
+ 18
+
+
+ 24
+
+
+ 24
+
+
+ 24
+
+
+ 24
+
+ -
+
+
+ 学科编辑
+
+
+
+ -
+
+
+ 12
+
+ -
+
+
+ QAbstractScrollArea::AdjustToContents
+
+
+ Qt::SolidLine
+
+
+
+
+
+ -
+
+
+ 2
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 150
+ 0
+
+
+
+ 保存
+
+
+
+
+
+
+
+
+
+ PushButton
+ QPushButton
+
+
+
+ PrimaryPushButton
+ PushButton
+
+
+
+ TitleLabel
+ QLabel
+
+
+
+ TableWidget
+ QTableWidget
+
+
+
+
+
+
diff --git a/view/menu/timeline_edit.ui b/view/menu/timeline_edit.ui
new file mode 100644
index 0000000..0c4cf22
--- /dev/null
+++ b/view/menu/timeline_edit.ui
@@ -0,0 +1,599 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 733
+ 658
+
+
+
+ Form
+
+
+
+ 18
+
+
+ 24
+
+
+ 24
+
+
+ 24
+
+
+ 24
+
+ -
+
+
+ 时间线编辑
+
+
+
+ -
+
+
+ 12
+
+ -
+
+ -
+
+
+
+ 0
+ 33
+
+
+
+ 节点
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ FluentLabelBase {
+ color: black;
+}
+
+HyperlinkLabel {
+ color: #009faa;
+ border: none;
+ background-color: transparent;
+ text-align: left;
+ padding: 0;
+ margin: 0;
+}
+
+HyperlinkLabel[underline=true] {
+ text-decoration: underline;
+}
+
+HyperlinkLabel[underline=false] {
+ text-decoration: none;
+}
+
+HyperlinkLabel:hover {
+ color: #007780;
+}
+
+HyperlinkLabel:pressed {
+ color: #00a7b3;
+}
+FluentLabelBase{color:#7d000000}
+
+
+ 还未添加任何节点
+
+
+ Qt::AlignBottom|Qt::AlignHCenter
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+ 14
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+ QAbstractItemView::InternalMove
+
+
+ Qt::MoveAction
+
+
+ true
+
+
+
+ -
+
+
+ 5
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 节点名称
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 类型
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+ -
+
+
+ 5
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 开始时间
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 150
+ 33
+
+
+
+ QDateTimeEdit::HourSection
+
+
+ h:mm
+
+
+
+ 7
+ 0
+ 0
+
+
+
+
+ -
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+ -
+
+ -
+
+ -
+
+
+ 时间线
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ FluentLabelBase {
+ color: black;
+}
+
+HyperlinkLabel {
+ color: #009faa;
+ border: none;
+ background-color: transparent;
+ text-align: left;
+ padding: 0;
+ margin: 0;
+}
+
+HyperlinkLabel[underline=true] {
+ text-decoration: underline;
+}
+
+HyperlinkLabel[underline=false] {
+ text-decoration: none;
+}
+
+HyperlinkLabel:hover {
+ color: #007780;
+}
+
+HyperlinkLabel:pressed {
+ color: #00a7b3;
+}
+FluentLabelBase{color:#7d000000}
+
+
+ 还未添加任何时间线
+
+
+ Qt::AlignBottom|Qt::AlignHCenter
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+ 14
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+ QAbstractItemView::InternalMove
+
+
+ Qt::MoveAction
+
+
+ true
+
+
+
+ -
+
+
+ 5
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 活动类型
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 时段
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+ -
+
+
+ 5
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 时长(分钟)
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 999
+
+
+ 5
+
+
+ 40
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+ 2
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 150
+ 0
+
+
+
+ 保存
+
+
+
+
+
+
+
+
+
+ ComboBox
+ QPushButton
+
+
+
+ EditableComboBox
+ LineEdit
+
+
+
+ PushButton
+ QPushButton
+
+
+
+ PrimaryPushButton
+ PushButton
+
+
+
+ ToolButton
+ QToolButton
+
+
+
+ VerticalSeparator
+ QWidget
+
+
+
+ CaptionLabel
+ QLabel
+
+
+
+ BodyLabel
+ QLabel
+
+
+
+ SubtitleLabel
+ QLabel
+
+
+
+ TitleLabel
+ QLabel
+
+
+
+ LineEdit
+ QLineEdit
+
+
+
+ TimeEdit
+ QTimeEdit
+
+
+
+ SpinBox
+ QSpinBox
+
+
+
+ ListWidget
+ QListWidget
+
+
+
+
+
+
diff --git a/view/pp/home.ui b/view/pp/home.ui
new file mode 100644
index 0000000..876aa35
--- /dev/null
+++ b/view/pp/home.ui
@@ -0,0 +1,313 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 688
+ 777
+
+
+
+
+ 0
+ 0
+
+
+
+ Form
+
+
+
+ 18
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+ background: transparent; border: none
+
+
+ true
+
+
+ Qt::AlignCenter
+
+
+
+
+ 0
+ 0
+ 688
+ 818
+
+
+
+
+ 12
+
+
+ 24
+
+
+ 24
+
+
+ 24
+
+ -
+
+ -
+
+
+ 今天
+
+
+
+ -
+
+
+
+ HarmonyOS Sans SC
+ 18
+ 75
+ true
+
+
+
+ 11月45日 周日
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+ 93
+ 93
+ 93
+
+
+
+
+ 209
+ 209
+ 209
+
+
+
+
+
+
+ -
+
+
+
+ 480
+ 450
+
+
+
+
+ 16777215
+ 450
+
+
+
+
+ -
+
+ -
+
+
+
+
+ -
+
+
+ 推荐插件
+
+
+
+ -
+
+
+ -
+
+
+ 0
+
+
+ 12
+
+
+ 12
+
+ -
+
+
+
+ 45
+ 45
+
+
+
+
+ 45
+ 45
+
+
+
+
+
+
+ -
+
+
+ 这就到底了吗……(っ °Д °;)っ
+
+
+ Qt::AlignCenter
+
+
+ true
+
+
+
+ 125
+ 125
+ 125
+
+
+
+
+ 211
+ 211
+ 211
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+ -
+
+
+
+ 0
+ 25
+
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+ 5
+
+
+
+
+
+
+
+
+
+
+
+ SmoothScrollArea
+ QScrollArea
+
+ 1
+
+
+ BodyLabel
+ QLabel
+
+
+
+ SubtitleLabel
+ QLabel
+
+
+
+ TitleLabel
+ QLabel
+
+
+
+ IndeterminateProgressRing
+ QProgressBar
+
+
+
+ HorizontalFlipView
+ QListWidget
+
+
+
+ HorizontalPipsPager
+ QListWidget
+
+
+
+
+
+
diff --git a/view/pp/latests.ui b/view/pp/latests.ui
new file mode 100644
index 0000000..0ee0883
--- /dev/null
+++ b/view/pp/latests.ui
@@ -0,0 +1,173 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 688
+ 713
+
+
+
+
+ 0
+ 0
+
+
+
+ Form
+
+
+
+ 18
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+ background: transparent; border: none
+
+
+ true
+
+
+
+
+ 0
+ 0
+ 688
+ 713
+
+
+
+
+ 24
+
+
+ 24
+
+
+ 24
+
+ -
+
+ -
+
+
+ 分类
+
+
+
+
+
+ -
+
+
+ 所有插件
+
+
+
+ -
+
+
+ -
+
+
+ Coming Soon~
+
+
+ Qt::AlignCenter
+
+
+
+ 125
+ 125
+ 125
+
+
+
+
+ 211
+ 211
+ 211
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+ -
+
+
+
+ 0
+ 25
+
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+ 5
+
+
+
+
+
+
+
+
+
+
+
+ SmoothScrollArea
+ QScrollArea
+
+ 1
+
+
+ BodyLabel
+ QLabel
+
+
+
+ SubtitleLabel
+ QLabel
+
+
+
+ TitleLabel
+ QLabel
+
+
+
+
+
+
diff --git a/view/pp/plugin_detail.ui b/view/pp/plugin_detail.ui
new file mode 100644
index 0000000..164e810
--- /dev/null
+++ b/view/pp/plugin_detail.ui
@@ -0,0 +1,405 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 688
+ 713
+
+
+
+
+ 0
+ 0
+
+
+
+ Form
+
+
+
+ 18
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+ background: transparent; border: none
+
+
+ true
+
+
+
+
+ 0
+ 0
+ 688
+ 713
+
+
+
+
+ 12
+
+
+ 24
+
+
+ 24
+
+
+ 24
+
+
+ 24
+
+ -
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+ 24
+
+
+ 12
+
+
+ 12
+
+ -
+
+
+
+ 128
+ 128
+
+
+
+
+ 128
+ 128
+
+
+
+
+ -
+
+
+ 0
+
+ -
+
+
+ 12
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ PluginName
+
+
+ false
+
+
+
+ -
+
+
+ 1.1.0
+
+
+
+ 153
+ 153
+ 153
+
+
+
+
+ 153
+ 153
+ 153
+
+
+
+
+
+
+ -
+
+
+ 12
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Author
+
+
+
+ -
+
+
+ |
+
+
+
+ 153
+ 153
+ 153
+
+
+
+
+ 153
+ 153
+ 153
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Tag
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+ QSizePolicy::Preferred
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+ -
+
+
+ 18
+
+
+ 18
+
+ -
+
+
+ 24
+
+ -
+
+
+
+ 425
+ 16777215
+
+
+
+ Plugin Description Plugin Description Plugin Description Plugin Description Plugin Description Plugin Description Plugin Description Plugin Description Plugin Description Plugin Description Plugin Description Plugin Description Plugin Description Plugin Description
+
+
+ true
+
+
+
+ -
+
+ -
+
+
+ Install
+
+
+
+ . .
+
+
+
+ -
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ SplitPushButton
+ QWidget
+
+
+
+ PrimarySplitPushButton
+ SplitPushButton
+
+
+
+ ToolButton
+ QToolButton
+
+
+
+ TransparentToolButton
+ ToolButton
+
+
+
+ SmoothScrollArea
+ QScrollArea
+
+ 1
+
+
+ CaptionLabel
+ QLabel
+
+
+
+ BodyLabel
+ QLabel
+
+
+
+ TitleLabel
+ QLabel
+
+
+
+ HyperlinkLabel
+ QPushButton
+
+
+
+ ImageLabel
+ QLabel
+
+
+
+
+
+
diff --git a/view/pp/search.ui b/view/pp/search.ui
new file mode 100644
index 0000000..79b6caf
--- /dev/null
+++ b/view/pp/search.ui
@@ -0,0 +1,171 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 688
+ 713
+
+
+
+
+ 0
+ 0
+
+
+
+ Form
+
+
+
+ 18
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+ background: transparent; border: none
+
+
+ true
+
+
+ Qt::AlignCenter
+
+
+
+
+ 0
+ 0
+ 688
+ 713
+
+
+
+
+ 12
+
+
+ 24
+
+
+ 24
+
+
+ 24
+
+ -
+
+
+ 搜索你希望查找的插件、Tag等
+
+
+
+ -
+
+
+ 3
+
+ -
+
+
+ 探索更多
+
+
+
+ -
+
+
+ 12
+
+
+ 6
+
+
+ 12
+
+
+
+
+
+ -
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+ -
+
+
+
+ 0
+ 25
+
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+ 5
+
+
+
+
+
+
+
+
+
+
+
+ SmoothScrollArea
+ QScrollArea
+
+ 1
+
+
+ StrongBodyLabel
+ QLabel
+
+
+
+ LineEdit
+ QLineEdit
+
+
+
+ SearchLineEdit
+ LineEdit
+
+
+
+
+
+
diff --git a/view/pp/settings.ui b/view/pp/settings.ui
new file mode 100644
index 0000000..bb87547
--- /dev/null
+++ b/view/pp/settings.ui
@@ -0,0 +1,326 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 688
+ 806
+
+
+
+
+ 0
+ 0
+
+
+
+ Form
+
+
+
+ 18
+
+
+ 24
+
+
+ 24
+
+
+ 24
+
+
+ 0
+
+ -
+
+
+ 设置
+
+
+
+ -
+
+
+ background: transparent; border: none
+
+
+ true
+
+
+
+
+ 0
+ 0
+ 640
+ 725
+
+
+
+ -
+
+
+ 3
+
+ -
+
+
+ 插件
+
+
+
+ -
+
+
+
+ 0
+ 70
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+ 下载后自动启用插件
+
+
+
+ -
+
+
+ 在下载插件后,将为您自动启用插件以便您重启可以立即使用。
+但请确信您在“插件广场”中需要的插件是安全的。
+
+
+ true
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 启用
+
+
+ 禁用
+
+
+
+
+
+
+
+
+ -
+
+
+ 3
+
+ -
+
+
+ 网络
+
+
+
+ -
+
+
+
+ 0
+ 70
+
+
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+ -
+
+
+ 0
+
+ -
+
+
+ 选择镜像源
+
+
+
+ -
+
+
+ 若需要在中国大陆正常使用“插件广场”,最好为其设置一个镜像源。
+
+
+ true
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+ -
+
+
+
+ 0
+ 25
+
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+ 5
+
+
+
+
+
+
+
+
+
+
+
+ ComboBox
+ QPushButton
+
+
+
+ SwitchButton
+ QWidget
+
+
+
+ CardWidget
+ QFrame
+
+ 1
+
+
+ SmoothScrollArea
+ QScrollArea
+
+ 1
+
+
+ CaptionLabel
+ QLabel
+
+
+
+ StrongBodyLabel
+ QLabel
+
+
+
+ SubtitleLabel
+ QLabel
+
+
+
+ TitleLabel
+ QLabel
+
+
+
+
+
+
diff --git a/view/widget-toast-bar.ui b/view/widget-toast-bar.ui
new file mode 100644
index 0000000..fd79e12
--- /dev/null
+++ b/view/widget-toast-bar.ui
@@ -0,0 +1,201 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 646
+ 125
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 65536
+
+
+
+ Form
+
+
+
+ 8
+
+
+ 8
+
+
+ 8
+
+
+ 22
+
+ -
+
+
+ border: none;
+color: rgba(255, 255, 255, 255);
+font-weight: bold;
+border-radius: 8px;
+background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:1, stop:0 rgba(255, 200, 150, 255), stop:1 rgba(217, 147, 107, 255));
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+ 52
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ 23
+
+ -
+
+
+ background: transparent
+
+
+
+
+
+ ../img/attend_class.svg
+
+
+
+ -
+
+
+
+ Microsoft YaHei UI
+ 22
+ 75
+ true
+
+
+
+ color: rgb(255, 255, 255);
+background: transparent
+
+
+ 上课
+
+
+
+
+
+ -
+
+
+ 16
+
+ -
+
+
+
+ Microsoft YaHei UI
+ 14
+ 75
+ false
+ true
+
+
+
+ background: transparent;
+font-weight: bold;
+color: rgba(255, 255, 255, 200);
+
+
+ 当前课程
+
+
+
+ -
+
+
+
+ Microsoft YaHei UI
+ 20
+ 75
+ true
+
+
+
+ color: rgb(255, 255, 255);
+background: transparent
+
+
+ 英语
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
+
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/weather_db.py b/weather_db.py
new file mode 100644
index 0000000..badb665
--- /dev/null
+++ b/weather_db.py
@@ -0,0 +1,239 @@
+import datetime
+import sqlite3
+import json
+from loguru import logger
+
+from conf import base_directory
+from file import config_center
+
+path = f'{base_directory}/config/data/xiaomi_weather.db'
+api_config = json.load(open(f'{base_directory}/config/data/weather_api.json', encoding='utf-8'))
+
+
+def update_path():
+ global path
+ path = (f"{base_directory}/config/"
+ f"data/{api_config['weather_api_parameters'][config_center.read_conf('Weather', 'api')]['database']}")
+
+
+def search_by_name(search_term):
+ update_path()
+ conn = sqlite3.connect(path)
+ cursor = conn.cursor()
+
+ cursor.execute('SELECT * FROM citys WHERE name LIKE ?', ('%' + search_term + '%',)) # 模糊查询
+ cities_results = cursor.fetchall()
+ conn.close()
+ result_list = []
+ for city in cities_results:
+ result_list.append(city[2])
+ # 返回两个表的搜索结果
+ return result_list
+
+
+def search_code_by_name(search_term):
+ if search_term == ('', ''):
+ return 101010100
+ update_path()
+ conn = sqlite3.connect(path)
+ cursor = conn.cursor()
+
+ logger.info(f"Searching for city: {search_term}")
+ search_term = (search_term[0].replace('市',''), search_term[1].replace('区',''))
+
+ cursor.execute('SELECT * FROM citys WHERE name = ?', (f"{search_term[0]}.{search_term[1]}",))
+ exact_results = cursor.fetchall()
+
+ if not exact_results:
+ search_term = search_term[0]
+ cursor.execute('SELECT * FROM citys WHERE name LIKE ?', ('%' + f"{search_term}" + '%',))
+ cities_results = cursor.fetchall()
+ else:
+ cities_results = exact_results
+
+ conn.close()
+
+ if cities_results:
+ # 多结果优先完全匹配,否则返回第一个
+ for city in cities_results:
+ if city[2] == search_term or city[2] == search_term + '市' or city[2] + '市' == search_term:
+ logger.debug(f"找到城市: {city[2]},代码: {city[3]}")
+ return city[3]
+ result = cities_results[0][3]
+ logger.debug(f"模糊找到城市: {cities_results[0][2]},代码: {result}")
+ else:
+ result = "101010100" # 默认城市代码
+ logger.warning(f'未找到城市: {search_term},使用默认城市代码')
+
+ return result
+
+
+def search_by_num(search_term):
+ update_path()
+ conn = sqlite3.connect(path)
+ cursor = conn.cursor()
+
+ cursor.execute('SELECT * FROM citys WHERE city_num LIKE ?', ('%' + search_term + '%',)) # 模糊查询
+ cities_results = cursor.fetchall()
+
+ conn.close()
+
+ if cities_results:
+ result = cities_results[0][2]
+ else:
+ result = '北京' # 默认城市
+ # 返回两个表的搜索结果
+ return result
+
+
+def get_weather_by_code(code): # 用代码获取天气描述
+ weather_status = json.load(
+ open(f"{base_directory}/config/data/{config_center.read_conf('Weather', 'api')}_status.json", encoding="utf-8"))
+ for weather in weather_status['weatherinfo']:
+ if str(weather['code']) == code:
+ return weather['wea']
+ return '未知'
+
+
+def get_weather_icon_by_code(code): # 用代码获取天气图标
+ weather_status = json.load(
+ open(f"{base_directory}/config/data/{config_center.read_conf('Weather', 'api')}_status.json",
+ encoding="utf-8")
+ )
+ weather_code = None
+ current_time = datetime.datetime.now()
+ # 遍历获取天气代码
+ for weather in weather_status['weatherinfo']:
+ if str(weather['code']) == code:
+ original_code = weather.get('original_code')
+ if original_code is not None:
+ weather_code = str(weather['original_code'])
+ else:
+ weather_code = str(weather['code'])
+ break
+ if not weather_code:
+ logger.error(f'未找到天气代码 {code}')
+ return f'{base_directory}/img/weather/99.svg'
+ # 根据天气和时间获取天气图标
+ if weather_code in ('0', '1', '3', '13'): # 晴、多云、阵雨、阵雪
+ if current_time.hour < 6 or current_time.hour >= 18: # 如果是夜间
+ return f'{base_directory}/img/weather/{weather_code}d.svg'
+ return f'{base_directory}/img/weather/{weather_code}.svg'
+
+
+def get_weather_stylesheet(code): # 天气背景样式
+ current_time = datetime.datetime.now()
+ weather_status = json.load(
+ open(f"{base_directory}/config/data/{config_center.read_conf('Weather', 'api')}_status.json", encoding="utf-8"))
+ weather_code = '99'
+ for weather in weather_status['weatherinfo']:
+ if str(weather['code']) == code:
+ original_code = weather.get('original_code')
+ if original_code is not None:
+ weather_code = str(weather['original_code'])
+ else:
+ weather_code = str(weather['code'])
+ break
+ if weather_code in ('0', '1', '3', '99', '900'): # 晴、多云、阵雨、未知
+ if 6 <= current_time.hour < 18: # 如果是日间
+ # return 'spread:pad, x1:0, y1:0, x2:1, y2:1, stop:0 rgba(40, 60, 110, 255), stop:1 rgba(75, 175, 245, 255)'
+ return 'img/weather/bkg/day.png'
+ else: # 如果是夜间
+ return 'img/weather/bkg/night.png'
+ # return 'spread:pad, x1:0, y1:0, x2:1, y2:1, stop:0 rgba(20, 60, 90, 255), stop:1 rgba(10, 20, 29, 255)'
+ return 'img/weather/bkg/rain.png'
+
+
+def get_weather_url():
+ if config_center.read_conf('Weather', 'api') in api_config['weather_api_list']:
+ return api_config['weather_api'][config_center.read_conf('Weather', 'api')]
+ else:
+ return api_config['weather_api']['xiaomi_weather']
+
+
+def get_weather_alert_url():
+ if not api_config['weather_api_parameters'][config_center.read_conf('Weather', 'api')]['alerts']:
+ return 'NotSupported'
+ if config_center.read_conf('Weather', 'api') in api_config['weather_api_list']:
+ return api_config['weather_api_parameters'][config_center.read_conf('Weather', 'api')]['alerts']['url']
+ else:
+ return api_config['weather_api_parameters']['xiaomi_weather']['alerts']['url']
+
+
+def get_weather_code_by_description(value):
+ weather_status = json.load(
+ open(f"{base_directory}/config/data/{config_center.read_conf('Weather', 'api')}_status.json", encoding="utf-8"))
+ for weather in weather_status['weatherinfo']:
+ if str(weather['wea']) == value:
+ return str(weather['code'])
+ return '99'
+
+
+def get_alert_image(alert_type):
+ alerts_list = api_config['weather_api_parameters'][config_center.read_conf('Weather', 'api')]['alerts']['types']
+ return f'{base_directory}/img/weather/alerts/{alerts_list[alert_type]}'
+
+
+def is_supported_alert():
+ if not api_config['weather_api_parameters'][config_center.read_conf('Weather', 'api')]['alerts']:
+ return False
+ return True
+
+
+def get_weather_data(key='temp', weather_data=None): # 获取天气数据
+ if weather_data is None:
+ logger.error('weather_data is None!')
+ return None
+ '''
+ 根据key值获取weather_data中的对应值
+ key值可以为:temp、icon、alert_title
+ '''
+ # 各个天气api的可访问值
+ api_parameters = api_config['weather_api_parameters'][config_center.read_conf('Weather', 'api')]
+ if key == 'alert':
+ parameter = api_parameters['alerts']['type'].split('.')
+ elif key == 'alert_title':
+ if 'alerts' not in api_parameters or 'title' not in api_parameters['alerts']:
+ return None
+ parameter = api_parameters['alerts']['title'].split('.')
+ else:
+ parameter = api_parameters[key].split('.')
+ # 遍历获取值
+ value = weather_data
+ if config_center.read_conf('Weather', 'api') == 'amap_weather':
+ value = weather_data['lives'][0][api_parameters[key]]
+ elif config_center.read_conf('Weather', 'api') == 'qq_weather':
+ value = str(weather_data['result']['realtime'][0]['infos'][api_parameters[key]])
+ else:
+ for parameter in parameter:
+ if not value:
+ logger.warning(f'天气信息值{key}为空')
+ return None
+ if parameter == '0':
+ value = value[0]
+ continue
+ if parameter in value:
+ value = value[parameter]
+ else:
+ logger.error(f'获取天气参数失败,{parameter}不存在于{config_center.read_conf("Weather", "api")}中')
+ return '错误'
+ if key == 'temp':
+ value += '°'
+ elif key == 'icon': # 修复此代码影响其他天气源的问题
+ if api_parameters['return_desc']: # 如果此api返回的是天气描述而不是代码
+ value = get_weather_code_by_description(value)
+ return value
+
+
+if __name__ == '__main__':
+ # 测试代码
+ try:
+ num_results = search_by_num('101310101') # [2]城市名称
+ print(num_results)
+ cities_results_ = search_by_name('上海') # [3]城市代码
+ print(cities_results_)
+ cities_results_ = search_code_by_name('上海','') # [3]城市代码
+ print(cities_results_)
+ get_weather_by_code(3)
+ except Exception as e:
+ print(e)