Compare commits

...

2 Commits

Author SHA1 Message Date
82fd8ab199 feat(interface): 图形化设置页原型 2026-04-20 03:33:05 +08:00
334d04444d style: 删除旧配置文件 2026-04-20 01:46:40 +08:00
10 changed files with 146 additions and 185 deletions

View File

@@ -1,48 +0,0 @@
persist_to_file = 1
daystamp_override = -1
timestamp_override = -1
quick_pass = true
auto_pass = false
scheduled_num = 420
timezone_offset = 28800
[algorithm]
default = "FAST-0"
[paths]
data = "./data"
cache = "./data/cache"
config = "./data/config"
global = "./data/global"
repo = "./data/repo"
[services]
audio = "playsound"
tts = "edgetts"
llm = "openai"
sync = "webdav"
[sync]
[interface.memorizor]
autovoice = true
[puzzles.mcq]
max_riddles_num = 2
[puzzles.cloze]
min_denominator = 3
[providers.tts.edgetts]
voice = "zh-CN-XiaoxiaoNeural"
[providers.llm.openai]
url = ""
key = ""
[providers.sync.webdav]
url = ""
username = ""
password = ""
remote_path = "/heurams/"
verify_ssl = true

4
data/config/test.toml Normal file
View File

@@ -0,0 +1,4 @@
[aa.bb]
str = "sq"
boolean = true
int = 123

View File

@@ -20,6 +20,7 @@ from .screens.about import AboutScreen
from .screens.dashboard import DashboardScreen from .screens.dashboard import DashboardScreen
from .screens.navigator import NavigatorScreen from .screens.navigator import NavigatorScreen
from .screens.precache import PrecachingScreen from .screens.precache import PrecachingScreen
from .screens.setting import SettingScreen
from .screens.synctool import SyncScreen from .screens.synctool import SyncScreen
_end = perf_counter() _end = perf_counter()
print(f"已完成! (耗时: {round(1000 * (_end - _start))}ms)") print(f"已完成! (耗时: {round(1000 * (_end - _start))}ms)")
@@ -44,7 +45,7 @@ class HeurAMSApp(App):
"synctool": SyncScreen, "synctool": SyncScreen,
"about": AboutScreen, "about": AboutScreen,
"navigator": NavigatorScreen, "navigator": NavigatorScreen,
# "config": ConfigScreen, "setting": SettingScreen,
} }
def on_mount(self) -> None: def on_mount(self) -> None:

View File

@@ -27,7 +27,7 @@ class NavigatorScreen(ModalScreen):
# ("创建仓库", "repo_creator"), # ("创建仓库", "repo_creator"),
("缓存管理器", "precache_all"), ("缓存管理器", "precache_all"),
("收藏夹", FavoriteManagerScreen), ("收藏夹", FavoriteManagerScreen),
# ("配置设置", "config"), ("设置页面", "setting"),
# ("调试日志", "logviewer"), # ("调试日志", "logviewer"),
("同步工具", "synctool"), ("同步工具", "synctool"),
("关于此软件", "about"), ("关于此软件", "about"),

View File

@@ -0,0 +1,116 @@
"""设置页面"""
from functools import reduce
import pathlib
from pathlib import Path
import os
from textual.app import ComposeResult
from textual.containers import ScrollableContainer, Container, Horizontal, Vertical
from textual.screen import Screen
from textual.widgets import Button, Footer, Header, Label, ListItem, ListView, Static, Collapsible, Input, Switch
from textual.layouts import horizontal
import heurams.kernel.particles as pt
import heurams.services.timer as timer
import heurams.services.version as version
from heurams.context import *
from heurams.kernel.particles import *
from heurams.kernel.repolib import *
from heurams.kernel.algorithms import algorithms
from heurams.services.logger import get_logger
from heurams.services.textproc import domize, undomize
from heurams.services.epath import epath
logger = get_logger(__name__)
class SettingScreen(Screen):
"""设置页面屏幕"""
SUB_TITLE = "设置"
BINDINGS = [
("q", "go_back", "返回"),
]
def __init__(
self,
name: str | None = None,
id: str | None = None,
classes: str | None = None,
) -> None:
super().__init__(name, id, classes)
def compose(self) -> ComposeResult:
"""组合界面组件"""
yield Header(show_clock=True)
with ScrollableContainer():
yield Label('设置页面')
for i in config_var.get():
yield Collapsible(*self._get_subcfg(f'{i}'), title=i)
yield Footer()
def _get_subcfg(self, parent_epath: str):
parent = epath(config_var.get(), parent_epath)
if isinstance(parent, ConfigDict):
if parent.is_dir:
lst = list()
for i in parent:
lst.append(Collapsible(*self._get_subcfg(f"{parent_epath}.{i}"), title=i))
return lst
if isinstance(parent, dict) or (isinstance(parent, ConfigDict) and not parent.is_dir):
lst = list()
for i in parent:
if i.startswith('_'):
continue
if isinstance(parent[i], dict):
lst.append(Collapsible(*self._get_subcfg(f"{parent_epath}.{i}"), title=i))
elif isinstance(parent[i], float):
lst.extend([
Label(i),
Input(value=str(parent[i]), placeholder='要求一个浮点数', type='number', id=domize(f"{parent_epath}.{i}"))
])
elif isinstance(parent[i], str):
lst.extend([
Label(i),
Input(value=parent[i], placeholder='要求一个字符串', type='text', id=domize(f"{parent_epath}.{i}"))
])
elif isinstance(parent[i], bool):
lst.extend([
Label(i),
Switch(value=str(parent[i]), id=domize(f"{parent_epath}.{i}"))
])
elif isinstance(parent[i], int):
lst.extend([
Label(i),
Input(value=str(parent[i]), placeholder='要求一个整数', type='integer', id=domize(f"{parent_epath}.{i}"))
])
elif isinstance(parent[i], list):
pass
else:
lst.append(Label('未知类型'))
return lst
return [Label('无子项')]
def on_mount(self) -> None:
"""挂载组件时初始化"""
pass
def action_quit_app(self) -> None:
"""退出应用程序"""
self.app.exit()
def action_open_navigator(self) -> None:
"""打开导航器"""
self.app.push_screen(NavigatorScreen())
def on_button_pressed(self, event: Button.Pressed) -> None:
logger.debug(f"event.button.id: {event.button.id}")
"""处理按钮点击事件"""
if str(event.button.id) == 'apply':
pass
if str(event.button.id) == 'openfolder':
pass
if str(event.button.id) == 'cancel':
pass

View File

@@ -1,127 +0,0 @@
"""配置文件服务"""
import pathlib
import typing
import toml
from heurams.services.logger import get_logger
default_config = {
"persist_to_file": 1, # 将更改保存到文件
"daystamp_override": -1, # 覆写时间, 设为 -1 以禁用
"timestamp_override": -1, # 覆写时间, 设为 -1 以禁用
"quick_pass": 1, # 启用用于测试的快速通过
"scheduled_num": 8, # 对于每个项目的默认新记忆原子数量
"timezone_offset": 28800, # UTC 时间戳修正 仅用于 UNIX 日时间戳的生成修正, 单位为秒
# 28800 是中国标准时间 (UTC+8)
"interface": {
"memorizor": {
"autovoice": True # 自动语音播放, 仅限于 recognition 组件
}
},
"algorithm": {
"default": "SM-2" # 主要算法
# 可选项: SM-2, SM-15M, FSRS
},
"puzzles": { # 谜题默认配置
"mcq": {
"max_riddles_num": 2
},
"cloze": {
"min_denominator": 3
}
},
"paths": { # 相对于配置文件的 ".." (即工作目录) 而言 或绝对路径
"data": "./data"
},
"services": { # 定义服务到提供者的映射
"audio": "playsound", # 可选项: playsound(通用), termux(仅用于 Android Termux), mpg123(TODO)
"tts": "edgetts", # 可选项: edgetts
"llm": "openai", # 可选项: openai
"sync": "webdav" # 可选项: 留空, webdav
},
"providers": {
"tts": {
"edgetts": { # EdgeTTS 设置
"voice": "zh-CN-XiaoxiaoNeural" # 可选项: zh-CN-YunjianNeural (男声), zh-CN-XiaoxiaoNeural (女声)
}
},
"llm": {
"openai": { # 与 OpenAI 相容的语言模型接口服务设置
"url": "",
"key": ""
}
},
"sync": {
"webdav": { # WebDAV 同步设置
"url": "",
"username": "",
"password": "",
"remote_path": "/heurams/",
"verify_ssl": True
}
}
},
}
class ConfigFile:
def __init__(self, path: pathlib.Path):
self.logger = get_logger(__name__)
self.path = path
self.data = dict()
if not self.path.exists():
self.path.touch()
self.logger.debug("创建配置文件: %s", self.path)
self.data = default_config
self.valid_configfile = 1
# 考虑到可能临时编辑格式错误, 所以不覆写格式错误的配置文件, 而是提示损坏并使用默认配置
self._load()
def _load(self):
"""从文件加载配置数据"""
with open(self.path, "r") as f:
try:
self.data = toml.load(f)
self.logger.debug("配置文件加载成功: %s", self.path)
except toml.TomlDecodeError as e:
print(f"{e}")
self.logger.error("TOML解析错误: %s", e)
self.data = default_config
self.valid_configfile = 0
def modify(self, key: str, value: typing.Any):
"""修改配置值并保存"""
self.data[key] = value
self.logger.debug("修改配置项: %s = %s", key, value)
self.save()
def save(self, path: typing.Union[str, pathlib.Path] = ""):
"""保存配置到文件"""
if self.valid_configfile:
save_path = pathlib.Path(path) if path else self.path
with open(save_path, "w") as f:
toml.dump(self.data, f)
self.logger.debug("配置文件已保存: %s", save_path)
else:
pass
def get(self, key: str, default: typing.Any = None) -> typing.Any:
"""获取配置值, 如果不存在返回默认值"""
return self.data.get(key, default)
def __getitem__(self, key: str) -> typing.Any:
return self.data[key]
def __setitem__(self, key: str, value: typing.Any):
self.data[key] = value
self.save()
def __contains__(self, key: str) -> bool:
"""支持 in 语法"""
return key in self.data

View File

@@ -41,13 +41,9 @@ class ConfigDict(UserDict): # 舒服了
return self.path / s+'.toml' return self.path / s+'.toml'
def __contains__(self, key): def __contains__(self, key):
if isinstance(key, str):
key = self.converter(key)
return super().__contains__(key) return super().__contains__(key)
def __setitem__(self, key, value): def __setitem__(self, key, value):
if isinstance(key, str):
key = self.converter(key)
origvalue = super().__getitem__(key) # 所以你不该访问不存在的对象 origvalue = super().__getitem__(key) # 所以你不该访问不存在的对象
if isinstance(origvalue, ConfigDict): if isinstance(origvalue, ConfigDict):
if origvalue.path.is_dir(): if origvalue.path.is_dir():
@@ -78,3 +74,10 @@ class ConfigDict(UserDict): # 舒服了
def __del__(self): def __del__(self):
if not self.is_dir: if not self.is_dir:
self.persist() # 不准循环引用, 懂了吧 self.persist() # 不准循环引用, 懂了吧
@staticmethod
def titleize(objt):
if isinstance(objt, pathlib.Path):
return objt.stem
else:
return objt

View File

@@ -1,13 +1,19 @@
from heurams.services.config import ConfigDict
from heurams.services.logger import get_logger
logger = get_logger(__name__)
def epath(dct, path: str = '', default=None, parents=False): def epath(dct, path: str = '', default=None, parents=False):
if not path: if not path:
return dct return dct
path = path.rstrip('/') path = path.rstrip('.')
path = path.lstrip('.')
target = dct target = dct
for i in path.split('/'): for i in path.split('.'):
# 处理字典键 # 处理字典键
if isinstance(target, dict) and i in target: logger.debug(f'处理 {i}, {(isinstance(target, dict) or isinstance(target, ConfigDict))} {i in target}')
if (isinstance(target, dict) or isinstance(target, ConfigDict)) and i in target:
target = target[i] target = target[i]
# 处理列表索引 # 处理列表索引
elif i.startswith('[') and i.endswith(']') and isinstance(target, (list, tuple)): elif i.startswith('[') and i.endswith(']') and isinstance(target, (list, tuple)):

View File

@@ -2,3 +2,9 @@ def truncate(text):
if len(text) <= 3: if len(text) <= 3:
return text return text
return text[:3] + ">" return text[:3] + ">"
def domize(text):
return text.replace('.', '--DOT--')
def undomize(text):
return text.replace('--DOT--', '.')