diff --git a/app/Language/modules/extraction_settings.py b/app/Language/modules/extraction_settings.py index c52a4b58..1770e86e 100644 --- a/app/Language/modules/extraction_settings.py +++ b/app/Language/modules/extraction_settings.py @@ -205,6 +205,93 @@ }, } +# 时间设置语言配置 +time_settings = { + "ZH_CN": { + "title": {"name": "时间设置", "description": "设置课间禁用和课程表导入"}, + "class_break_settings": { + "name": "课间禁用设置", + "description": "设置课间禁用功能", + }, + "cses_import_settings": { + "name": "CSES课程表导入", + "description": "从CSES格式文件导入课程表", + }, + "class_break_function": { + "name": "课间禁用功能", + "description": "开启后,在下课时间段内抽取需要安全验证", + }, + "cses_import": { + "name": "课程表导入", + "description": "从CSES格式文件导入上课时间段,用于课间禁用功能", + }, + "import_from_file": {"name": "从文件导入"}, + "importing": {"name": "导入中..."}, + "view_template": {"name": "查看模板"}, + "no_schedule_imported": {"name": "未导入课程表"}, + "schedule_imported": {"name": "已导入 {} 个非上课时间段"}, + "copy_to_clipboard": {"name": "复制到剪贴板"}, + "save_as_file": {"name": "保存为文件"}, + "close": {"name": "关闭"}, + "copy_success": {"name": "复制成功"}, + "template_copied": {"name": "模板已复制到剪贴板"}, + "save_success": {"name": "保存成功"}, + "template_saved": {"name": "模板已保存到: {}"}, + "import_success": {"name": "导入成功"}, + "import_failed": {"name": "导入失败"}, + "import_error": {"name": "导入过程中发生错误: {}"}, + "template_title": {"name": "CSES课程表模板"}, + "select_cses_file": {"name": "选择CSES课程表文件"}, + "yaml_files": {"name": "YAML文件 (*.yaml *.yml)"}, + "all_files": {"name": "所有文件 (*.*)"}, + "save_template": {"name": "保存CSES模板"}, + "cses_template": {"name": "cses_template.yaml"}, + }, + "EN_US": { + "title": { + "name": "Time Settings", + "description": "Set class break restrictions and schedule import", + }, + "class_break_settings": { + "name": "Class Break Settings", + "description": "Configure class break restrictions", + }, + "cses_import_settings": { + "name": "CSES Schedule Import", + "description": "Import schedule from CSES format files", + }, + "class_break_function": { + "name": "Class Break Restriction", + "description": "When enabled, drawing during break times requires safety verification", + }, + "cses_import": { + "name": "Schedule Import", + "description": "Import class time slots from CSES format files for break time functionality", + }, + "import_from_file": {"name": "Import from File"}, + "importing": {"name": "Importing..."}, + "view_template": {"name": "View Template"}, + "no_schedule_imported": {"name": "No schedule imported"}, + "schedule_imported": {"name": "Imported {} non-class time periods"}, + "copy_to_clipboard": {"name": "Copy to Clipboard"}, + "save_as_file": {"name": "Save as File"}, + "close": {"name": "Close"}, + "copy_success": {"name": "Copy Successful"}, + "template_copied": {"name": "Template copied to clipboard"}, + "save_success": {"name": "Save Successful"}, + "template_saved": {"name": "Template saved to: {}"}, + "import_success": {"name": "Import Successful"}, + "import_failed": {"name": "Import Failed"}, + "import_error": {"name": "Error during import: {}"}, + "template_title": {"name": "CSES Schedule Template"}, + "select_cses_file": {"name": "Select CSES Schedule File"}, + "yaml_files": {"name": "YAML files (*.yaml *.yml)"}, + "all_files": {"name": "All files (*.*)"}, + "save_template": {"name": "Save CSES Template"}, + "cses_template": {"name": "cses_template.yaml"}, + }, +} + # 闪抽设置 quick_draw_settings = { "ZH_CN": { diff --git a/app/common/extraction/cses_parser.py b/app/common/extraction/cses_parser.py new file mode 100644 index 00000000..d5241418 --- /dev/null +++ b/app/common/extraction/cses_parser.py @@ -0,0 +1,252 @@ +# ================================================== +# CSES (Course Schedule Exchange Schema) 解析器 +# ================================================== +import yaml +import json +from datetime import datetime, time +from typing import Dict, List, Optional +from loguru import logger + + +class CSESParser: + """CSES格式课程表解析器""" + + def __init__(self): + self.schedule_data = None + + def load_from_file(self, file_path: str) -> bool: + """从文件加载CSES数据 + + Args: + file_path: CSES文件路径 + + Returns: + bool: 加载成功返回True,否则返回False + """ + try: + with open(file_path, "r", encoding="utf-8") as f: + self.schedule_data = yaml.safe_load(f) + return self._validate_schedule() + except Exception as e: + logger.error(f"加载CSES文件失败: {e}") + return False + + def load_from_content(self, content: str) -> bool: + """从字符串内容加载CSES数据 + + Args: + content: YAML格式的CSES内容 + + Returns: + bool: 加载成功返回True,否则返回False + """ + try: + self.schedule_data = yaml.safe_load(content) + return self._validate_schedule() + except Exception as e: + logger.error(f"解析CSES内容失败: {e}") + return False + + def _validate_schedule(self) -> bool: + """验证课程表数据的有效性 + + Returns: + bool: 数据有效返回True,否则返回False + """ + if not self.schedule_data: + logger.error("课程表数据为空") + return False + + # 基本结构验证 + if "schedule" not in self.schedule_data: + logger.error("缺少'schedule'字段") + return False + + schedule = self.schedule_data["schedule"] + if not isinstance(schedule, dict): + logger.error("'schedule'字段必须是字典类型") + return False + + # 验证时间段配置 + if "timeslots" not in schedule: + logger.error("缺少'timeslots'字段") + return False + + timeslots = schedule["timeslots"] + if not isinstance(timeslots, list): + logger.error("'timeslots'字段必须是列表类型") + return False + + # 验证每个时间段 + for i, timeslot in enumerate(timeslots): + if not self._validate_timeslot(timeslot, i): + return False + + return True + + def _validate_timeslot(self, timeslot: dict, index: int) -> bool: + """验证单个时间段的配置 + + Args: + timeslot: 时间段配置字典 + index: 时间段索引 + + Returns: + bool: 有效返回True,否则返回False + """ + required_fields = ["name", "start_time", "end_time"] + for field in required_fields: + if field not in timeslot: + logger.error(f"时间段{index}缺少'{field}'字段") + return False + + # 验证时间格式 + try: + start_time = self._parse_time(timeslot["start_time"]) + end_time = self._parse_time(timeslot["end_time"]) + + if start_time >= end_time: + logger.error(f"时间段{index}的开始时间必须早于结束时间") + return False + + except ValueError as e: + logger.error(f"时间段{index}时间格式错误: {e}") + return False + + return True + + def _parse_time(self, time_str: str) -> time: + """解析时间字符串 + + Args: + time_str: 时间字符串 (HH:MM 或 HH:MM:SS) + + Returns: + time: 时间对象 + + Raises: + ValueError: 时间格式错误 + """ + try: + if ":" in time_str: + parts = time_str.split(":") + if len(parts) == 2: + return time(int(parts[0]), int(parts[1])) + elif len(parts) == 3: + return time(int(parts[0]), int(parts[1]), int(parts[2])) + raise ValueError(f"无效的时间格式: {time_str}") + except (ValueError, IndexError): + raise ValueError(f"无法解析时间: {time_str}") + + def get_non_class_times(self) -> Dict[str, str]: + """获取非上课时间段配置 + + 将CSES格式的时间段转换为SecRandom使用的非上课时间段格式 + + Returns: + Dict[str, str]: 非上课时间段字典,格式为 {"name": "HH:MM:SS-HH:MM:SS"} + """ + if not self.schedule_data: + return {} + + non_class_times = {} + schedule = self.schedule_data["schedule"] + timeslots = schedule["timeslots"] + + # 按开始时间排序 + sorted_timeslots = sorted(timeslots, key=lambda x: x["start_time"]) + + # 构建上课时间段列表 + class_periods = [] + for timeslot in sorted_timeslots: + start_time = self._format_time_for_secrandom(timeslot["start_time"]) + end_time = self._format_time_for_secrandom(timeslot["end_time"]) + class_periods.append((start_time, end_time)) + + # 生成非上课时间段 + # 1. 第一节课之前的时间 + if class_periods: + first_start = class_periods[0][0] + if first_start != "00:00:00": + non_class_times["before_first_class"] = f"00:00:00-{first_start}" + + # 2. 课间时间(两节课之间) + for i in range(len(class_periods) - 1): + current_end = class_periods[i][1] + next_start = class_periods[i + 1][0] + if current_end != next_start: + period_name = f"break_{i + 1}" + non_class_times[period_name] = f"{current_end}-{next_start}" + + # 3. 最后一节课之后的时间 + if class_periods: + last_end = class_periods[-1][1] + if last_end != "23:59:59": + non_class_times["after_last_class"] = f"{last_end}-23:59:59" + + logger.info(f"成功解析CSES课程表,生成{len(non_class_times)}个非上课时间段") + return non_class_times + + def _format_time_for_secrandom(self, time_str: str) -> str: + """将时间字符串格式化为SecRandom需要的格式 (HH:MM:SS) + + Args: + time_str: 原始时间字符串 (HH:MM 或 HH:MM:SS) + + Returns: + str: 格式化后的时间字符串 (HH:MM:SS) + """ + if time_str.count(":") == 1: # HH:MM 格式 + return f"{time_str}:00" + return time_str + + def get_class_info(self) -> List[Dict]: + """获取课程信息列表 + + Returns: + List[Dict]: 课程信息列表 + """ + if not self.schedule_data: + return [] + + schedule = self.schedule_data["schedule"] + timeslots = schedule.get("timeslots", []) + + class_info = [] + for timeslot in timeslots: + info = { + "name": timeslot.get("name", ""), + "start_time": timeslot.get("start_time", ""), + "end_time": timeslot.get("end_time", ""), + "teacher": timeslot.get("teacher", ""), + "location": timeslot.get("location", ""), + "day_of_week": timeslot.get("day_of_week", ""), + } + class_info.append(info) + + return class_info + + def get_summary(self) -> str: + """获取课程表摘要信息 + + Returns: + str: 摘要信息 + """ + if not self.schedule_data: + return "未加载课程表" + + schedule = self.schedule_data["schedule"] + timeslots = schedule.get("timeslots", []) + + if not timeslots: + return "课程表为空" + + # 获取最早和最晚时间 + start_times = [slot["start_time"] for slot in timeslots] + end_times = [slot["end_time"] for slot in timeslots] + + summary = f"课程表包含{len(timeslots)}个时间段," + summary += f"最早开始时间:{min(start_times)}," + summary += f"最晚结束时间:{max(end_times)}" + + return summary diff --git a/app/common/extraction/extract.py b/app/common/extraction/extract.py index b54b6a7d..a6f0ceb0 100644 --- a/app/common/extraction/extract.py +++ b/app/common/extraction/extract.py @@ -13,6 +13,7 @@ from PySide6.QtCore import QDateTime from app.tools.path_utils import * +from app.common.extraction.cses_parser import CSESParser # ================================================== @@ -164,3 +165,164 @@ def _parse_time_string_to_seconds(time_str: str) -> int: seconds = time_parts[2] if len(time_parts) > 2 else 0 return hours * 3600 + minutes * 60 + seconds + + +# ================================================== +# CSES导入功能 +# ================================================== +def import_cses_schedule(file_path: str) -> tuple[bool, str]: + """从CSES文件导入课程表 + + Args: + file_path: CSES文件路径 + + Returns: + tuple[bool, str]: (是否成功, 结果消息) + """ + try: + # 创建CSES解析器 + parser = CSESParser() + + # 加载CSES文件 + if not parser.load_from_file(file_path): + return False, "CSES文件格式错误或文件无法读取" + + # 获取非上课时间段配置 + non_class_times = parser.get_non_class_times() + if not non_class_times: + return False, "未能从课程表中提取有效的时间段信息" + + # 保存到设置文件 + success = _save_non_class_times_to_settings(non_class_times) + if not success: + return False, "保存设置失败" + + # 获取摘要信息 + summary = parser.get_summary() + return True, f"成功导入课程表: {summary}" + + except Exception as e: + logger.error(f"导入CSES文件失败: {e}") + return False, f"导入失败: {str(e)}" + + +def import_cses_schedule_from_content(content: str) -> tuple[bool, str]: + """从CSES内容字符串导入课程表 + + Args: + content: CSES格式的YAML内容 + + Returns: + tuple[bool, str]: (是否成功, 结果消息) + """ + try: + # 创建CSES解析器 + parser = CSESParser() + + # 加载CSES内容 + if not parser.load_from_content(content): + return False, "CSES内容格式错误" + + # 获取非上课时间段配置 + non_class_times = parser.get_non_class_times() + if not non_class_times: + return False, "未能从课程表中提取有效的时间段信息" + + # 保存到设置文件 + success = _save_non_class_times_to_settings(non_class_times) + if not success: + return False, "保存设置失败" + + # 获取摘要信息 + summary = parser.get_summary() + return True, f"成功导入课程表: {summary}" + + except Exception as e: + logger.error(f"导入CSES内容失败: {e}") + return False, f"导入失败: {str(e)}" + + +def _save_non_class_times_to_settings(non_class_times: Dict[str, str]) -> bool: + """保存非上课时间段到设置文件 + + Args: + non_class_times: 非上课时间段字典 + + Returns: + bool: 保存成功返回True,否则返回False + """ + try: + settings_path = get_settings_path() + + # 读取现有设置 + if file_exists(settings_path): + with open_file(settings_path, "r", encoding="utf-8") as f: + settings = json.load(f) + else: + settings = {} + + # 更新非上课时间段配置 + settings["non_class_times"] = non_class_times + + # 写入设置文件 + with open_file(settings_path, "w", encoding="utf-8") as f: + json.dump(settings, f, ensure_ascii=False, indent=2) + + logger.info(f"成功保存{len(non_class_times)}个非上课时间段到设置文件") + return True + + except Exception as e: + logger.error(f"保存非上课时间段失败: {e}") + return False + + +def get_cses_import_template() -> str: + """获取CSES导入模板内容 + + Returns: + str: CSES格式的模板内容 + """ + template = """# CSES (Course Schedule Exchange Schema) 课程表模板 +# 更多详情请参考: https://github.com/SmartTeachCN/CSES + +schedule: + timeslots: + - name: "第一节课" + start_time: "08:00" + end_time: "08:45" + teacher: "张老师" + location: "教室A" + day_of_week: 1 + + - name: "第二节课" + start_time: "08:55" + end_time: "09:40" + teacher: "李老师" + location: "教室B" + day_of_week: 1 + + - name: "第三节课" + start_time: "10:00" + end_time: "10:45" + teacher: "王老师" + location: "教室C" + day_of_week: 1 + + - name: "第四节课" + start_time: "10:55" + end_time: "11:40" + teacher: "赵老师" + location: "教室D" + day_of_week: 1 +""" + return template + + +# ================================================== +# 导出函数列表 +# ================================================== +__all__ = [ + "import_cses_schedule", + "import_cses_schedule_from_content", + "get_cses_import_template", +] diff --git a/app/page_building/another_window.py b/app/page_building/another_window.py index 21a0cf09..8d7303c3 100644 --- a/app/page_building/another_window.py +++ b/app/page_building/another_window.py @@ -13,6 +13,7 @@ from app.view.another_window.prize.prize_name_setting import PrizeNameSettingWindow from app.view.another_window.prize.prize_weight_setting import PrizeWeightSettingWindow from app.view.another_window.remaining_list import RemainingListPage +from app.view.another_window.cses_template_viewer import CsesTemplateViewerWindow from app.Language.obtain_language import * from app.tools.variable import * @@ -48,6 +49,38 @@ def create_set_class_name_window(): return +# ================================================== +# CSES模板查看窗口 +# ================================================== +class cses_template_viewer_window_template(PageTemplate): + """CSES模板查看窗口类 + 使用PageTemplate创建CSES模板查看页面""" + + def __init__(self, parent=None): + super().__init__(content_widget_class=CsesTemplateViewerWindow, parent=parent) + + +def create_cses_template_viewer_window(): + """ + 创建CSES模板查看窗口 + + Returns: + 创建的窗口实例 + """ + title = get_content_name_async("time_settings", "template_title") + window = SimpleWindowTemplate(title, width=700, height=500) + window.add_page_from_template( + "cses_template_viewer", cses_template_viewer_window_template + ) + window.switch_to_page("cses_template_viewer") + _window_instances["cses_template_viewer"] = window + window.windowClosed.connect( + lambda: _window_instances.pop("cses_template_viewer", None) + ) + window.show() + return + + # ================================================== # 导入学生名单导入窗口 # ================================================== diff --git a/app/page_building/settings_window_page.py b/app/page_building/settings_window_page.py index 5138d77b..dcb6dca6 100644 --- a/app/page_building/settings_window_page.py +++ b/app/page_building/settings_window_page.py @@ -53,6 +53,7 @@ def __init__(self, parent: QFrame = None): "instant_draw_settings", "title" ), "lottery_settings": get_content_name_async("lottery_settings", "title"), + "time_settings": get_content_name_async("time_settings", "title"), } super().__init__(page_config, parent) self.set_base_path("app.view.settings.extraction_settings") diff --git a/app/view/another_window/cses_template_viewer.py b/app/view/another_window/cses_template_viewer.py new file mode 100644 index 00000000..81e1b3db --- /dev/null +++ b/app/view/another_window/cses_template_viewer.py @@ -0,0 +1,150 @@ +# ================================================== +# 导入库 +# ================================================== +from loguru import logger +from PySide6.QtWidgets import * +from PySide6.QtGui import * +from PySide6.QtCore import * +from qfluentwidgets import * + +from app.tools.variable import * +from app.tools.path_utils import * +from app.tools.personalised import * +from app.Language.obtain_language import * +from app.common.extraction.extract import get_cses_import_template + + +class CsesTemplateViewerWindow(QWidget): + """CSES模板查看器窗口""" + + def __init__(self, parent=None): + """初始化CSES模板查看器窗口""" + super().__init__(parent) + self.parent_window = parent + self.init_ui() + + def init_ui(self): + """初始化UI""" + # 设置窗口标题 + self.setWindowTitle(get_content_name_async("time_settings", "template_title")) + + # 创建主布局 + self.main_layout = QVBoxLayout(self) + self.main_layout.setContentsMargins(20, 20, 20, 20) + self.main_layout.setSpacing(15) + + # 创建标题标签 + title_label = QLabel(get_content_name_async("time_settings", "template_title")) + title_label.setStyleSheet("font-size: 16px; font-weight: bold;") + + # 创建文本编辑器 + self.text_edit = QTextEdit() + self.text_edit.setReadOnly(True) + self.text_edit.setFont(QFont("Consolas", 10)) + + # 获取并设置模板内容 + try: + template_content = get_cses_import_template() + self.text_edit.setPlainText(template_content) + except Exception as e: + logger.error(f"获取模板内容失败: {e}") + self.text_edit.setPlainText(f"无法加载模板: {str(e)}") + + # 创建按钮布局 + button_layout = QHBoxLayout() + + # 复制到剪贴板按钮 + self.copy_button = PushButton( + get_content_name_async("time_settings", "copy_to_clipboard") + ) + self.copy_button.setIcon(get_theme_icon("ic_fluent_copy_20_filled")) + self.copy_button.clicked.connect(self.on_copy_clicked) + + # 保存为文件按钮 + self.save_button = PushButton( + get_content_name_async("time_settings", "save_as_file") + ) + self.save_button.setIcon(get_theme_icon("ic_fluent_save_20_filled")) + self.save_button.clicked.connect(self.on_save_clicked) + + # 关闭按钮 + self.close_button = PushButton(get_content_name_async("time_settings", "close")) + self.close_button.clicked.connect(self.close) + + button_layout.addWidget(self.copy_button) + button_layout.addWidget(self.save_button) + button_layout.addStretch() + button_layout.addWidget(self.close_button) + + # 添加到主布局 + self.main_layout.addWidget(title_label) + self.main_layout.addWidget(self.text_edit) + self.main_layout.addLayout(button_layout) + + def on_copy_clicked(self): + """复制到剪贴板""" + try: + content = self.text_edit.toPlainText() + clipboard = QApplication.clipboard() + clipboard.setText(content) + + InfoBar.success( + title=get_content_name_async("time_settings", "copy_success"), + content=get_content_name_async("time_settings", "template_copied"), + orient=Qt.Horizontal, + isClosable=True, + position=InfoBarPosition.TOP, + duration=2000, + parent=self, + ) + except Exception as e: + logger.error(f"复制到剪贴板失败: {e}") + InfoBar.error( + title=get_content_name_async("time_settings", "import_failed"), + content=f"复制失败: {str(e)}", + orient=Qt.Horizontal, + isClosable=True, + position=InfoBarPosition.TOP, + duration=3000, + parent=self, + ) + + def on_save_clicked(self): + """保存为文件""" + try: + # 打开保存文件对话框 + file_path, _ = QFileDialog.getSaveFileName( + self, + get_content_name_async("time_settings", "save_template"), + get_content_name_async("time_settings", "cses_template"), + f"{get_content_name_async('time_settings', 'yaml_files')};;{get_content_name_async('time_settings', 'all_files')}", + ) + + if file_path: + content = self.text_edit.toPlainText() + with open(file_path, "w", encoding="utf-8") as f: + f.write(content) + + InfoBar.success( + title=get_content_name_async("time_settings", "save_success"), + content=get_content_name_async( + "time_settings", "template_saved" + ).format(file_path), + orient=Qt.Horizontal, + isClosable=True, + position=InfoBarPosition.TOP, + duration=3000, + parent=self, + ) + + except Exception as e: + logger.error(f"保存模板文件失败: {e}") + InfoBar.error( + title=get_content_name_async("time_settings", "import_failed"), + content=f"保存失败: {str(e)}", + orient=Qt.Horizontal, + isClosable=True, + position=InfoBarPosition.TOP, + duration=3000, + parent=self, + ) diff --git a/app/view/settings/extraction_settings/time_settings.py b/app/view/settings/extraction_settings/time_settings.py new file mode 100644 index 00000000..9ebcdd2b --- /dev/null +++ b/app/view/settings/extraction_settings/time_settings.py @@ -0,0 +1,299 @@ +# ================================================== +# 导入库 +# ================================================== +import os +import json +from datetime import datetime + +from loguru import logger +from PySide6.QtWidgets import * +from PySide6.QtGui import * +from PySide6.QtCore import * +from PySide6.QtNetwork import * +from qfluentwidgets import * + +from app.tools.variable import * +from app.tools.path_utils import * +from app.tools.personalised import * +from app.tools.settings_default import * +from app.tools.settings_access import * +from app.tools.settings_access import get_safe_font_size +from app.Language.obtain_language import * +from app.common.extraction.extract import import_cses_schedule, get_cses_import_template +from app.page_building.another_window import create_cses_template_viewer_window + + +# ================================================== +# 时间设置 +# ================================================== +class time_settings(QWidget): + def __init__(self, parent=None): + super().__init__(parent) + # 创建垂直布局 + self.vBoxLayout = QVBoxLayout(self) + self.vBoxLayout.setContentsMargins(0, 0, 0, 0) + self.vBoxLayout.setSpacing(10) + + # 添加课间禁用设置组件 + self.class_break_widget = class_break_settings(self) + self.vBoxLayout.addWidget(self.class_break_widget) + + # 添加CSES导入组件 + self.cses_import_widget = cses_import_settings(self) + self.vBoxLayout.addWidget(self.cses_import_widget) + + +class class_break_settings(GroupHeaderCardWidget): + def __init__(self, parent=None): + super().__init__(parent) + self.setTitle( + get_content_name_async("time_settings", "class_break_settings", "name") + ) + self.setBorderRadius(8) + + # 课间禁用开关 + self.class_break_switch = SwitchButton() + self.class_break_switch.setOffText( + get_content_name_async("time_settings", "disable") + ) + self.class_break_switch.setOnText( + get_content_name_async("time_settings", "enable") + ) + + # 从设置中读取当前状态 + current_enabled = self._get_class_break_enabled() + self.class_break_switch.setChecked(current_enabled) + + self.class_break_switch.checkedChanged.connect(self.on_class_break_changed) + + # 添加设置项到分组 + self.addGroup( + get_theme_icon("ic_fluent_clock_lock_20_filled"), + get_content_name_async("time_settings", "class_break_function", "name"), + get_content_name_async( + "time_settings", "class_break_function", "description" + ), + self.class_break_switch, + ) + + def _get_class_break_enabled(self) -> bool: + """获取课间禁用功能是否启用""" + try: + settings_path = get_settings_path() + if not file_exists(settings_path): + return False + + with open_file(settings_path, "r", encoding="utf-8") as f: + settings = json.load(f) + + program_functionality = settings.get("program_functionality", {}) + return program_functionality.get("instant_draw_disable", False) + except Exception as e: + logger.error(f"读取课间禁用设置失败: {e}") + return False + + def on_class_break_changed(self, is_checked: bool): + """当课间禁用开关状态改变时的处理""" + try: + settings_path = get_settings_path() + + # 读取现有设置 + if file_exists(settings_path): + with open_file(settings_path, "r", encoding="utf-8") as f: + settings = json.load(f) + else: + settings = {} + + # 更新程序功能设置 + if "program_functionality" not in settings: + settings["program_functionality"] = {} + + settings["program_functionality"]["instant_draw_disable"] = is_checked + + # 写入设置文件 + with open_file(settings_path, "w", encoding="utf-8") as f: + json.dump(settings, f, ensure_ascii=False, indent=2) + + logger.info(f"课间禁用功能已{'开启' if is_checked else '关闭'}") + + except Exception as e: + logger.error(f"保存课间禁用设置失败: {e}") + # 恢复开关状态 + self.class_break_switch.setChecked(not is_checked) + + +class cses_import_settings(GroupHeaderCardWidget): + def __init__(self, parent=None): + super().__init__(parent) + self.setTitle( + get_content_name_async("time_settings", "cses_import_settings", "name") + ) + self.setBorderRadius(8) + + # 导入文件按钮 + self.import_file_button = PushButton( + get_content_name_async("time_settings", "import_from_file") + ) + self.import_file_button.setIcon( + get_theme_icon("ic_fluent_folder_open_20_filled") + ) + self.import_file_button.clicked.connect(self.on_import_file_clicked) + + # 查看模板按钮 + self.view_template_button = PushButton( + get_content_name_async("time_settings", "view_template") + ) + self.view_template_button.setIcon( + get_theme_icon("ic_fluent_document_20_filled") + ) + self.view_template_button.clicked.connect(self.on_view_template_clicked) + + # 当前课程表信息标签 + self.schedule_info_label = QLabel( + get_content_name_async("time_settings", "no_schedule_imported") + ) + self._update_schedule_info() + + # 创建按钮布局 + button_layout = QHBoxLayout() + button_layout.addWidget(self.import_file_button) + button_layout.addWidget(self.view_template_button) + button_layout.addStretch() + + # 创建信息布局 + info_layout = QVBoxLayout() + info_layout.addLayout(button_layout) + info_layout.addWidget(self.schedule_info_label) + + # 创建容器控件来包含布局 + info_widget = QWidget() + info_widget.setLayout(info_layout) + + # 添加设置项到分组 + self.addGroup( + get_theme_icon("ic_fluent_calendar_ltr_20_filled"), + get_content_name_async("time_settings", "cses_import", "name"), + get_content_name_async("time_settings", "cses_import", "description"), + info_widget, + ) + + def _update_schedule_info(self): + """更新课程表信息显示""" + try: + settings_path = get_settings_path() + if not file_exists(settings_path): + self.schedule_info_label.setText( + get_content_name_async("time_settings", "no_schedule_imported") + ) + return + + with open_file(settings_path, "r", encoding="utf-8") as f: + settings = json.load(f) + + non_class_times = settings.get("non_class_times", {}) + if non_class_times: + count = len(non_class_times) + self.schedule_info_label.setText( + get_content_name_async("time_settings", "schedule_imported").format( + count + ) + ) + else: + self.schedule_info_label.setText( + get_content_name_async("time_settings", "no_schedule_imported") + ) + + except Exception as e: + logger.error(f"更新课程表信息失败: {e}") + self.schedule_info_label.setText("获取课程表信息失败") + + def on_import_file_clicked(self): + """当点击导入文件按钮时的处理""" + # 打开文件选择对话框 + file_path, _ = QFileDialog.getOpenFileName( + self, + get_content_name_async("time_settings", "select_cses_file"), + "", + f"{get_content_name_async('time_settings', 'yaml_files')};;{get_content_name_async('time_settings', 'all_files')}", + ) + + if file_path: + self._import_cses_file(file_path) + + def _import_cses_file(self, file_path: str): + """导入CSES文件""" + try: + # 显示等待对话框 + self.import_file_button.setEnabled(False) + self.import_file_button.setText( + get_content_name_async("time_settings", "importing") + ) + + # 调用导入函数 + success, message = import_cses_schedule(file_path) + + if success: + # 显示成功信息 + InfoBar.success( + title=get_content_name_async("time_settings", "import_success"), + content=message, + orient=Qt.Horizontal, + isClosable=True, + position=InfoBarPosition.TOP, + duration=3000, + parent=self, + ) + + # 更新课程表信息 + self._update_schedule_info() + + else: + # 显示错误信息 + InfoBar.error( + title=get_content_name_async("time_settings", "import_failed"), + content=message, + orient=Qt.Horizontal, + isClosable=True, + position=InfoBarPosition.TOP, + duration=5000, + parent=self, + ) + + except Exception as e: + logger.error(f"导入CSES文件失败: {e}") + InfoBar.error( + title=get_content_name_async("time_settings", "import_failed"), + content=get_content_name_async("time_settings", "import_error").format( + str(e) + ), + orient=Qt.Horizontal, + isClosable=True, + position=InfoBarPosition.TOP, + duration=5000, + parent=self, + ) + + finally: + # 恢复按钮状态 + self.import_file_button.setEnabled(True) + self.import_file_button.setText( + get_content_name_async("time_settings", "import_from_file") + ) + + def on_view_template_clicked(self): + """当点击查看模板按钮时的处理""" + try: + # 使用独立窗口模板创建CSES模板查看器 + create_cses_template_viewer_window() + + except Exception as e: + logger.error(f"显示模板失败: {e}") + InfoBar.error( + title=get_content_name_async("time_settings", "import_failed"), + content=f"无法显示模板: {str(e)}", + orient=Qt.Horizontal, + isClosable=True, + position=InfoBarPosition.TOP, + duration=3000, + parent=self, + )