fix: 改进与日志简化
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 解释器版本"""
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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']))
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
"""挂载组件时初始化"""
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
# 添加当前题目的选项按钮
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user