a13rollcall/config_manager.py
2026-10-06 18:38:55 +08:00

266 行
8.2 KiB
Python

import json
import os
import copy
RESTART_REQUIRED_KEYS = [
"experimental.dpi_optimization",
"ui.window_opacity",
"ui.sidebar_width",
"experimental.smooth_scroll",
"ui.rounded_window",
"ui.window_radius",
"ui.window_shadow",
"ui.dark_mode",
]
DEFAULT_CONFIG = {
"app": {
"class_name": "A13",
"app_name": "课堂点名程序",
"engine_name": "A13",
"version": "6.8"
},
"ui": {
"theme": "light",
"font_family": "微软雅黑",
"font_size": 12,
"name_font_size": 50,
"text_font_size": 14,
"name_font_weight": "bold",
"animation_duration": 3.0,
"ease_strength": 0.92,
"animation_curve": "ease_out",
"button_radius": 10,
"card_radius": 12,
"border_width": 1,
"shadow_enabled": True,
"sidebar_width": 300,
"window_opacity": 1.0,
"start_animation": True,
"start_anim_duration": 2500,
"show_status_bar": True,
"result_popup": False,
"popup_duration": 2000,
"draw_countdown": False,
"countdown_number": 3,
"particle_enabled": True,
"particle_count": 30,
"particle_speed": 2,
"scroll_speed": 1.0,
"dark_mode": False,
"status_bar": True,
"startup_animation": True
},
"draw": {
"mode": "auto",
"no_repeat": True,
"auto_duration": 4.0,
"dynamic_focus": True,
"dynamic_weight": True,
"rate_mastered": 1.0,
"rate_familiar": 2.0,
"rate_unlearned": 3.0,
"auto_reset_round": True,
"sound_enabled": False,
"anti_cheat": False,
"history_limit": 50,
"weight_multiplier": {
"已背过": 1,
"未背熟": 2,
"未背过": 3
}
},
"text": {
"extract_mode": "段落抽取",
"paragraph_random": True,
"show_title": True,
"text_align": "left"
},
"files": {
"students": "students.txt",
"texts": "texts.txt",
"records": "records.txt",
"stats": "stats.txt",
"students_file": "students.txt",
"texts_file": "texts.txt",
"records_file": "records.txt",
"stats_file": "stats.txt"
},
"shortcut": {
"enabled": True,
"key_draw": "space",
"key_mastered": "1",
"key_familiar": "2",
"key_unlearned": "3",
"key_hide_panel": "Escape",
"key_reset_round": "r"
},
"student_status": {},
"student_weights": {},
"text_progress": {},
"experimental": {
"dpi_optimization": False,
"particle_effect": True,
"sound_effect": False,
"voice_broadcast": False,
"anti_cheat_mode": False,
"history_recall": False,
"smooth_scroll": False,
"blur_background": False,
"result_popup": False,
"countdown_before_draw": False,
"auto_reset_round": False
},
"hooks": {
"draw_start": [],
"draw_finish": [],
"status_change": []
},
"quick_draw": {
"enabled": True,
"always_on_top": True,
"show_history": True,
"auto_hide": False,
"sound_enabled": False,
"no_repeat": True,
"animation_duration": 2.0,
"name_font_size": 48,
"window_width": 420,
"window_height": 520,
"show_count": True,
"confirm_on_close": True
},
"tts": {
"enabled": False,
"rate": 150,
"volume": 1.0,
"speak_text": True
},
"startup": {
"default_mode": None
}
}
class ConfigManager:
def __init__(self, config_path="config.json"):
self.config_path = config_path
self.config = {}
self.old_config = None
self._hooks = {
"draw_start": [],
"draw_finish": [],
"status_change": []
}
self.load_config()
def _load_default(self):
return copy.deepcopy(DEFAULT_CONFIG)
def _merge_config(self, source, default):
"""递归深度合并配置,用户配置覆盖默认值,缺失的key自动补全"""
result = copy.deepcopy(default)
for key, value in source.items():
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
result[key] = self._merge_config(value, result[key])
else:
result[key] = value
return result
def load_config(self):
"""加载配置文件,不存在则生成默认配置;损坏则回退默认值"""
default = self._load_default()
if os.path.exists(self.config_path):
try:
with open(self.config_path, "r", encoding="utf-8") as f:
user_config = json.load(f)
self.config = self._merge_config(user_config, default)
except Exception:
self.config = default
self.save_config()
else:
self.config = default
self.save_config()
def save_config(self):
"""将内存配置序列化写入磁盘"""
try:
with open(self.config_path, "w", encoding="utf-8") as f:
json.dump(self.config, f, indent=2, ensure_ascii=False)
except Exception:
pass
def _get_by_path(self, data, keys):
"""按key路径链式读取字典值"""
current = data
for key in keys:
if not isinstance(current, dict) or key not in current:
return None
current = current[key]
return current
def get(self, *keys):
"""链式获取配置值,不存在返回None"""
return self._get_by_path(self.config, keys)
def set(self, value, *keys):
"""链式设置内存配置值,自动创建中间字典"""
current = self.config
for key in keys[:-1]:
if key not in current or not isinstance(current[key], dict):
current[key] = {}
current = current[key]
current[keys[-1]] = value
def backup_current(self):
"""创建当前配置快照,用于取消修改或重启项回滚"""
self.old_config = copy.deepcopy(self.config)
def check_restart_required(self):
"""对比快照与当前配置,判断是否修改了需要重启的项"""
if self.old_config is None:
return False
for path in RESTART_REQUIRED_KEYS:
keys = path.split(".")
old_val = self._get_by_path(self.old_config, keys)
new_val = self._get_by_path(self.config, keys)
if old_val != new_val:
return True
return False
def register_hook(self, hook_type, callback):
"""注册事件钩子回调"""
if hook_type in self._hooks:
self._hooks[hook_type].append(callback)
def trigger_hooks(self, hook_type, *args):
"""触发事件钩子,单个回调异常不影响整体"""
if hook_type not in self._hooks:
return
for callback in self._hooks[hook_type]:
try:
callback(*args)
except Exception:
continue
def reset_all_files(self):
"""
危险操作:清空所有数据文件 + 重置配置
删除 students.txt / texts.txt / records.txt / stats.txt / config.json
重新生成默认配置文件
"""
target_files = [
self.get("files", "students"),
self.get("files", "texts"),
self.get("files", "records"),
self.get("files", "stats"),
self.config_path
]
for file_path in target_files:
try:
if file_path and os.path.exists(file_path):
os.remove(file_path)
except Exception:
pass
self.config = self._load_default()
self.save_config()