fix: 改进与日志简化

This commit is contained in:
2026-05-22 22:33:57 +08:00
parent 31996f2532
commit 0e42d0410c
64 changed files with 2772 additions and 875 deletions
+20 -19
View File
@@ -2,52 +2,53 @@ from time import sleep, perf_counter
# import gc
# gc.set_threshold(100, 1, 1)
print("欢迎使用基本用户界面!")
print("加载配置与上下文... ", end="", flush=True)
from heurams.i18n import _
print(_("Welcome to the basic user interface!"))
print(_("Loading config and context... "), end="", flush=True)
_start_all = perf_counter()
_start = _start_all
from heurams.context import rootdir, workdir, config_var
_end = perf_counter()
print(f"已完成! (耗时: {round(1000 * (_end - _start))}ms)")
print(_("Done! ({time}ms)").format(time=round(1000 * (_end - _start))))
print("加载用户界面框架... ", end="", flush=True)
print(_("Loading UI framework... "), end="", flush=True)
_start = perf_counter()
from textual.app import App
from textual.widgets import Button
_end = perf_counter()
print(f"已完成! (耗时: {round(1000 * (_end - _start))}ms)")
print(_("Done! ({time}ms)").format(time=round(1000 * (_end - _start))))
print("加载用户界面布局... ", end="", flush=True)
print(_("Loading UI layout... "), end="", flush=True)
_start = perf_counter()
from .screens.about import AboutScreen
from .screens.dashboard import DashboardScreen
from .screens.navigator import NavigatorScreen
from .screens.precache import PrecachingScreen
from .screens.setting import SettingScreen
from .screens.synctool import SyncScreen
from .screens.about import AboutScreen
from . import shim
_end = perf_counter()
print(f"已完成! (耗时: {round(1000 * (_end - _start))}ms)")
print(_("Done! ({time}ms)").format(time=round(1000 * (_end - _start))))
print(f"组件目录: {rootdir}")
print(f"工作目录: {workdir}")
print(_("Component directory: {path}").format(path=rootdir))
print(_("Working directory: {path}").format(path=workdir))
_end_all = perf_counter()
print(f"前置工作共计耗时: {round(1000 * (_end_all - _start_all))}ms")
print(_("Pre-work total: {time}ms").format(time=round(1000 * (_end_all - _start_all))))
class HeurAMSApp(App):
TITLE = "潜进"
TITLE = "HeurAMS"
CSS_PATH = rootdir / "interface" / "css" / "main.tcss"
SUB_TITLE = "启发式辅助记忆调度器"
SUB_TITLE = _("Heuristic Auxiliary Memorizing Scheduler")
BINDINGS = [
("q", "go_back", "退出"),
("d", "toggle_dark", "主题"),
("n", "app.push_screen('navigator')", "导航"),
("s", "app.push_screen('setting')", "设置"),
("z", "app.push_screen('about')", "关于"),
("q", "go_back", _("Quit")),
("d", "toggle_dark", _("Theme")),
("n", "app.push_screen('navigator')", _("Navigate")),
("s", "app.push_screen('setting')", _("Settings")),
("z", "app.push_screen('about')", _("About")),
]
SCREENS = {
"dashboard": DashboardScreen,
+2 -1
View File
@@ -1,5 +1,6 @@
from heurams.interface import *
from heurams.context import config_var
from heurams.i18n import _
from heurams.services.logger import get_logger
import threading
import pickle
@@ -21,7 +22,7 @@ def start_debug_server(app):
code = pickle.loads(msg)
namespace = {"app": app, "logger": logger, "config_var": config_var}
if first:
app.title += " [调试已连接]"
app.title += _(" [Debug Connected]")
first = 0
try:
# 先尝试 eval
+47 -39
View File
@@ -1,4 +1,4 @@
"""关于界面"""
"""About screen"""
from textual.app import ComposeResult
from textual.containers import ScrollableContainer
@@ -8,6 +8,7 @@ from textual import events, on
import heurams.services.version as version
from heurams.context import *
from heurams.i18n import _
import platform
import shutil
import os
@@ -16,10 +17,10 @@ import sys
class AboutScreen(Screen):
BINDINGS = [
("q", "go_back", "返回"),
("z", "go_back", "关于"),
("q", "go_back", _("Back")),
("z", "go_back", _("About")),
]
SUB_TITLE = "关于"
SUB_TITLE = _("About")
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -37,55 +38,64 @@ class AboutScreen(Screen):
show_clock=config_var.get()["interface"]["global"]["clock_on_header"]
)
with ScrollableContainer(id="about_container"):
yield Label("[b]关于与版本信息[/b]")
# 获取系统信息
yield Label(_("[b]About & Version Info[/b]"))
# Get system info
textual_version = self._get_textual_version()
terminal_info = self._get_terminal_info()
python_version = self._get_python_version()
os_version = self._get_os_version()
disk_usage = self._get_disk_usage()
about_text = f"""
# 关于 HeurAMS "潜进"
about_text = _(
"""# About HeurAMS
主程序库版本: `{version.ver}-python`
用户界面分支: `Textual TUI (基本用户界面)`
用户界面版本: `{version.ver}`
API 版本代号: `{version.codename.capitalize()}`
Main library version: `{ver}-python`
UI frontend: `Textual TUI (Basic UI)`
UI version: `{ver}`
API codename: `{codename}`
> 一个基于启发式算法与认知科学理论的辅助记忆调度器, 旨在帮助用户更高效地进行记忆工作与学习规划.
> 一个开放, 优雅, 易于扩展的间隔重复调度器实验平台, 旨在帮助研究者更高效地进行前沿记忆算法的研究.
> A heuristic auxiliary memorizing scheduler based on heuristic algorithms and cognitive science theories, designed to help users memorize and plan learning more efficiently.
> An open, elegant, and extensible spaced repetition scheduler experimental platform, designed to help researchers conduct investigations, experiments, and research on cutting-edge memory algorithms more efficiently.
您可在项目主页 https://ams.pluv27.top 获取用户指南, 开发文档与软件更新, 并参与到软件的开发与改进工作.
You can visit the project homepage at https://ams.pluv27.top for user guides, development documentation and software updates, and participate in software development and improvement.
以 GNU Affero 通用公共许可证 (第3版) 开放源代码, 并有一条豁免本机 API 调用的附加条款, 用于其他前端到程序库的接口调用.
Open source under the GNU Affero General Public License (version 3), with an additional exemption clause for local API calls, used for other frontend to library interface calls.
您正使用程序库内置的终端用户界面, 它是第一个全功能前端实现与程序库测试套件, 位于程序库的 interface 子目录.
You are using the built-in terminal user interface, which is the first full-featured frontend implementation and library test suite, located in the interface subdirectory of the library.
开发人员列表:
- Wang Zhiyu ([@pluvium27](https://github.com/pluvium27)): 项目发起与主要开发者
Developers:
- Wang Zhiyu ([@pluvium27](https://github.com/pluvium27)): Project initiator and lead developer
感谢以下人士与团体, 他们的算法与理论构成了此软件现有算法的基石:
Special thanks to the following individuals and groups; their algorithms and theories form the cornerstone of the current software algorithms:
- [Piotr A. Woźniak](https://supermemo.guru/wiki/Piotr_Wozniak): SM-2 算法与 SM-15 算法理论
- [Jarrett Ye](https://github.com/L-M-Sherlock): FSRS 算法与间隔重复理论文献参考
- [Kazuaki Tanida](https://github.com/slaypni): SM-15 算法的 CoffeeScript 逆向实现
- [Open Spaced Repetition](https://github.com/open-spaced-repetition): FSRS 算法底层实现
- [Piotr A. Woźniak](https://supermemo.guru/wiki/Piotr_Wozniak): SM-2 algorithm and SM-15 algorithm theory
- [Jarrett Ye](https://github.com/L-M-Sherlock): FSRS algorithm and spaced repetition theory references
- [Kazuaki Tanida](https://github.com/slaypni): CoffeeScript reverse implementation of SM-15 algorithm
- [Open Spaced Repetition](https://github.com/open-spaced-repetition): FSRS algorithm underlying implementation
# 运行环境信息
# Runtime Environment
Python 解释器版本: {python_version}
Python 解释器路径: {sys.executable}
Textual 框架版本: {textual_version}
终端模拟器: {terminal_info}
操作系统版本: {os_version}
存储余量: {disk_usage}
Python interpreter version: {python_version}
Python interpreter path: {executable}
Textual framework version: {textual_version}
Terminal emulator: {terminal_info}
Operating system version: {os_version}
Disk free space: {disk_usage}
报告问题时, 请复制这些信息到问题描述, 并上传软件日志 `heurams.log` 作为附件, 以协助开发者定位错误
"""
When reporting issues, please copy this information into the issue description and attach `heurams.log` as an attachment to help developers locate the error."""
).format(
ver=version.ver,
codename=version.codename.capitalize(),
python_version=python_version,
executable=sys.executable,
textual_version=textual_version,
terminal_info=terminal_info,
os_version=os_version,
disk_usage=disk_usage,
)
yield Markdown(about_text, classes="about-markdown")
yield Button(
"返回主界面",
_("Back to Main"),
id="back_button",
variant="primary",
flat=True,
@@ -102,22 +112,20 @@ Textual 框架版本: {textual_version}
self.action_go_back()
def _get_textual_version(self) -> str:
"""获取 Textual 框架版本"""
try:
import textual
return textual.__version__
except (ImportError, AttributeError):
return "未知"
return _("Unknown")
def _get_terminal_info(self) -> str:
"""获取终端模拟器信息"""
terminal = shutil.which("terminal")
if terminal:
return terminal
# 尝试从环境变量获取
# Try from environment variables
terminal_env = os.environ.get("TERM_PROGRAM") or os.environ.get("TERM")
return terminal_env or "未知"
return terminal_env or _("Unknown")
def _get_python_version(self) -> str:
"""获取 Python 解释器版本"""
+46 -23
View File
@@ -1,4 +1,4 @@
"""仪表盘界面"""
"""Dashboard screen"""
from functools import reduce
from pathlib import Path
@@ -14,6 +14,7 @@ import heurams.kernel.particles as pt
import heurams.services.timer as timer
import heurams.services.version as version
from heurams.context import *
from heurams.i18n import _
from heurams.kernel.particles import *
from heurams.kernel.repolib import *
from heurams.services.logger import get_logger
@@ -25,11 +26,11 @@ logger = get_logger(__name__)
class DashboardScreen(Screen):
"""主仪表盘屏幕"""
"""Main dashboard screen"""
SUB_TITLE = "仪表盘"
SUB_TITLE = _("Dashboard")
BINDINGS = [
("q", "go_back", "返回"),
("q", "go_back", _("Back")),
]
CSS_PATH = rootdir / "interface" / "css" / "screens" / "dashboard.tcss"
@@ -46,45 +47,50 @@ class DashboardScreen(Screen):
self._load_data()
def compose(self) -> ComposeResult:
"""组合界面组件"""
"""Compose UI components"""
if config_var.get()["interface"]["global"]["show_header"]:
yield Header(
show_clock=config_var.get()["interface"]["global"]["clock_on_header"]
)
with ScrollableContainer():
yield Horizontal( # 顶部的状态
yield Horizontal(
Vertical(
Label(f"当前日时间戳: {timer.get_daystamp()}"),
Label(_("Current daystamp: {ds}").format(ds=timer.get_daystamp())),
Label(
f"应用时区修正: UTC+{str(config_var.get()['services']['timer']['timezone_offset'] / 3600).removesuffix('.0')}"
_("Timezone offset: UTC+{offset}").format(offset=str(config_var.get()['services']['timer']['timezone_offset'] / 3600).removesuffix('.0'))
),
Label(
f"默认算法设置: {config_var.get()['interface']['global']['algorithm']}",
_("Default algorithm: {algo}").format(algo=config_var.get()['interface']['global']['algorithm']),
),
classes="left",
),
Vertical(
Label(f"已加载 {len(self.repos)} 个单元集"),
Label(_("Loaded {n} repo(s)").format(n=len(self.repos))),
Label(
f"共计 {reduce(lambda x, y: x + y, map(lambda x: x.progress['total'], self.repos)) if self.repos else 0} 个单元"
_("Total {n} unit(s)").format(n=reduce(lambda x, y: x + y, map(lambda x: x.progress['total'], self.repos)) if self.repos else 0)
),
Label(
f"已激活 {reduce(lambda x, y: x + y, map(lambda x: x.progress['touched'], self.repos)) if self.repos else 0} 个单元"
_("Activated {n} unit(s)").format(n=reduce(lambda x, y: x + y, map(lambda x: x.progress['touched'], self.repos)) if self.repos else 0)
),
Label(f""),
classes="right",
),
id="header",
)
yield ListView(id="repo_list", classes="repo-list") # 单元集选择
yield ListView(id="repo_list", classes="repo-list")
from heurams.services.attic import Attic
a = Attic("ana", {"totaltime": 0, "openpuzzles": 0, "puzzles_err": 0})
yield Label(f"版本 {version.ver}-{version.stage}") # 版本信息
yield Label(_("Version {ver}-{stage}").format(ver=version.ver, stage=version.stage))
yield Label(
f"{round(a.data['totaltime'], 2)} 秒内处理了 {a.data['openpuzzles']} 个谜题, 正确率{'无法求解' if not a.data['openpuzzles'] else ' ' + str(round(100 * (1 - a.data['puzzles_err']/a.data['openpuzzles']), 2)) + '%'}, 平均速度{'无法求解' if not a.data['totaltime'] else ' ' + str(round(a.data['openpuzzles']/a.data['totaltime'], 2)) + ' 个每秒'}",
_("Processed {puzzles} puzzles in {time}s, accuracy {accuracy}, speed {speed} puzzle(s)/s").format(
puzzles=a.data['openpuzzles'],
time=round(a.data['totaltime'], 2),
accuracy=_("N/A") if not a.data['openpuzzles'] else str(round(100 * (1 - a.data['puzzles_err']/a.data['openpuzzles']), 2)) + '%',
speed=_("N/A") if not a.data['totaltime'] else str(round(a.data['openpuzzles']/a.data['totaltime'], 2)),
),
id="analysis",
) # 版本信息
) # Version info
yield Footer()
@on(events.ScreenResume)
@@ -140,9 +146,28 @@ class DashboardScreen(Screen):
repo.preview["review"] += 1
# initial_time = min(initial_time, e.)
repo.need_review = timer.get_daystamp() >= repo.nearest_review_time
repo.prompt = f"""{repo.manifest['title']} \\[{repo.config['algorithm']}]
[d]进度: {repo.progress['touched']}/{repo.progress['total']} ({round(repo.progress['touched']/repo.progress['total']*100, 1)}%)[/d]
[d]{f'需要学习: {repo.preview['review']}R + {repo.preview['new']}U' if repo.need_review else (f"暂未开始: 0R + {repo.preview['new']}U" if not repo.progress['have_activated_ever'] else '无需操作')}[/d]"""
repo.prompt = _(
"""{title} [{algo}]
[d]Progress: {touched}/{total} ({pct}%)[/d]
[d]{status}[/d]"""
).format(
title=repo.manifest["title"],
algo=repo.config["algorithm"],
touched=repo.progress["touched"],
total=repo.progress["total"],
pct=round(repo.progress["touched"] / repo.progress["total"] * 100, 1),
status=(
_("Due: {review}R + {new}U").format(
review=repo.preview["review"], new=repo.preview["new"]
)
if repo.need_review
else (
_("Not started: 0R + {new}U").format(new=repo.preview["new"])
if not repo.progress["have_activated_ever"]
else _("Up to date")
)
),
)
def on_mount(self) -> None:
"""挂载组件时初始化"""
@@ -160,8 +185,7 @@ class DashboardScreen(Screen):
repo_list_widget.append(
ListItem(
Static(
f"{config_var.get()['global']['paths']['repo']} 中未找到任何单元集仓库目录.\n"
"请导入单元集后重启应用, 或者新建单元集."
_("No repo directories found in {path}.\nPlease import a repo and restart, or create a new one.").format(path=config_var.get()['global']['paths']['repo'])
),
id="not-found",
)
@@ -175,7 +199,7 @@ class DashboardScreen(Screen):
list_item = ListItem(
*[Label(line) for line in r.prompt.splitlines()],
Button(
f"开始学习",
_("Start Learning"),
flat=True,
variant="primary",
id=f"slaunch_repo_{r.manifest['package']}",
@@ -210,7 +234,6 @@ class DashboardScreen(Screen):
def on_button_pressed(self, event: Button.Pressed) -> None:
"""处理按钮点击事件"""
logger.debug(f"event.button.id: {event.button.id}")
if event.button.id.startswith("slaunch_repo_"): # type: ignore
from .preparation import launch
+46 -45
View File
@@ -1,4 +1,4 @@
"""收藏夹管理器界面"""
"""Favorites manager screen"""
import base64
from pathlib import Path
@@ -21,6 +21,7 @@ from textual.widgets import (
from textual import events, on
from heurams.context import config_var
from heurams.i18n import _
from heurams.kernel.repolib import Repo
from heurams.services.favorite_service import FavoriteItem, favorite_manager
from heurams.services.logger import get_logger
@@ -29,12 +30,12 @@ logger = get_logger(__name__)
class FavoriteManagerScreen(Screen):
"""收藏夹管理器屏幕"""
"""Favorites manager screen"""
SUB_TITLE = "收藏夹"
SUB_TITLE = _("Favorites")
BINDINGS = [
("q", "go_back", "返回"),
("q", "go_back", _("Back")),
("d", "toggle_dark", ""),
]
@@ -49,12 +50,12 @@ class FavoriteManagerScreen(Screen):
self._load_favorites()
def _load_favorites(self) -> None:
"""加载收藏列表"""
"""Load favorites list"""
self.favorites = favorite_manager.get_all()
logger.debug("加载 %d 个收藏项", len(self.favorites))
logger.info("Loaded %d favorites", len(self.favorites))
def compose(self) -> ComposeResult:
"""组合界面组件"""
"""Compose UI components"""
if config_var.get()["interface"]["global"]["show_header"]:
yield Header(
@@ -62,10 +63,10 @@ class FavoriteManagerScreen(Screen):
)
with ScrollableContainer(id="favorites-container"):
if not self.favorites:
yield Label("暂无收藏", classes="empty-label")
yield Static("使用 * 键在记忆界面中添加收藏.")
yield Label(_("No favorites"), classes="empty-label")
yield Static(_("Press * in the memorization screen to add favorites."))
else:
yield Label(f"{len(self.favorites)} 个收藏项", classes="count-label")
yield Label(_("Total {n} favorite(s)").format(n=len(self.favorites)), classes="count-label")
yield ListView(id="favorites-list")
yield Footer()
@@ -83,41 +84,41 @@ class FavoriteManagerScreen(Screen):
list_view.append(self._create_favorite_item(fav)) # type: ignore
def _encode_favorite_key(self, repo_path: str, ident: str) -> str:
"""编码仓库路径和标识符为安全的按钮 ID 部分"""
# 使用 \x00 分隔两部分, 然后进行 base64 编码
"""Encode repo path and identifier into a safe button ID part"""
# Use \x00 as separator between the two parts, then base64 encode
combined = f"{repo_path}\x00{ident}"
encoded = base64.urlsafe_b64encode(combined.encode()).decode()
# 去掉填充的等号
# Strip padding
return encoded.rstrip("=")
def _decode_favorite_key(self, key: str) -> tuple[str, str]:
"""解码按钮 ID 部分为仓库路径和标识符"""
# 补全等号以使长度是4的倍数
"""Decode button ID part back to repo path and identifier"""
# Pad to make length multiple of 4
padded = key + "=" * ((4 - len(key) % 4) % 4)
decoded = base64.urlsafe_b64decode(padded.encode()).decode()
repo_path, ident = decoded.split("\x00", 1)
return repo_path, ident
def _create_favorite_item(self, fav: FavoriteItem) -> ListItem:
"""创建收藏项列表项"""
# 尝试获取仓库信息
"""Create a favorite list item"""
# Try to get repo info
repo_info = self._get_repo_info(fav.repo_path, fav)
title = repo_info.get("title", fav.repo_path) if repo_info else fav.repo_path
added_time = self._format_time(fav.added)
# 构建显示文本
# Build display text
display_text = f"{fav.ident}\n"
display_text += f" [d]添加于: {added_time}\n 来自 {title}[/d]"
display_text += _(" [d]Added: {time}\n From {title}[/d]").format(time=added_time, title=title)
if fav.tags:
display_text += f"{', '.join(fav.tags)}"
# 创建安全的按钮 ID
# Create safe button ID
button_key = self._encode_favorite_key(fav.repo_path, fav.ident)
# 创建列表项, 包含移除按钮
# Create list item with remove button
container = Horizontal(
Label(display_text, classes="favorite-content"),
Button(
"移除",
_("Remove"),
id=f"remove-{button_key}",
variant="error",
flat=True,
@@ -128,21 +129,21 @@ class FavoriteManagerScreen(Screen):
return ListItem(container)
def _get_repo_info(self, repo_path: str, fav: FavoriteItem) -> Optional[dict]:
"""获取仓库信息(标题、原子内容预览)"""
"""Get repo info (title, atom content preview)"""
try:
data_repo = Path(config_var.get()["global"]["paths"]["data"]) / "repo"
repo_dir = data_repo / repo_path
if not repo_dir.exists():
logger.warning("仓库目录不存在: %s", repo_dir)
logger.warning("Repo directory does not exist: %s", repo_dir)
return None
repo = Repo.from_repodir(repo_dir)
# 获取原子内容预览
# Get atom content preview
content_preview = ""
payload = repo.payload
# 查找对应 ident 的 payload 条目
# Find the payload entry matching ident
for ident_key, content in payload:
if ident_key == fav.ident:
# 截断过长的内容
# Truncate long content
if isinstance(content, dict) and "content" in content:
text = content["content"]
else:
@@ -157,53 +158,53 @@ class FavoriteManagerScreen(Screen):
"content_preview": content_preview,
}
except Exception as e:
logger.error("获取仓库信息失败: %s", e)
logger.error("Failed to get repo info: %s", e)
return None
def _format_time(self, timestamp: int) -> str:
"""格式化时间戳"""
"""Format timestamp as datetime string"""
from datetime import datetime
dt = datetime.fromtimestamp(timestamp)
return dt.strftime("%Y-%m-%d %H:%M")
def on_button_pressed(self, event: Button.Pressed) -> None:
"""处理按钮点击事件"""
"""Handle button press event"""
button_id = event.button.id
if button_id and button_id.startswith("remove-"):
# 提取编码后的键
key = button_id[7:] # 去掉 "remove-" 前缀
# Extract encoded key
key = button_id[7:] # Remove "remove-" prefix
try:
repo_path, ident = self._decode_favorite_key(key)
self._remove_favorite(repo_path, ident)
except Exception as e:
logger.error("解析按钮 ID 失败: %s", e)
self.app.notify("操作失败: 无效的按钮标识", severity="error")
logger.error("Failed to parse button ID: %s", e)
self.app.notify(_("Operation failed: invalid button identifier"), severity="error")
def _remove_favorite(self, repo_path: str, ident: str) -> None:
"""移除收藏项"""
"""Remove a favorite item"""
if favorite_manager.remove(repo_path, ident):
self.app.notify(f"已移除收藏: {ident}", severity="information")
# 重新加载列表
self.app.notify(_("Removed favorite: {ident}").format(ident=ident), severity="information")
# Reload list
self._load_favorites()
# 刷新界面
# Refresh UI
self._refresh_list()
else:
self.app.notify(f"移除失败: {ident}", severity="error")
self.app.notify(_("Failed to remove: {ident}").format(ident=ident), severity="error")
def _refresh_list(self) -> None:
"""刷新列表显示"""
"""Refresh the list display"""
container = self.query_one("#favorites-container")
# 清空容器
# Clear container
for child in container.children:
child.remove()
# 重新组合
# Re-compose
if not self.favorites:
container.mount(Label("暂无收藏", classes="empty-label"))
container.mount(Static("使用 * 键在记忆界面中添加收藏。"))
container.mount(Label(_("No favorites"), classes="empty-label"))
container.mount(Static(_("Press * in the memorization screen to add favorites.")))
else:
container.mount(
Label(f"{len(self.favorites)} 个收藏项", classes="count-label")
Label(_("Total {n} favorite(s)").format(n=len(self.favorites)), classes="count-label")
)
list_view = ListView(id="favorites-list")
container.mount(list_view)
+31 -31
View File
@@ -1,4 +1,4 @@
"""队列式记忆工作界面"""
"""Queue-based memorization screen"""
from pathlib import Path
@@ -12,6 +12,7 @@ from textual import events, on
import heurams.kernel.particles as pt
from heurams.context import config_var, rootdir
from heurams.i18n import _
from heurams.kernel.reactor import *
from heurams.services.favorite_service import favorite_manager
from heurams.services.logger import get_logger
@@ -24,11 +25,11 @@ logger = get_logger(__name__)
class MemScreen(Screen):
BINDINGS = [
("q", "go_back_notif", "返回"),
("p", "prev", "查看上一个"),
("q", "go_back_notif", _("Back")),
("p", "prev", _("Previous")),
("d", "toggle_dark", ""),
("v", "play_voice", "朗读"),
("*", "toggle_favorite", "收藏"),
("v", "play_voice", _("Read Aloud")),
("*", "toggle_favorite", _("Favorite")),
("r", "resume_mark"),
("Q", "go_back"),
("n", "block_prompt"),
@@ -36,12 +37,12 @@ class MemScreen(Screen):
("z", "block_prompt"),
]
SUB_TITLE = "学习中"
SUB_TITLE = _("Learning")
CSS_PATH = rootdir / "interface" / "css" / "screens" / "memoqueue.tcss"
if config_var.get()["interface"]["global"]["quick_pass"]:
BINDINGS.append(("k", "quick_pass", "正确应答"))
BINDINGS.append(("f", "quick_fail", "错误应答"))
BINDINGS.append(("k", "quick_pass", _("Correct")))
BINDINGS.append(("f", "quick_fail", _("Incorrect")))
rating = reactive(-1)
@@ -80,7 +81,7 @@ class MemScreen(Screen):
yield Footer()
def update_state(self):
"""更新状态机"""
"""Update state machine"""
self.procession: Procession = self.router.current_procession() # type: ignore
self.atom: pt.Atom = self.procession.current_atom # type: ignore
@@ -101,17 +102,17 @@ class MemScreen(Screen):
atom=self.atom, alia=puzzle["alia"] # type: ignore
)
except Exception as e:
logger.debug(f"调度展开出错: {e}")
return Static(f"无法生成谜题 {e}")
logger.error(f"Failed to expand puzzle: {e}")
return Static(_("Failed to generate puzzle: {e}").format(e=e))
def _get_progress_text(self):
s = ""
if self.repo is not None:
fav_status = "已收藏" if self._is_current_atom_favorited() else "未收藏"
fav_status = _("Favorited") if self._is_current_atom_favorited() else _("Not favorited")
s += f"[{fav_status}] "
s += f"[{self.procession.process() + 1}/{self.procession.total_length()}] \[{self.procession.route.name}]\n"
s += f"[{self.procession.process() + 1}/{self.procession.total_length()}] \\[{self.procession.route.name}]\n"
if self.procession.cursor - 1 >= 0:
s += f"上一个: [d]{self.procession.atoms[self.procession.cursor - 1]['ident']}[/d]"
s += _("Previous: {ident}").format(ident=f"[d]{self.procession.atoms[self.procession.cursor - 1]['ident']}[/d]")
return s
def update_display(self):
@@ -120,7 +121,7 @@ class MemScreen(Screen):
progress_widget.update(self._get_progress_text()) # type: ignore
def mount_puzzle(self):
"""挂载当前谜题组件"""
"""Mount current puzzle widget"""
if self.procession.route == RouterState.FINISHED:
self.mount_finished_widget()
return
@@ -130,7 +131,7 @@ class MemScreen(Screen):
container.mount(self.puzzle_widget())
def mount_finished_widget(self):
"""挂载已完成组件"""
"""Mount finished widget"""
a = Attic("ana", {"finished": 0})
a.data["finished"] += 1
container = self.query_one("#puzzle_container")
@@ -153,7 +154,7 @@ class MemScreen(Screen):
self.run_worker(self.play_voice, exclusive=True, thread=True)
def play_voice(self):
"""朗读当前内容"""
"""Read current content aloud"""
from pathlib import Path
from heurams.services.audio_service import play_by_path
@@ -161,7 +162,6 @@ class MemScreen(Screen):
path = Path(config_var.get()["global"]["paths"]["data"]) / "cache" / "voice"
path = path / f"{get_md5(self.atom.registry['nucleon']["tts_text"])}.wav"
logger.debug(str(path))
if path.exists():
play_by_path(path)
else:
@@ -205,7 +205,7 @@ class MemScreen(Screen):
if not self.atom.registry["runtime"]["locked"]:
if not self.atom.registry["electron"].is_activated():
self.atom.registry["electron"].activate()
logger.debug(f"激活原子 {self.atom}")
logger.debug(f"Activated atom: {self.atom}")
self.atom.lock(1)
self.atom.minimize(5)
else:
@@ -228,7 +228,7 @@ class MemScreen(Screen):
self.expander = self.procession.get_expander()
def action_go_back_notif(self):
self.notify("确定吗? 按下大写 Q 以返回")
self.notify(_("Are you sure? Press uppercase Q to go back."))
def action_go_back(self):
self.app.pop_screen()
@@ -240,44 +240,44 @@ class MemScreen(Screen):
self.rating = 3
def _get_repo_rel_path(self) -> str:
"""获取仓库相对路径(相对于 data/repo"""
"""Get repo relative path (relative to data/repo)"""
if self.repo is None:
return ""
# self.repo.source 是 Path 对象, 指向仓库目录
# self.repo.source is the Path object pointing to the repo directory
repo_full_path = self.repo.source
data_repo_path = Path(config_var.get()["global"]["paths"]["data"]) / "repo"
try:
rel_path = repo_full_path.relative_to(data_repo_path)
return str(rel_path)
except ValueError:
# 如果不在 data/repo 下, 则返回完整路径(字符串形式)
# If not under data/repo, return the full path as string
return str(repo_full_path)
def _is_current_atom_favorited(self) -> bool:
"""检查当前原子是否已收藏"""
"""Check if current atom is favorited"""
if self.repo is None:
return False
repo_path = self._get_repo_rel_path()
return favorite_manager.has(repo_path, self.atom.ident)
def action_toggle_favorite(self):
"""切换收藏状态"""
"""Toggle favorite status"""
if self.repo is None:
self.app.notify("无法收藏:未关联仓库", severity="error")
self.app.notify(_("Cannot favorite: no repo associated"), severity="error")
return
repo_path = self._get_repo_rel_path()
ident = self.atom.ident
if favorite_manager.has(repo_path, ident):
favorite_manager.remove(repo_path, ident)
self.app.notify(f"已取消收藏:{ident}", severity="information")
self.app.notify(_("Unfavorited: {ident}").format(ident=ident), severity="information")
else:
favorite_manager.add(repo_path, ident)
self.app.notify(f"已收藏:{ident}", severity="information")
# 更新显示(如果需要)
self.app.notify(_("Favorited: {ident}").format(ident=ident), severity="information")
# Update display if needed
self.update_display()
def action_block_prompt(self):
self.app.notify("功能在记忆界面中不可用, 完成或返回后再试", severity="error")
self.app.notify(_("This function is not available during memorization. Please finish or go back first."), severity="error")
def action_resume_mark(self):
from heurams.services.attic import Attic
@@ -286,4 +286,4 @@ class MemScreen(Screen):
a = Attic("ana")
l = a.data["last"]
a.data["last"] = time.time()
self.app.notify(f"时间恢复已修正: {l} -> {a.data['last']}")
self.app.notify(_("Time resume corrected: {old} -> {new}").format(old=l, new=a.data['last']))
+17 -19
View File
@@ -4,6 +4,7 @@ from textual.screen import ModalScreen
from textual.widgets import Button, Label, ListItem, ListView, Static
from heurams.i18n import _
from heurams.services.logger import get_logger
from .favmgr import FavoriteManagerScreen
@@ -12,36 +13,33 @@ logger = get_logger(__name__)
class NavigatorScreen(ModalScreen):
"""导航器模态窗口"""
"""Navigator modal screen"""
BINDINGS = [
("q", "go_back", "返回"),
("escape", "go_back", "返回"),
("n", "go_back", "切换"),
("q", "go_back", _("Back")),
("escape", "go_back", _("Back")),
("n", "go_back", _("Switch")),
]
SCREENS = [
("仪表盘", "dashboard"),
# ("创建仓库", "repo_creator"),
("缓存管理器", "precache_all"),
("收藏夹", FavoriteManagerScreen),
("设置页面", "setting"),
# ("调试日志", "logviewer"),
("同步工具", "synctool"),
("关于此软件", "about"),
# ("仓库编辑器", "repo_editor"),
(_("Dashboard"), "dashboard"),
(_("Cache Manager"), "precache_all"),
(_("Favorites"), FavoriteManagerScreen),
(_("Settings Page"), "setting"),
(_("Sync Tool"), "synctool"),
(_("About"), "about"),
]
OTHERS = [
("退出程序", "self.app.exit()"),
("项目主页", "webbrowser.open('https://ams.pluv27.top')"),
(_("Exit"), "self.app.exit()"),
(_("Project Homepage"), "webbrowser.open('https://ams.pluv27.top')"),
]
def compose(self) -> ComposeResult:
"""组合界面组件"""
"""Compose UI components"""
with Grid(id="dialog"):
yield Label(
"[b]请选择要跳转的功能\n或记忆会话实例[/b]\n\n将在此处显示提示",
_("[b]Select a function to navigate to\nor a memorization session instance[/b]\n\nTips will be displayed here"),
classes="title-label",
)
yield ListView(
@@ -49,9 +47,9 @@ class NavigatorScreen(ModalScreen):
id="nav-list",
classes="nav-list-view",
)
yield Static("按下回车以完成切换\n所有会话将被保存")
yield Static(_("Press Enter to switch\nAll sessions will be saved"))
yield Button(
"关闭 (n)",
_("Close (n)"),
id="close_button",
variant="primary",
classes="close-button",
+65 -57
View File
@@ -1,4 +1,4 @@
"""缓存工具界面"""
"""Cache tool screen"""
import pathlib
@@ -13,14 +13,15 @@ from textual import events, on
import heurams.kernel.particles as pt
import heurams.services.hasher as hasher
from heurams.context import *
from heurams.i18n import _
# 兼容性缓存路径:优先使用 paths.cache, 否则使用 data/cache
# Compatibility cache path: prefer paths.cache, otherwise data/cache
paths = config_var.get()["global"]["paths"]
cache_dir = pathlib.Path(paths.get("cache", paths["data"] + "/cache")) / "voice"
def human_size(bytes_num: int) -> str:
"""将字节数格式化为人类可读的字符串"""
"""Format byte count as human-readable string"""
for unit in ["B", "KB", "MB", "GB", "TB"]:
if bytes_num < 1024.0:
return f"{bytes_num:.2f} {unit}"
@@ -29,18 +30,18 @@ def human_size(bytes_num: int) -> str:
class PrecachingScreen(Screen):
"""预缓存音频文件屏幕
"""Audio file pre-caching screen
缓存记忆单元音频文件, 全部(默认) 或部分记忆单元(可选参数传入)
Cache memory unit audio files, all (default) or some memory units (optional params)
Args:
nucleons (list): 可选列表, 仅包含 Nucleon 对象
desc (list): 可选字符串, 包含对此次调用的文字描述
nucleons (list): Optional list containing Nucleon objects only
desc (list): Optional string containing description of this call
"""
SUB_TITLE = "缓存管理器"
SUB_TITLE = _("Cache Manager")
BINDINGS = [
("q", "go_back", "返回"),
("q", "go_back", _("Back")),
]
def __init__(self, nucleons: list = [], desc: str = ""):
@@ -67,7 +68,7 @@ class PrecachingScreen(Screen):
self._update_cache_stats()
def _get_total_units(self) -> int:
"""获取所有仓库的总单元数"""
"""Get total units across all repos"""
from heurams.context import config_var
from heurams.kernel.repolib import Repo
@@ -89,7 +90,7 @@ class PrecachingScreen(Screen):
shim.set_term_title(f"{self.app.TITLE} - {self.SUB_TITLE}")
def _update_cache_stats(self) -> None:
"""更新缓存统计信息"""
"""Update cache statistics"""
total_size = 0
file_count = 0
cached_units = 0
@@ -117,21 +118,25 @@ class PrecachingScreen(Screen):
show_clock=config_var.get()["interface"]["global"]["clock_on_header"]
)
with ScrollableContainer(id="precache_container"):
yield Label("[b]音频预缓存[/b]", classes="title-label")
yield Label(_("[b]Audio Pre-cache[/b]"), classes="title-label")
with Container():
yield Static(
f"缓存率: {self.cache_stats.get('cache_rate', 0):.1f}% (已缓存 {self.cache_stats.get('cached_units', 0)} / {self.cache_stats.get('total_units', 0)} 个单元)",
_("Cache rate: {rate:.1f}% ({cached} / {total} units)").format(
rate=self.cache_stats.get('cache_rate', 0),
cached=self.cache_stats.get('cached_units', 0),
total=self.cache_stats.get('total_units', 0),
),
classes="cache-usage-text",
)
if self.nucleons:
yield Static(
f"目标单元归属: [b]{self.desc}[/b]", classes="target-info"
_("Target units from: [b]{desc}[/b]").format(desc=self.desc), classes="target-info"
)
yield Static(
f"单元数量: {len(self.nucleons)}", classes="target-info"
_("Unit count: {n}").format(n=len(self.nucleons)), classes="target-info"
)
else:
yield Static("目标: 所有单元", classes="target-info")
yield Static(_("Target: all units"), classes="target-info")
yield Static(id="status", classes="status-info")
yield Static(id="current_item", classes="current-item")
@@ -139,69 +144,72 @@ class PrecachingScreen(Screen):
with Horizontal(classes="button-group"):
if not self.is_precaching:
yield Button(
"开始预缓存", id="start_precache", variant="primary"
_("Start Pre-cache"), id="start_precache", variant="primary"
)
else:
yield Button(
"取消预缓存", id="cancel_precache", variant="error"
_("Cancel Pre-cache"), id="cancel_precache", variant="error"
)
yield Button("清空缓存", id="clear_cache", variant="warning")
yield Button("返回", id="go_back", variant="default")
yield Button(_("Clear Cache"), id="clear_cache", variant="warning")
yield Button(_("Back"), id="go_back", variant="default")
with Container(classes="cache-info"):
yield Static(f"缓存路径: {cache_dir}", classes="cache-path")
yield Static(_("Cache path: {path}").format(path=cache_dir), classes="cache-path")
yield Static(
f"文件数: {self.cache_stats['file_count']}", classes="cache-count"
_("Files: {n}").format(n=self.cache_stats['file_count']), classes="cache-count"
)
yield Static(
f"总大小: {self.cache_stats['human_size']}", classes="cache-size"
_("Total size: {size}").format(size=self.cache_stats['human_size']), classes="cache-size"
)
yield Button(
"刷新", id="refresh_cache_stats", variant="default", flat=True
_("Refresh"), id="refresh_cache_stats", variant="default", flat=True
)
yield Static("若您离开此界面, 未完成的缓存进程会自动停止.")
yield Static('缓存程序支持 "断点续传".')
yield Static(_("If you leave this screen, ongoing cache processes will stop automatically."))
yield Static(_('Cache supports "resume from break".'))
yield Footer()
def on_mount(self):
"""挂载时初始化状态"""
self.update_status("就绪", "等待开始...")
"""Initialise state on mount"""
self.update_status(_("Ready"), _("Waiting to start..."))
self._update_cache_display()
def update_status(self, status, current_item="", progress=None):
"""更新状态显示"""
"""Update status display"""
status_widget = self.query_one("#status", Static)
item_widget = self.query_one("#current_item", Static)
progress_bar = self.query_one("#progress_bar", ProgressBar)
status_widget.update(f"状态: {status}")
item_widget.update(f"当前项目: {current_item}" if current_item else "")
status_widget.update(_("Status: {s}").format(s=status))
item_widget.update(_("Current item: {item}").format(item=current_item) if current_item else "")
if progress is not None:
progress_bar.progress = progress
progress_bar.advance(0) # 刷新显示
progress_bar.advance(0) # Refresh display
def _update_cache_display(self) -> None:
"""更新缓存信息显示"""
# 更新统计信息
"""Update cache info display"""
# Update stats
self._update_cache_stats()
# 更新缓存率进度条
# 更新缓存大小和文件数显示
# Update cache rate progress bar
# Update cache size and file count display
cache_count_widget = self.query_one(".cache-count", Static)
cache_size_widget = self.query_one(".cache-size", Static)
cache_usage_text = self.query_one(".cache-usage-text", Static)
if cache_count_widget:
cache_count_widget.update(f"文件数: {self.cache_stats['file_count']}")
cache_count_widget.update(_("Files: {n}").format(n=self.cache_stats['file_count']))
if cache_size_widget:
cache_size_widget.update(f"总大小: {self.cache_stats['human_size']}")
cache_size_widget.update(_("Total size: {size}").format(size=self.cache_stats['human_size']))
if cache_usage_text:
cache_usage_text.update(
f"缓存率: {self.cache_stats.get('cache_rate', 0):.1f}% "
f"(已缓存 {self.cache_stats.get('cached_units', 0)} / {self.cache_stats.get('total_units', 0)} 个单元)"
_("Cache rate: {rate:.1f}% ({cached} / {total} units)").format(
rate=self.cache_stats.get('cache_rate', 0),
cached=self.cache_stats.get('cached_units', 0),
total=self.cache_stats.get('total_units', 0),
)
)
def precache_by_text(self, text: str):
"""预缓存单段文本的音频"""
"""Pre-cache audio for a single text string"""
cache_dir.mkdir(parents=True, exist_ok=True)
cache_file = cache_dir / f"{hasher.get_md5(text)}.wav"
@@ -212,21 +220,21 @@ class PrecachingScreen(Screen):
convertor(text, cache_file)
return 1
except Exception as e:
print(f"预缓存失败 '{text}': {e}")
print(f"Pre-cache failed '{text}': {e}")
return 0
return 1
def precache_by_nucleon(self, nucleon: pt.Nucleon):
"""依据 Nucleon 缓存"""
"""Cache based on Nucleon"""
ret = self.precache_by_text(nucleon["tts_text"])
return ret
def precache_by_list(self, nucleons: list):
"""依据 Nucleons 列表缓存"""
"""Cache based on Nucleons list"""
for idx, nucleon in enumerate(nucleons):
# print(f"PROC: {nucleon}")
worker = get_current_worker()
if worker and worker.is_cancelled: # 函数在worker中执行且已被取消
if worker and worker.is_cancelled: # Function running in worker and has been cancelled
return False
text = nucleon["tts_text"]
# self.current_item = text[:30] + "..." if len(text) > 50 else text
@@ -236,12 +244,12 @@ class PrecachingScreen(Screen):
# print(self.total)
progress = int((self.processed / self.total) * 100) if self.total > 0 else 0
# print(progress)
self.update_status(f"正处理 ({idx + 1}/{len(nucleons)})", text, progress)
self.update_status(_("Processing ({i}/{total})").format(i=idx + 1, total=len(nucleons)), text, progress)
ret = self.precache_by_nucleon(nucleon)
if not ret:
self.update_status(
"出错",
f"处理失败, 跳过: {self.current_item}",
_("Error"),
_("Failed, skipping: {item}").format(item=self.current_item),
)
import time
@@ -286,7 +294,7 @@ class PrecachingScreen(Screen):
def on_button_pressed(self, event: Button.Pressed) -> None:
event.stop()
if event.button.id == "start_precache" and not self.is_precaching:
# 开始预缓存
# Start pre-cache
if self.nucleons:
self.precache_worker = self.run_worker(
self.precache_by_nucleons,
@@ -303,32 +311,32 @@ class PrecachingScreen(Screen):
)
elif event.button.id == "cancel_precache" and self.is_precaching:
# 取消预缓存
# Cancel pre-cache
if self.precache_worker:
self.precache_worker.cancel()
self.is_precaching = False
self.processed = 0
self.progress = 0
self.update_status("已取消", "预缓存操作被用户取消", 0)
self.update_status(_("Cancelled"), _("Pre-cache cancelled by user"), 0)
elif event.button.id == "clear_cache":
# 清空缓存
# Clear cache
try:
import shutil
shutil.rmtree(cache_dir, ignore_errors=True)
self.update_status("已清空", "音频缓存已清空", 0)
self._update_cache_display() # 更新缓存统计显示
self.update_status(_("Cleared"), _("Audio cache cleared"), 0)
self._update_cache_display() # Update cache stats display
except Exception as e:
self.update_status("错误", f"清空缓存失败: {e}")
self.update_status(_("Error"), _("Failed to clear cache: {error}").format(error=e))
self.cancel_flag = 1
self.processed = 0
self.progress = 0
elif event.button.id == "refresh_cache_stats":
# 刷新缓存统计信息
# Refresh cache stats
self._update_cache_display()
self.app.notify("缓存信息已刷新", severity="information")
self.app.notify(_("Cache info refreshed"), severity="information")
elif event.button.id == "go_back":
self.action_go_back()
+23 -20
View File
@@ -1,4 +1,4 @@
"""记忆准备界面"""
"""Memorization preparation screen"""
from textual.app import ComposeResult
from textual.containers import ScrollableContainer, Horizontal
@@ -19,6 +19,7 @@ from textual import events, on
import heurams.kernel.particles as pt
from heurams.context import *
from heurams.context import config_var
from heurams.i18n import _
from heurams.kernel.repolib import *
from heurams.kernel.algorithms import algorithms
from heurams.services.logger import get_logger
@@ -28,11 +29,11 @@ logger = get_logger(__name__)
class PreparationScreen(Screen):
SUB_TITLE = "准备记忆集"
SUB_TITLE = _("Prepare Repository")
BINDINGS = [
("q", "go_back", "返回"),
("p", "precache", "缓存"),
("q", "go_back", _("Back")),
("p", "precache", _("Cache")),
("d", "toggle_dark", ""),
("0,1,2,3", "app.push_screen('about')", ""),
]
@@ -61,29 +62,40 @@ class PreparationScreen(Screen):
)
with ScrollableContainer(id="main_container"):
yield Markdown(
f"**准备就绪**: `{self.repo.manifest['title']}`\n", id="title"
_("**Ready**: `{title}`\n").format(title=self.repo.manifest['title']), id="title"
)
yield Label(f"单元集路径: {self.repo.source}")
yield Label(_("Repo path: {path}").format(path=self.repo.source))
yield Label(
f"学习完成度: {self.repo.progress['touched']}/{len(self.repo)} [d]\\[{round(self.repo.progress['touched']/self.repo.progress['total']*100, 1)}%][/d]"
_("Progress: {touched}/{total} [{pct}%]").format(
touched=self.repo.progress['touched'],
total=len(self.repo),
pct=round(self.repo.progress['touched'] / self.repo.progress['total'] * 100, 1)
)
)
yield Label(
f"调度算法: {self.repo.config["algorithm"]} {algorithms[self.repo.config["algorithm"]].desc}"
_("Scheduling algorithm: {algo} {desc}").format(
algo=self.repo.config["algorithm"],
desc=algorithms[self.repo.config["algorithm"]].desc
)
)
yield Label(
f"学习数量: {self.repo.preview['review'] + self.scheduled_num} = {self.repo.preview['review']} [d][复习][/d] + {self.scheduled_num} [d][新识记][/d]\n",
_("Study count: {total} = {review} [d][Review][/d] + {new} [d][New][/d]\n").format(
total=self.repo.preview['review'] + self.scheduled_num,
review=self.repo.preview['review'],
new=self.scheduled_num,
),
id="schnum_label",
)
yield Horizontal(
Button(
"开始记忆",
_("Start Memorizing"),
id="start_memorizing_button",
variant="primary",
classes="btn",
),
Button(
"管理缓存",
_("Manage Cache"),
id="precache_button",
variant="success",
classes="btn",
@@ -99,14 +111,6 @@ class PreparationScreen(Screen):
yield Static(i, classes="unit-statline")
yield Footer()
# def watch_scheduled_num(self, old_scheduled_num, new_scheduled_num):
# logger.debug("响应", old_scheduled_num, "->", new_scheduled_num)
# try:
# one = self.query_one("#schnum_label")
# one.update(f"单次记忆数量: {new_scheduled_num}") # type: ignore
# except:
# pass
def load_data(self):
self.scheduled_num = self.repo.config["scheduled_num"]
content = ""
@@ -154,7 +158,6 @@ class PreparationScreen(Screen):
def on_button_pressed(self, event: Button.Pressed) -> None:
event.stop()
logger.debug("按下按钮")
if event.button.id == "start_memorizing_button":
launch(repo=self.repo, app=self.app, scheduled_num=self.scheduled_num)
+14 -13
View File
@@ -1,4 +1,4 @@
"""设置页面"""
"""Settings screen"""
from textual.app import ComposeResult
from textual.containers import ScrollableContainer, Horizontal
@@ -16,6 +16,7 @@ from textual.widgets import (
from textual import events, on
from heurams.context import *
from heurams.i18n import _
from heurams.kernel.particles import *
from heurams.kernel.repolib import *
from heurams.services.logger import get_logger
@@ -26,12 +27,12 @@ logger = get_logger(__name__)
class SettingScreen(Screen):
"""设置页面屏幕"""
"""Settings screen"""
SUB_TITLE = "设置"
SUB_TITLE = _("Settings")
BINDINGS = [
("q", "go_back", "返回"),
("s", "go_back", "设置"),
("q", "go_back", _("Back")),
("s", "go_back", _("Settings")),
]
CSS_PATH = rootdir / "interface" / "css" / "screens" / "setting.tcss"
@@ -50,13 +51,13 @@ class SettingScreen(Screen):
shim.set_term_title(f"{self.app.TITLE} - {self.SUB_TITLE}")
def compose(self) -> ComposeResult:
"""组合界面组件"""
"""Compose UI components"""
if config_var.get()["interface"]["global"]["show_header"]:
yield Header(
show_clock=config_var.get()["interface"]["global"]["clock_on_header"]
)
with ScrollableContainer():
yield Label("[b]设置页面[/b]")
yield Label("[b]" + _("Settings") + "[/b]")
for i in config_var.get():
if i.startswith("_"):
continue
@@ -67,7 +68,7 @@ class SettingScreen(Screen):
title=i + f'\n[d]{config_var.get().get(f"_{i}_desc", "")}[/d]',
)
yield Label(
"退出页面时, 所作的更改会立即保存, 但仍建议重启软件以确保新的配置得到应用",
_("Changes are saved immediately when you leave this page, but restart is recommended to ensure the new configuration is applied."),
classes="foot",
)
yield Footer()
@@ -138,7 +139,7 @@ class SettingScreen(Screen):
Label(i + f'\n[d]{parent.get(f"_{i}_desc", "")}[/d]'),
Input(
value=str(parent[i]),
placeholder="要求一个浮点数",
placeholder=_("Requires a float"),
type="number",
id=domize(f"{parent_epath}.{i}"),
),
@@ -151,7 +152,7 @@ class SettingScreen(Screen):
Label(i + f'\n[d]{parent.get(f"_{i}_desc", "")}[/d]'),
Input(
value=parent[i],
placeholder="要求一个字符串",
placeholder=_("Requires a string"),
type="text",
id=domize(f"{parent_epath}.{i}"),
),
@@ -176,7 +177,7 @@ class SettingScreen(Screen):
Label(i + f'\n[d]{parent.get(f"_{i}_desc", "")}[/d]'),
Input(
value=str(parent[i]),
placeholder="要求一个整数",
placeholder=_("Requires an integer"),
type="integer",
id=domize(f"{parent_epath}.{i}"),
),
@@ -186,9 +187,9 @@ class SettingScreen(Screen):
elif isinstance(parent[i], list):
pass
else:
lst.append(Label("未知类型"))
lst.append(Label(_("Unknown type")))
return lst
return [Label("无子项")]
return [Label(_("No sub-items"))]
def on_mount(self) -> None:
"""挂载组件时初始化"""
+93 -83
View File
@@ -1,4 +1,4 @@
"""同步工具界面"""
"""Sync tool screen"""
import pathlib
import time
@@ -12,11 +12,12 @@ from textual.worker import get_current_worker
from textual import events, on
from heurams.context import *
from heurams.i18n import _
class SyncScreen(Screen):
BINDINGS = [("q", "go_back", "返回")]
BINDINGS = [("q", "go_back", _("Back"))]
def __init__(self, nucleons: list = [], desc: str = ""):
super().__init__(name=None, id=None, classes=None)
@@ -39,67 +40,67 @@ class SyncScreen(Screen):
show_clock=config_var.get()["interface"]["global"]["clock_on_header"]
)
with ScrollableContainer(id="sync_container"):
# 标题和连接状态
yield Static("同步工具", classes="title")
# Title and connection status
yield Static(_("Sync Tool"), classes="title")
yield Static("", id="status_label", classes="status")
# 配置信息
yield Static(f"同步协议: {config_var.get()['services']['sync']}")
yield Static("服务器配置:", classes="section_title")
# Config info
yield Static(_("Sync protocol: {proto}").format(proto=config_var.get()['services']['sync']))
yield Static(_("Server Configuration:"), classes="section_title")
with Horizontal(classes="config_info"):
yield Static("远程服务器:", classes="config_label")
yield Static(_("Remote server:"), classes="config_label")
yield Static("", id="server_url", classes="config_value")
with Horizontal(classes="config_info"):
yield Static("远程路径:", classes="config_label")
yield Static(_("Remote path:"), classes="config_label")
yield Static("", id="remote_path", classes="config_value")
with Horizontal(classes="control_buttons"):
yield Button("测试连接", id="test_connection", variant="primary")
yield Button("开始同步", id="start_sync", variant="success")
yield Button("暂停", id="pause_sync", variant="warning", disabled=True)
yield Button("取消", id="cancel_sync", variant="error", disabled=True)
yield Button(_("Test Connection"), id="test_connection", variant="primary")
yield Button(_("Start Sync"), id="start_sync", variant="success")
yield Button(_("Pause"), id="pause_sync", variant="warning", disabled=True)
yield Button(_("Cancel"), id="cancel_sync", variant="error", disabled=True)
yield Static("同步进度", classes="section_title")
yield Static(_("Sync Progress"), classes="section_title")
yield ProgressBar(id="progress_bar", show_percentage=True, total=100)
yield Static("", id="progress_label", classes="progress_text")
yield Static("同步日志", classes="section_title")
yield Static(_("Sync Log"), classes="section_title")
yield Static("", id="log_output", classes="log_output")
yield Footer()
def on_mount(self):
"""挂载时初始化状态"""
"""Initialise state on mount"""
self.update_ui_from_config()
self.log_message("同步工具已启动")
self.log_message(_("Sync tool started"))
def update_ui_from_config(self):
"""更新 UI 显示配置信息"""
"""Update UI with config info"""
try:
sync_cfg: dict = config_var.get()["providers"]["sync"]["webdav"]
# 更新服务器 URL
url = sync_cfg.get("url", "未配置")
# Update server URL
url = sync_cfg.get("url", _("Not configured"))
url_widget = self.query_one("#server_url")
url_widget.update(url) # type: ignore
# 更新远程路径
# Update remote path
remote_path = sync_cfg.get("remote_path", "/")
path_widget = self.query_one("#remote_path")
path_widget.update(remote_path) # type: ignore
# 更新状态标签
# Update status label
status_widget = self.query_one("#status_label")
if self.sync_service and self.sync_service.client:
status_widget.update("同步服务已就绪") # type: ignore
status_widget.update(_("Sync service ready")) # type: ignore
status_widget.add_class("ready")
else:
status_widget.update("同步服务未配置或未启用") # type: ignore
status_widget.update(_("Sync service not configured or not enabled")) # type: ignore
status_widget.add_class("error")
except Exception as e:
self.log_message(f"更新 UI 失败: {e}", is_error=True)
self.log_message(_("Failed to update UI: {error}").format(error=e), is_error=True)
def update_status(self, status, current_item="", progress=None):
"""更新状态显示"""
"""Update status display"""
try:
status_widget = self.query_one("#status_label")
status_widget.update(status) # type: ignore
@@ -112,28 +113,28 @@ class SyncScreen(Screen):
progress_label.update(f"{progress}% - {current_item}" if current_item else f"{progress}%") # type: ignore
except Exception as e:
self.log_message(f"更新状态失败: {e}", is_error=True)
self.log_message(_("Failed to update status: {error}").format(error=e), is_error=True)
def log_message(self, message: str, is_error: bool = False):
"""添加日志消息并更新显示"""
"""Add log message and update display"""
timestamp = time.strftime("%H:%M:%S")
prefix = "[ERROR]" if is_error else "[INFO]"
log_line = f"{timestamp} {prefix} {message}"
self.log_messages.append(log_line)
# 保持日志行数不超过最大值
# Keep log lines under max
if len(self.log_messages) > self.max_log_lines:
self.log_messages = self.log_messages[-self.max_log_lines :]
# 更新日志显示
# Update log display
try:
log_widget = self.query_one("#log_output")
log_widget.update("\n".join(self.log_messages)) # type: ignore
except Exception:
pass # 如果组件未就绪, 忽略错误
pass # Ignore if widget not ready
def on_button_pressed(self, event: Button.Pressed) -> None:
"""处理按钮点击事件"""
"""Handle button press events"""
button_id = event.button.id
if button_id == "test_connection":
@@ -148,124 +149,133 @@ class SyncScreen(Screen):
event.stop()
def test_connection(self):
"""测试 WebDAV 服务器连接"""
"""Test WebDAV server connection"""
if not self.sync_service:
self.log_message("同步服务未初始化, 请检查配置", is_error=True)
self.update_status("同步服务未初始化")
self.log_message(_("Sync service not initialised, please check configuration"), is_error=True)
self.update_status(_("Sync service not initialised"))
return
self.log_message("正在测试 WebDAV 连接...")
self.update_status("正在测试连接...")
self.log_message(_("Testing WebDAV connection..."))
self.update_status(_("Testing connection..."))
try:
success = self.sync_service.test_connection()
if success:
self.log_message("连接测试成功")
self.update_status("连接正常")
self.log_message(_("Connection test successful"))
self.update_status(_("Connection OK"))
else:
self.log_message("连接测试失败", is_error=True)
self.update_status("连接失败")
self.log_message(_("Connection test failed"), is_error=True)
self.update_status(_("Connection failed"))
except Exception as e:
self.log_message(f"连接测试异常: {e}", is_error=True)
self.update_status("连接异常")
self.log_message(_("Connection test error: {error}").format(error=e), is_error=True)
self.update_status(_("Connection error"))
def start_sync(self):
"""开始同步"""
"""Start syncing"""
if not self.sync_service:
self.log_message("同步服务未初始化, 无法开始同步", is_error=True)
self.log_message(_("Sync service not initialised, cannot start sync"), is_error=True)
return
if self.is_syncing:
self.log_message("同步已在进行中", is_error=True)
self.log_message(_("Sync already in progress"), is_error=True)
return
self.is_syncing = True
self.is_paused = False
self.update_button_states()
self.log_message("开始同步数据...")
self.update_status("正在同步...", progress=0)
self.log_message(_("Starting data sync..."))
self.update_status(_("Syncing..."), progress=0)
# 启动后台同步任务
# Start background sync task
self.run_worker(self.perform_sync, thread=True)
def perform_sync(self):
"""执行同步任务(在后台线程中运行)"""
"""Execute sync task (runs in background thread)"""
worker = get_current_worker()
try:
# 获取需要同步的本地目录
# Get local directories to sync
from heurams.context import config_var
config = config_var.get()
paths = config.get("paths", {})
# 同步 nucleon 目录
# Sync nucleon directory
nucleon_dir = pathlib.Path(paths.get("nucleon_dir", "./data/nucleon"))
if nucleon_dir.exists():
self.log_message(f"同步 nucleon 目录: {nucleon_dir}")
self.update_status(f"同步 nucleon 目录...", progress=10)
self.log_message(_("Syncing nucleon directory: {dir}").format(dir=nucleon_dir))
self.update_status(_("Syncing nucleon directory..."), progress=10)
result = self.sync_service.sync_directory(nucleon_dir) # type: ignore
if result.get("success"):
self.log_message(
f"nucleon 同步完成: 上传 {result.get('uploaded', 0)} 个, 下载 {result.get('downloaded', 0)}"
_("nucleon sync complete: uploaded {up}, downloaded {down}").format(
up=result.get('uploaded', 0),
down=result.get('downloaded', 0),
)
)
else:
self.log_message(
f"nucleon 同步失败: {result.get('error', '未知错误')}",
_("nucleon sync failed: {err}").format(err=result.get('error', _('Unknown error'))),
is_error=True,
)
# 同步 electron 目录
# Sync electron directory
electron_dir = pathlib.Path(paths.get("electron_dir", "./data/electron"))
if electron_dir.exists():
self.log_message(f"同步 electron 目录: {electron_dir}")
self.update_status(f"同步 electron 目录...", progress=60)
self.log_message(_("Syncing electron directory: {dir}").format(dir=electron_dir))
self.update_status(_("Syncing electron directory..."), progress=60)
result = self.sync_service.sync_directory(electron_dir) # type: ignore
if result.get("success"):
self.log_message(
f"electron 同步完成: 上传 {result.get('uploaded', 0)} 个, 下载 {result.get('downloaded', 0)}"
_("electron sync complete: uploaded {up}, downloaded {down}").format(
up=result.get('uploaded', 0),
down=result.get('downloaded', 0),
)
)
else:
self.log_message(
f"electron 同步失败: {result.get('error', '未知错误')}",
_("electron sync failed: {err}").format(err=result.get('error', _('Unknown error'))),
is_error=True,
)
# 同步 orbital 目录(如果存在)
# Sync orbital directory (if exists)
orbital_dir = pathlib.Path(paths.get("orbital_dir", "./data/orbital"))
if orbital_dir.exists():
self.log_message(f"同步 orbital 目录: {orbital_dir}")
self.update_status(f"同步 orbital 目录...", progress=80)
self.log_message(_("Syncing orbital directory: {dir}").format(dir=orbital_dir))
self.update_status(_("Syncing orbital directory..."), progress=80)
result = self.sync_service.sync_directory(orbital_dir) # type: ignore
if result.get("success"):
self.log_message(
f"orbital 同步完成: 上传 {result.get('uploaded', 0)} 个, 下载 {result.get('downloaded', 0)}"
_("orbital sync complete: uploaded {up}, downloaded {down}").format(
up=result.get('uploaded', 0),
down=result.get('downloaded', 0),
)
)
else:
self.log_message(
f"orbital 同步失败: {result.get('error', '未知错误')}",
_("orbital sync failed: {err}").format(err=result.get('error', _('Unknown error'))),
is_error=True,
)
# 同步完成
self.update_status("同步完成", progress=100)
self.log_message("所有目录同步完成")
# Sync complete
self.update_status(_("Sync complete"), progress=100)
self.log_message(_("All directories synced"))
except Exception as e:
self.log_message(f"同步过程中发生错误: {e}", is_error=True)
self.update_status("同步失败")
self.log_message(_("Error during sync: {error}").format(error=e), is_error=True)
self.update_status(_("Sync failed"))
finally:
# 重置同步状态
# Reset sync state
self.is_syncing = False
self.is_paused = False
self.update_button_states() # type: ignore
def pause_sync(self):
"""暂停同步"""
"""Pause sync"""
if not self.is_syncing:
return
@@ -273,14 +283,14 @@ class SyncScreen(Screen):
self.update_button_states()
if self.is_paused:
self.log_message("同步已暂停")
self.update_status("同步已暂停")
self.log_message(_("Sync paused"))
self.update_status(_("Sync paused"))
else:
self.log_message("同步已恢复")
self.update_status("正在同步...")
self.log_message(_("Sync resumed"))
self.update_status(_("Syncing..."))
def cancel_sync(self):
"""取消同步"""
"""Cancel sync"""
if not self.is_syncing:
return
@@ -288,11 +298,11 @@ class SyncScreen(Screen):
self.is_paused = False
self.update_button_states()
self.log_message("同步已取消")
self.update_status("同步已取消")
self.log_message(_("Sync cancelled"))
self.update_status(_("Sync cancelled"))
def update_button_states(self):
"""更新按钮状态"""
"""Update button states"""
try:
start_button = self.query_one("#start_sync")
pause_button = self.query_one("#pause_sync")
@@ -302,14 +312,14 @@ class SyncScreen(Screen):
start_button.disabled = True
pause_button.disabled = False
cancel_button.disabled = False
pause_button.label = "继续" if self.is_paused else "暂停" # type: ignore
pause_button.label = _("Resume") if self.is_paused else _("Pause") # type: ignore
else:
start_button.disabled = False
pause_button.disabled = True
cancel_button.disabled = True
except Exception as e:
self.log_message(f"更新按钮状态失败: {e}", is_error=True)
self.log_message(_("Failed to update button state: {error}").format(error=e), is_error=True)
def action_go_back(self):
self.app.pop_screen()
+20 -19
View File
@@ -5,6 +5,7 @@ from textual.widgets import Button, Label, Static
import heurams.kernel.particles as pt
from heurams.i18n import _
from .base_puzzle_widget import BasePuzzleWidget
@@ -32,49 +33,49 @@ class BasicEvaluation(BasePuzzleWidget):
class RatingChanged(Message):
def __init__(self, rating: int) -> None:
self.rating = rating # 评分值 (0-5)
self.rating = rating # Rating value (0-5)
super().__init__()
# 反馈映射表
# Feedback mapping
feedback_mapping = {
"feedback_5": {"rating": 5, "text": "完美回想"},
"feedback_4": {"rating": 4, "text": "犹豫后正确"},
"feedback_3": {"rating": 3, "text": "困难地正确"},
"feedback_2": {"rating": 2, "text": "错误但熟悉"},
"feedback_1": {"rating": 1, "text": "错误且不熟"},
"feedback_0": {"rating": 0, "text": "完全空白"},
"feedback_5": {"rating": 5, "text": _("Perfect recall")},
"feedback_4": {"rating": 4, "text": _("Correct after hesitation")},
"feedback_3": {"rating": 3, "text": _("Correct with difficulty")},
"feedback_2": {"rating": 2, "text": _("Wrong but familiar")},
"feedback_1": {"rating": 1, "text": _("Wrong and unfamiliar")},
"feedback_0": {"rating": 0, "text": _("Complete blank")},
}
def compose(self):
# 显示主要内容
# Show main content
yield Label(self.atom.registry["nucleon"]["content"], id="main")
# 显示评估说明(可选)
yield Static("请评估你对这个内容的记忆程度: ", classes="instruction")
# Show instruction (optional)
yield Static(_("Evaluate how well you remember this content: "), classes="instruction")
# 按钮容器
# Button container
with ScrollableContainer(id="button_container"):
btn = {}
btn["5"] = Button(
"完美回想", variant="success", id="feedback_5", classes="choice"
_("Perfect recall"), variant="success", id="feedback_5", classes="choice"
)
btn["4"] = Button(
"犹豫后正确", variant="success", id="feedback_4", classes="choice"
_("Correct after hesitation"), variant="success", id="feedback_4", classes="choice"
)
btn["3"] = Button(
"困难地正确", variant="warning", id="feedback_3", classes="choice"
_("Correct with difficulty"), variant="warning", id="feedback_3", classes="choice"
)
btn["2"] = Button(
"错误但熟悉", variant="warning", id="feedback_2", classes="choice"
_("Wrong but familiar"), variant="warning", id="feedback_2", classes="choice"
)
btn["1"] = Button(
"错误且不熟", variant="error", id="feedback_1", classes="choice"
_("Wrong and unfamiliar"), variant="error", id="feedback_1", classes="choice"
)
btn["0"] = Button(
"完全空白", variant="error", id="feedback_0", classes="choice"
_("Complete blank"), variant="error", id="feedback_0", classes="choice"
)
# 布局按钮
# Layout buttons
yield Horizontal(btn["5"], btn["4"])
yield Horizontal(btn["3"], btn["2"])
yield Horizontal(btn["1"], btn["0"])
@@ -10,6 +10,7 @@ from textual.events import Key
import heurams.kernel.particles as pt
import heurams.kernel.puzzles as pz
from heurams.services.hasher import hash
from heurams.i18n import _
from heurams.services.logger import get_logger
from .base_puzzle_widget import BasePuzzleWidget
@@ -79,7 +80,6 @@ class ClozePuzzle(BasePuzzleWidget):
c += 1
self.hashmap[h] = i
btnid = f"sel000-{h}"
logger.debug(f"建立按钮 {btnid}")
self.btn_shortcuts[f"{c}"] = btnid
btns.append(Button(f"{i}", id=f"{btnid}", classes="cloze-option-btn"))
for i in range((len(btns) + 1) // 2):
@@ -89,7 +89,7 @@ class ClozePuzzle(BasePuzzleWidget):
yield btns[i]
s.focus()
yield Button("退格", id="delete")
yield Button(_("Backspace"), id="delete")
self.btn_shortcuts[f"0"] = "delete"
self.btn_shortcuts[f"backspace"] = "delete"
self.btn_shortcuts[f"delete"] = "delete"
+5 -3
View File
@@ -1,6 +1,8 @@
from textual.widget import Widget
from textual.widgets import Button, Label
from heurams.i18n import _
class Finished(Widget):
def __init__(
@@ -26,9 +28,9 @@ class Finished(Widget):
)
def compose(self):
yield Label("本次记忆进程结束", id="finished_msg")
yield Label(f"算法数据{'已保存' if self.is_saved else "未能保存"}")
yield Button("返回上一级", flat=True, id="back-to-menu")
yield Label(_("This memorization session is finished"), id="finished_msg")
yield Label(_("Algorithm data {}").format(_("saved") if self.is_saved else _("not saved")))
yield Button(_("Back to Menu"), flat=True, id="back-to-menu")
def on_button_pressed(self, event):
button_id = event.button.id
+10 -18
View File
@@ -1,4 +1,4 @@
# 单项选择题
# Multiple-choice puzzle
from typing import TypedDict
from textual.containers import ScrollableContainer
@@ -8,6 +8,7 @@ from textual.widgets import Button, Label
import heurams.kernel.particles as pt
import heurams.kernel.puzzles as pz
from heurams.services.hasher import hash
from heurams.i18n import _
from heurams.services.logger import get_logger
from textual.events import Key
from .base_puzzle_widget import BasePuzzleWidget
@@ -65,15 +66,10 @@ class MCQPuzzle(BasePuzzleWidget):
def compose(self):
setting: Setting = self.atom.registry["nucleon"]["puzzles"][self.alia]
if len(self.inputlist) > len(self.puzzle.options):
logger.debug("ERR IDX")
logger.debug(self.inputlist)
logger.debug(self.puzzle.options)
else:
current_options = self.puzzle.options[len(self.inputlist)]
yield Label(setting["primary"], id="sentence")
yield Label(self.puzzle.wording[len(self.inputlist)], id="puzzle")
yield Label(f"当前输入: {self.inputlist}", id="inputpreview")
current_options = self.puzzle.options[len(self.inputlist)]
yield Label(setting["primary"], id="sentence")
yield Label(self.puzzle.wording[len(self.inputlist)], id="puzzle")
yield Label(_("Current input: {input}").format(input=self.inputlist), id="inputpreview")
# 渲染当前问题的选项
c = 0
@@ -85,21 +81,19 @@ class MCQPuzzle(BasePuzzleWidget):
h = str(hash(i))
self.hashmap[h] = i
btnid = f"sel{str(self.cursor).zfill(3)}-{h}"
logger.debug(f"建立按钮 {btnid}")
self.btn_shortcuts[f"{c}"] = f"{btnid}"
yield Button(f"[{c}] " + i, id=f"{btnid}")
s.focus()
yield Button("退格", id="delete")
yield Button(_("Backspace"), id="delete")
self.btn_shortcuts["0"] = f"delete"
self.btn_shortcuts["delete"] = f"delete"
self.btn_shortcuts["backspace"] = f"delete"
def update_display(self, error=0):
# 更新预览标签
# Update preview label
preview = self.query_one("#inputpreview")
preview.update(f"当前输入: {self.inputlist}") # type: ignore
logger.debug("已经更新预览标签")
preview.update(_("Current input: {input}").format(input=self.inputlist)) # type: ignore
# 更新问题标签
puzzle_label = self.query_one("#puzzle")
current_question_index = len(self.inputlist)
@@ -122,7 +116,7 @@ class MCQPuzzle(BasePuzzleWidget):
# 选项选择处理
answer_text = self.hashmap[button_id[7:]] # type: ignore
self.inputlist.append(answer_text)
logger.debug(f"{self.inputlist}")
logger.debug(f"Input list: {self.inputlist}")
# 检查是否完成所有题目
if len(self.inputlist) >= len(self.puzzle.answer):
is_correct = self.inputlist == self.puzzle.answer
@@ -143,7 +137,6 @@ class MCQPuzzle(BasePuzzleWidget):
def refresh_buttons(self):
"""刷新按钮显示(用于题目切换)"""
# 移除所有选项按钮
logger.debug("刷新按钮")
self.cursor += 1
container = self.query_one("#btn-container")
buttons_to_remove = [
@@ -153,7 +146,6 @@ class MCQPuzzle(BasePuzzleWidget):
]
container.focus()
for button in buttons_to_remove:
logger.info(button)
container.remove_children("#" + button.id) # type: ignore
# 添加当前题目的选项按钮
+4 -2
View File
@@ -1,6 +1,8 @@
from textual.widget import Widget
from textual.widgets import Button, Label
from heurams.i18n import _
class Placeholder(Widget):
def __init__(
@@ -23,8 +25,8 @@ class Placeholder(Widget):
)
def compose(self):
yield Label("示例标签", id="testlabel")
yield Button("示例按钮", id="testbtn", classes="choice")
yield Label(_("Sample Label"), id="testlabel")
yield Button(_("Sample Button"), id="testbtn", classes="choice")
def on_button_pressed(self, event):
pass
+3 -2
View File
@@ -6,6 +6,7 @@ from textual.widget import Widget
from textual.widgets import Button, Label, Markdown, Static
import heurams.kernel.particles as pt
from heurams.i18n import _
from heurams.services.logger import get_logger
from .base_puzzle_widget import BasePuzzleWidget
@@ -86,7 +87,7 @@ class Recognition(BasePuzzleWidget):
for item in cfg["secondary"]:
if isinstance(item, list):
for j in item:
yield Markdown(f"### 笔记: {j}") # TODO ANNOTATION
yield Markdown(_("### Note: {note}").format(note=j)) # TODO ANNOTATION
continue
if isinstance(item, Dict):
total = ""
@@ -97,7 +98,7 @@ class Recognition(BasePuzzleWidget):
yield Markdown(item)
with Center() as c:
with Button("我已知晓", id="ok") as b:
with Button(_("I know this"), id="ok") as b:
b.focus()
def on_button_pressed(self, event: Button.Pressed) -> None: