fix: 改进与日志简化
This commit is contained in:
+2
-2
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Welcome and thank you for supporting this project!
|
Welcome and thank you for supporting this project!
|
||||||
|
|
||||||
Currently, the primary project repository server is the <a href="https://git.pluv27.top/pluv/HeurAMS" target="_blank" rel="noopener noreferrer">author's Gitea instance</a>, which manages synchronization, ensures availability, and accepts collaboration from multiple communities. Mirror syncs are set up on <a href="https://github.com/pluvium27/HeurAMS" target="_blank" rel="noopener noreferrer">GitHub</a>, <a href="https://invent.kde.org/pluv/HeurAMS" target="_blank" rel="noopener noreferrer">KDE Invent</a>, and <a href="https://gitee.com/pluv/HeurAMS" target="_blank" rel="noopener noreferrer">Gitee</a>.
|
Currently, the primary project repository server is <a href="https://git.pluv27.top/pluv/HeurAMS" target="_blank" rel="noopener noreferrer">git.pluv27.top</a>, which manages synchronization, ensures availability, and accepts collaboration from multiple communities. Mirror syncs are set up on <a href="https://github.com/pluvium27/HeurAMS" target="_blank" rel="noopener noreferrer">GitHub</a>, <a href="https://invent.kde.org/pluv/HeurAMS" target="_blank" rel="noopener noreferrer">KDE Invent</a>, and <a href="https://gitee.com/pluv/HeurAMS" target="_blank" rel="noopener noreferrer">Gitee</a>.
|
||||||
|
|
||||||
This does not affect the project's acceptance of PRs from <a href="https://github.com/pluvium27/HeurAMS" target="_blank" rel="noopener noreferrer">GitHub</a>, <a href="https://invent.kde.org/pluv/HeurAMS" target="_blank" rel="noopener noreferrer">KDE Invent</a>, and <a href="https://gitee.com/pluv/HeurAMS" target="_blank" rel="noopener noreferrer">Gitee</a>. PRs accepted on GitHub, KDE Invent, and Gitee will retain contributor attribution and be synced back to all platforms as-is. Contributions on any platform are welcome.
|
This does not affect the project's acceptance of PRs from <a href="https://github.com/pluvium27/HeurAMS" target="_blank" rel="noopener noreferrer">GitHub</a>, <a href="https://invent.kde.org/pluv/HeurAMS" target="_blank" rel="noopener noreferrer">KDE Invent</a>, and <a href="https://gitee.com/pluv/HeurAMS" target="_blank" rel="noopener noreferrer">Gitee</a>. PRs accepted on GitHub, KDE Invent, and Gitee will retain contributor attribution and be synced back to all platforms as-is. Contributions on any platform are welcome.
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ HeurAMS is designed as a frontend-independent library, meaning:
|
|||||||
|
|
||||||
- If you have developed a usable HeurAMS frontend (e.g., a not-yet-implemented Flutter frontend) and open-sourced it under AGPL-3.0/GPL-3.0, you can contact us to transfer it to the HeurAMS official repository for joint maintenance. You will retain your copyright and can lead development under that repository. :)
|
- If you have developed a usable HeurAMS frontend (e.g., a not-yet-implemented Flutter frontend) and open-sourced it under AGPL-3.0/GPL-3.0, you can contact us to transfer it to the HeurAMS official repository for joint maintenance. You will retain your copyright and can lead development under that repository. :)
|
||||||
|
|
||||||
- You can also call HeurAMS as an independent process/service in your own projects. Per AGPL-3.0 and this project's additional license terms, if the call occurs on the same host and does not involve external network forwarding, it is exempt from specific obligations of the license and will not be "contaminated" by AGPL-3.0. To this end, we are improving the optional cross-process RPC module, which will become a cross-platform standard component of the "潜进" kernel.
|
- You can also call HeurAMS as an independent process/service in your own projects. Per AGPL-3.0 and this project's additional license terms, if the call occurs on the same host and does not involve external network forwarding, it is exempt from specific obligations of the license and will not be "contaminated" by AGPL-3.0. To this end, we are improving the optional cross-process RPC module, which will become a cross-platform standard component of the HeurAMS kernel.
|
||||||
|
|
||||||
- If you develop another piece of software through independent process/service invocation and open-source it but prefer not to use the AGPL-3.0/GPL-3.0 license, you can also contact us. We would be happy to add your project link to our friendly links.
|
- If you develop another piece of software through independent process/service invocation and open-source it but prefer not to use the AGPL-3.0/GPL-3.0 license, you can also contact us. We would be happy to add your project link to our friendly links.
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ Unlike Anki's SQLite `.apkg` packages, we insist on using human-readable folder-
|
|||||||
- **Cloud sync and sharing optimized**:
|
- **Cloud sync and sharing optimized**:
|
||||||
- Since memory data and unit set files are text files, fast incremental sync is possible without uploading all files, and the design natively supports version control
|
- Since memory data and unit set files are text files, fast incremental sync is possible without uploading all files, and the design natively supports version control
|
||||||
- If you want to share a single file, the software supports exporting as a compressed package or merging into a single text file for sharing on platforms like pastebin
|
- If you want to share a single file, the software supports exporting as a compressed package or merging into a single text file for sharing on platforms like pastebin
|
||||||
- **Performance**: Thanks to the modern, chunked file organization structure, "潜进" achieves agile and low-footprint user experience using only Python while maintaining high flexibility
|
- **Performance**: Thanks to the modern, chunked file organization structure, HeurAMS achieves agile and low-footprint user experience using only Python while maintaining high flexibility
|
||||||
- **AI-assisted friendly**: Imagine you have some `.apkg` decks or a large amount of textbook content — you can conveniently and efficiently use AI tools to create HeurAMS-compatible unit sets
|
- **AI-assisted friendly**: Imagine you have some `.apkg` decks or a large amount of textbook content — you can conveniently and efficiently use AI tools to create HeurAMS-compatible unit sets
|
||||||
|
|
||||||
### Built-in Practical User Interface
|
### Built-in Practical User Interface
|
||||||
@@ -65,7 +65,7 @@ Since some dependencies are only needed by a few features, we split optional dep
|
|||||||
|
|
||||||
## About This Repository
|
## About This Repository
|
||||||
|
|
||||||
This repository is the Python implementation of the HeurAMS "潜进" core library.
|
This repository is the Python implementation of the HeurAMS core library.
|
||||||
It contains data models and framework, and includes a built-in frontend based on the Textual framework (interface submodule).
|
It contains data models and framework, and includes a built-in frontend based on the Textual framework (interface submodule).
|
||||||
Besides learning through the built-in frontend, developers can import the `heurams` library in a Python environment or communicate with `heurams` library instances via `RPC`, using the framework to build other auxiliary memory frontends or applications.
|
Besides learning through the built-in frontend, developers can import the `heurams` library in a Python environment or communicate with `heurams` library instances via `RPC`, using the framework to build other auxiliary memory frontends or applications.
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
#print("欢迎使用 HeurAMS 及其组件!")
|
|
||||||
|
|
||||||
# 补充日志记录
|
|
||||||
from heurams.services.logger import get_logger
|
from heurams.services.logger import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
logger.info("欢迎使用 HeurAMS 及其组件!")
|
logger.info("HeurAMS is imported")
|
||||||
|
|||||||
+74
-35
@@ -1,79 +1,118 @@
|
|||||||
import platform
|
"""命令行入口
|
||||||
|
"""
|
||||||
|
|
||||||
|
import platform
|
||||||
import click
|
import click
|
||||||
|
from heurams.i18n import _, setup_locale
|
||||||
from heurams.services.version import ver, stage, codename, codename_cn
|
from heurams.services.version import ver, stage, codename, codename_cn
|
||||||
|
from heurams.services.logger import get_logger
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
def _apply_locale(ctx, param, value):
|
||||||
|
"""立即显式应用 locale"""
|
||||||
|
if value:
|
||||||
|
setup_locale(value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class _I18nGroup(click.Group):
|
||||||
|
"""在运行时完成翻译工作的命令组"""
|
||||||
|
|
||||||
|
_option_help_map: dict[str, str] = {
|
||||||
|
"version": "Show the version and exit.",
|
||||||
|
"locale": "Explicitly specify locale (defaults to LANG env).",
|
||||||
|
"host": "Listening address",
|
||||||
|
"port": "Listening port",
|
||||||
|
"reload": "Development mode hot reload",
|
||||||
|
}
|
||||||
|
|
||||||
|
def format_help(self, ctx, formatter):
|
||||||
|
# 重新翻译每个子命令的帮助信息
|
||||||
|
for cmd in self.commands.values():
|
||||||
|
raw = getattr(cmd, "_raw_help", None)
|
||||||
|
if raw is not None:
|
||||||
|
cmd.help = _(raw)
|
||||||
|
# Re-translate every option's help text by name lookup.
|
||||||
|
for param in self.params:
|
||||||
|
if isinstance(param, click.Option) and param.name in self._option_help_map:
|
||||||
|
param.help = _(self._option_help_map[param.name])
|
||||||
|
self.help = _("HeurAMS {ver} - Heuristic Auxiliary Memorizing Scheduler").format(
|
||||||
|
ver=ver
|
||||||
|
)
|
||||||
|
return super().format_help(ctx, formatter)
|
||||||
|
|
||||||
|
|
||||||
|
class _I18nCommand(click.Command):
|
||||||
|
"""在运行时完成翻译工作的命令"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self._raw_help = self.help or (self.short_help or "")
|
||||||
|
|
||||||
|
|
||||||
@click.group(
|
@click.group(
|
||||||
|
cls=_I18nGroup,
|
||||||
invoke_without_command=True,
|
invoke_without_command=True,
|
||||||
help=(
|
|
||||||
f"HeurAMS {ver} - 启发式辅助记忆调度器"
|
|
||||||
),
|
|
||||||
context_settings={"help_option_names": ["-h", "--help"]},
|
context_settings={"help_option_names": ["-h", "--help"]},
|
||||||
)
|
)
|
||||||
@click.version_option(
|
@click.version_option(
|
||||||
ver, "-v", "--version",
|
ver, "-v", "--version",
|
||||||
prog_name="HeurAMS",
|
prog_name="HeurAMS",
|
||||||
message=f"%(prog)s %(version)s {stage} ({codename}/{codename_cn}), {platform.system()}",
|
message=f"%(prog)s %(version)s {stage} ({codename}/{codename_cn}), {platform.system()}",
|
||||||
|
help=_I18nGroup._option_help_map["version"],
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--locale", "-l", default=None,
|
||||||
|
callback=_apply_locale,
|
||||||
|
is_eager=True,
|
||||||
|
expose_value=True,
|
||||||
|
help=_I18nGroup._option_help_map["locale"],
|
||||||
)
|
)
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def cli(ctx):
|
def cli(ctx, locale):
|
||||||
if ctx.invoked_subcommand is None:
|
if ctx.invoked_subcommand is None:
|
||||||
click.echo(cli.get_help(ctx))
|
click.echo(cli.get_help(ctx))
|
||||||
ctx.exit(0)
|
ctx.exit(0)
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command(cls=_I18nCommand)
|
||||||
def tui():
|
def tui():
|
||||||
"""启动内置基本用户界面 (TUI)"""
|
"""Launch the built-in user interface (TUI)"""
|
||||||
import heurams.interface.__main__ as tui_module
|
import heurams.interface.__main__ as tui_module
|
||||||
|
|
||||||
tui_module.main()
|
tui_module.main()
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command(cls=_I18nCommand)
|
||||||
@click.option("--host", default="127.0.0.1", help="监听地址")
|
@click.option("--host", default="127.0.0.1", help=_I18nGroup._option_help_map["host"])
|
||||||
@click.option("--port", default=8821, help="监听端口", type=int)
|
@click.option("--port", default=8821, help=_I18nGroup._option_help_map["port"], type=int)
|
||||||
@click.option("--reload", is_flag=True, help="开发模式热重载")
|
@click.option("--reload", is_flag=True, help=_I18nGroup._option_help_map["reload"])
|
||||||
def serve(host, port, reload):
|
def serve(host, port, reload):
|
||||||
"""启动 API 服务 (unifront)"""
|
"""Launch the API service (unifront)"""
|
||||||
from heurams.unifront.server import create_app
|
from heurams.unifront.server import create_app
|
||||||
|
|
||||||
app = create_app()
|
app = create_app()
|
||||||
click.echo(f"unifront API 服务启动: http://{host}:{port}")
|
click.echo(
|
||||||
|
_("unifront API service started: http://{host}:{port}").format(
|
||||||
|
host=host, port=port
|
||||||
|
)
|
||||||
|
)
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
uvicorn.run(app, host=host, port=port, reload=reload, log_level="info")
|
uvicorn.run(app, host=host, port=port, reload=reload, log_level="info")
|
||||||
|
|
||||||
|
|
||||||
def _print_version():
|
@cli.command(cls=_I18nCommand, name="help")
|
||||||
click.echo(
|
|
||||||
f"HeurAMS {ver} ({codename}/{codename_cn}), 阶段: {stage}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
|
||||||
def version():
|
|
||||||
"""输出版本信息"""
|
|
||||||
_print_version()
|
|
||||||
|
|
||||||
|
|
||||||
@cli.command(name="ver", hidden=True)
|
|
||||||
def ver_cmd():
|
|
||||||
"""输出版本信息"""
|
|
||||||
_print_version()
|
|
||||||
|
|
||||||
|
|
||||||
@cli.command(name="help")
|
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def help_cmd(ctx):
|
def help_cmd(ctx):
|
||||||
"""显示此帮助信息"""
|
"""Show this help message"""
|
||||||
click.echo(cli.get_help(ctx.parent))
|
click.echo(cli.get_help(ctx.parent))
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
cli()
|
cli()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
logger.info("HeurAMS cmdline entrypoint invoked")
|
||||||
|
main()
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
"""
|
"""全局上下文模块
|
||||||
全局上下文管理模块
|
|
||||||
以及基准路径
|
初始化并管理基准路径, 程序配置对象, 并提供调试所需上下文管理器
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import pathlib
|
import pathlib
|
||||||
@@ -19,13 +19,14 @@ workdir = pathlib.Path.cwd()
|
|||||||
"""工作目录路径."""
|
"""工作目录路径."""
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
logger.debug(f"包目录: {rootdir}")
|
logger.info(f"rootdir: {rootdir}")
|
||||||
logger.debug(f"工作目录: {workdir}")
|
logger.info(f"workdir: {workdir}")
|
||||||
|
|
||||||
default_data = rootdir / "assets" / "data"
|
default_data = rootdir / "assets" / "data"
|
||||||
user_data = workdir / "data"
|
user_data = workdir / "data"
|
||||||
|
|
||||||
if not user_data.exists():
|
if not user_data.exists():
|
||||||
logger.info("初始化数据目录: %s", user_data)
|
logger.info("Create a new data directory: %s", user_data)
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
shutil.copytree(default_data, user_data)
|
shutil.copytree(default_data, user_data)
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
"""国际化支持模块
|
||||||
|
|
||||||
|
基于 GNU gettext, 并使用英文 (en_US) 作为 msgid 的基础语言.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import gettext
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import locale
|
||||||
|
from heurams.context import rootdir
|
||||||
|
|
||||||
|
_LOCALE_DIR = rootdir / "locale"
|
||||||
|
|
||||||
|
_DEFAULT_LOCALE = "en_US"
|
||||||
|
_translation = gettext.NullTranslations()
|
||||||
|
|
||||||
|
def setup_locale(_locale: str | None = None) -> None:
|
||||||
|
"""获得翻译项.
|
||||||
|
|
||||||
|
如果没有找到对应的 locale, 就回退到原始 msgid.
|
||||||
|
"""
|
||||||
|
global _translation
|
||||||
|
if _locale is None:
|
||||||
|
_locale = locale.getlocale()[0]
|
||||||
|
try:
|
||||||
|
_translation = gettext.translation(
|
||||||
|
"heurams", localedir=str(_LOCALE_DIR), languages=[_locale], fallback=True
|
||||||
|
)
|
||||||
|
except FileNotFoundError:
|
||||||
|
_translation = gettext.NullTranslations()
|
||||||
|
|
||||||
|
|
||||||
|
def _(message: str) -> str:
|
||||||
|
"""执行翻译的函数"""
|
||||||
|
return _translation.gettext(message)
|
||||||
|
|
||||||
|
# 导入时初始化
|
||||||
|
setup_locale()
|
||||||
@@ -2,52 +2,53 @@ from time import sleep, perf_counter
|
|||||||
|
|
||||||
# import gc
|
# import gc
|
||||||
# gc.set_threshold(100, 1, 1)
|
# gc.set_threshold(100, 1, 1)
|
||||||
print("欢迎使用基本用户界面!")
|
from heurams.i18n import _
|
||||||
print("加载配置与上下文... ", end="", flush=True)
|
|
||||||
|
print(_("Welcome to the basic user interface!"))
|
||||||
|
print(_("Loading config and context... "), end="", flush=True)
|
||||||
_start_all = perf_counter()
|
_start_all = perf_counter()
|
||||||
_start = _start_all
|
_start = _start_all
|
||||||
from heurams.context import rootdir, workdir, config_var
|
from heurams.context import rootdir, workdir, config_var
|
||||||
|
|
||||||
_end = perf_counter()
|
_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()
|
_start = perf_counter()
|
||||||
from textual.app import App
|
from textual.app import App
|
||||||
from textual.widgets import Button
|
|
||||||
|
|
||||||
_end = perf_counter()
|
_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()
|
_start = perf_counter()
|
||||||
from .screens.about import AboutScreen
|
|
||||||
from .screens.dashboard import DashboardScreen
|
from .screens.dashboard import DashboardScreen
|
||||||
from .screens.navigator import NavigatorScreen
|
from .screens.navigator import NavigatorScreen
|
||||||
from .screens.precache import PrecachingScreen
|
from .screens.precache import PrecachingScreen
|
||||||
from .screens.setting import SettingScreen
|
from .screens.setting import SettingScreen
|
||||||
from .screens.synctool import SyncScreen
|
from .screens.synctool import SyncScreen
|
||||||
|
from .screens.about import AboutScreen
|
||||||
from . import shim
|
from . import shim
|
||||||
|
|
||||||
_end = perf_counter()
|
_end = perf_counter()
|
||||||
print(f"已完成! (耗时: {round(1000 * (_end - _start))}ms)")
|
print(_("Done! ({time}ms)").format(time=round(1000 * (_end - _start))))
|
||||||
|
|
||||||
print(f"组件目录: {rootdir}")
|
print(_("Component directory: {path}").format(path=rootdir))
|
||||||
print(f"工作目录: {workdir}")
|
print(_("Working directory: {path}").format(path=workdir))
|
||||||
_end_all = perf_counter()
|
_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):
|
class HeurAMSApp(App):
|
||||||
TITLE = "潜进"
|
TITLE = "HeurAMS"
|
||||||
CSS_PATH = rootdir / "interface" / "css" / "main.tcss"
|
CSS_PATH = rootdir / "interface" / "css" / "main.tcss"
|
||||||
SUB_TITLE = "启发式辅助记忆调度器"
|
SUB_TITLE = _("Heuristic Auxiliary Memorizing Scheduler")
|
||||||
BINDINGS = [
|
BINDINGS = [
|
||||||
("q", "go_back", "退出"),
|
("q", "go_back", _("Quit")),
|
||||||
("d", "toggle_dark", "主题"),
|
("d", "toggle_dark", _("Theme")),
|
||||||
("n", "app.push_screen('navigator')", "导航"),
|
("n", "app.push_screen('navigator')", _("Navigate")),
|
||||||
("s", "app.push_screen('setting')", "设置"),
|
("s", "app.push_screen('setting')", _("Settings")),
|
||||||
("z", "app.push_screen('about')", "关于"),
|
("z", "app.push_screen('about')", _("About")),
|
||||||
]
|
]
|
||||||
SCREENS = {
|
SCREENS = {
|
||||||
"dashboard": DashboardScreen,
|
"dashboard": DashboardScreen,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from heurams.interface import *
|
from heurams.interface import *
|
||||||
from heurams.context import config_var
|
from heurams.context import config_var
|
||||||
|
from heurams.i18n import _
|
||||||
from heurams.services.logger import get_logger
|
from heurams.services.logger import get_logger
|
||||||
import threading
|
import threading
|
||||||
import pickle
|
import pickle
|
||||||
@@ -21,7 +22,7 @@ def start_debug_server(app):
|
|||||||
code = pickle.loads(msg)
|
code = pickle.loads(msg)
|
||||||
namespace = {"app": app, "logger": logger, "config_var": config_var}
|
namespace = {"app": app, "logger": logger, "config_var": config_var}
|
||||||
if first:
|
if first:
|
||||||
app.title += " [调试已连接]"
|
app.title += _(" [Debug Connected]")
|
||||||
first = 0
|
first = 0
|
||||||
try:
|
try:
|
||||||
# 先尝试 eval
|
# 先尝试 eval
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""关于界面"""
|
"""About screen"""
|
||||||
|
|
||||||
from textual.app import ComposeResult
|
from textual.app import ComposeResult
|
||||||
from textual.containers import ScrollableContainer
|
from textual.containers import ScrollableContainer
|
||||||
@@ -8,6 +8,7 @@ from textual import events, on
|
|||||||
|
|
||||||
import heurams.services.version as version
|
import heurams.services.version as version
|
||||||
from heurams.context import *
|
from heurams.context import *
|
||||||
|
from heurams.i18n import _
|
||||||
import platform
|
import platform
|
||||||
import shutil
|
import shutil
|
||||||
import os
|
import os
|
||||||
@@ -16,10 +17,10 @@ import sys
|
|||||||
|
|
||||||
class AboutScreen(Screen):
|
class AboutScreen(Screen):
|
||||||
BINDINGS = [
|
BINDINGS = [
|
||||||
("q", "go_back", "返回"),
|
("q", "go_back", _("Back")),
|
||||||
("z", "go_back", "关于"),
|
("z", "go_back", _("About")),
|
||||||
]
|
]
|
||||||
SUB_TITLE = "关于"
|
SUB_TITLE = _("About")
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@@ -37,55 +38,64 @@ class AboutScreen(Screen):
|
|||||||
show_clock=config_var.get()["interface"]["global"]["clock_on_header"]
|
show_clock=config_var.get()["interface"]["global"]["clock_on_header"]
|
||||||
)
|
)
|
||||||
with ScrollableContainer(id="about_container"):
|
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()
|
textual_version = self._get_textual_version()
|
||||||
terminal_info = self._get_terminal_info()
|
terminal_info = self._get_terminal_info()
|
||||||
python_version = self._get_python_version()
|
python_version = self._get_python_version()
|
||||||
os_version = self._get_os_version()
|
os_version = self._get_os_version()
|
||||||
disk_usage = self._get_disk_usage()
|
disk_usage = self._get_disk_usage()
|
||||||
|
|
||||||
about_text = f"""
|
about_text = _(
|
||||||
# 关于 HeurAMS "潜进"
|
"""# About HeurAMS
|
||||||
|
|
||||||
主程序库版本: `{version.ver}-python`
|
Main library version: `{ver}-python`
|
||||||
用户界面分支: `Textual TUI (基本用户界面)`
|
UI frontend: `Textual TUI (Basic UI)`
|
||||||
用户界面版本: `{version.ver}`
|
UI version: `{ver}`
|
||||||
API 版本代号: `{version.codename.capitalize()}`
|
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.
|
||||||
|
|
||||||
开发人员列表:
|
Developers:
|
||||||
- Wang Zhiyu ([@pluvium27](https://github.com/pluvium27)): 项目发起与主要开发者
|
- 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 算法理论
|
- [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 算法与间隔重复理论文献参考
|
- [Jarrett Ye](https://github.com/L-M-Sherlock): FSRS algorithm and spaced repetition theory references
|
||||||
- [Kazuaki Tanida](https://github.com/slaypni): SM-15 算法的 CoffeeScript 逆向实现
|
- [Kazuaki Tanida](https://github.com/slaypni): CoffeeScript reverse implementation of SM-15 algorithm
|
||||||
- [Open Spaced Repetition](https://github.com/open-spaced-repetition): FSRS 算法底层实现
|
- [Open Spaced Repetition](https://github.com/open-spaced-repetition): FSRS algorithm underlying implementation
|
||||||
|
|
||||||
# 运行环境信息
|
# Runtime Environment
|
||||||
|
|
||||||
Python 解释器版本: {python_version}
|
Python interpreter version: {python_version}
|
||||||
Python 解释器路径: {sys.executable}
|
Python interpreter path: {executable}
|
||||||
Textual 框架版本: {textual_version}
|
Textual framework version: {textual_version}
|
||||||
终端模拟器: {terminal_info}
|
Terminal emulator: {terminal_info}
|
||||||
操作系统版本: {os_version}
|
Operating system version: {os_version}
|
||||||
存储余量: {disk_usage}
|
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 Markdown(about_text, classes="about-markdown")
|
||||||
yield Button(
|
yield Button(
|
||||||
"返回主界面",
|
_("Back to Main"),
|
||||||
id="back_button",
|
id="back_button",
|
||||||
variant="primary",
|
variant="primary",
|
||||||
flat=True,
|
flat=True,
|
||||||
@@ -102,22 +112,20 @@ Textual 框架版本: {textual_version}
|
|||||||
self.action_go_back()
|
self.action_go_back()
|
||||||
|
|
||||||
def _get_textual_version(self) -> str:
|
def _get_textual_version(self) -> str:
|
||||||
"""获取 Textual 框架版本"""
|
|
||||||
try:
|
try:
|
||||||
import textual
|
import textual
|
||||||
|
|
||||||
return textual.__version__
|
return textual.__version__
|
||||||
except (ImportError, AttributeError):
|
except (ImportError, AttributeError):
|
||||||
return "未知"
|
return _("Unknown")
|
||||||
|
|
||||||
def _get_terminal_info(self) -> str:
|
def _get_terminal_info(self) -> str:
|
||||||
"""获取终端模拟器信息"""
|
|
||||||
terminal = shutil.which("terminal")
|
terminal = shutil.which("terminal")
|
||||||
if terminal:
|
if terminal:
|
||||||
return terminal
|
return terminal
|
||||||
# 尝试从环境变量获取
|
# Try from environment variables
|
||||||
terminal_env = os.environ.get("TERM_PROGRAM") or os.environ.get("TERM")
|
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:
|
def _get_python_version(self) -> str:
|
||||||
"""获取 Python 解释器版本"""
|
"""获取 Python 解释器版本"""
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""仪表盘界面"""
|
"""Dashboard screen"""
|
||||||
|
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -14,6 +14,7 @@ import heurams.kernel.particles as pt
|
|||||||
import heurams.services.timer as timer
|
import heurams.services.timer as timer
|
||||||
import heurams.services.version as version
|
import heurams.services.version as version
|
||||||
from heurams.context import *
|
from heurams.context import *
|
||||||
|
from heurams.i18n import _
|
||||||
from heurams.kernel.particles import *
|
from heurams.kernel.particles import *
|
||||||
from heurams.kernel.repolib import *
|
from heurams.kernel.repolib import *
|
||||||
from heurams.services.logger import get_logger
|
from heurams.services.logger import get_logger
|
||||||
@@ -25,11 +26,11 @@ logger = get_logger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class DashboardScreen(Screen):
|
class DashboardScreen(Screen):
|
||||||
"""主仪表盘屏幕"""
|
"""Main dashboard screen"""
|
||||||
|
|
||||||
SUB_TITLE = "仪表盘"
|
SUB_TITLE = _("Dashboard")
|
||||||
BINDINGS = [
|
BINDINGS = [
|
||||||
("q", "go_back", "返回"),
|
("q", "go_back", _("Back")),
|
||||||
]
|
]
|
||||||
|
|
||||||
CSS_PATH = rootdir / "interface" / "css" / "screens" / "dashboard.tcss"
|
CSS_PATH = rootdir / "interface" / "css" / "screens" / "dashboard.tcss"
|
||||||
@@ -46,45 +47,50 @@ class DashboardScreen(Screen):
|
|||||||
self._load_data()
|
self._load_data()
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
"""组合界面组件"""
|
"""Compose UI components"""
|
||||||
if config_var.get()["interface"]["global"]["show_header"]:
|
if config_var.get()["interface"]["global"]["show_header"]:
|
||||||
yield Header(
|
yield Header(
|
||||||
show_clock=config_var.get()["interface"]["global"]["clock_on_header"]
|
show_clock=config_var.get()["interface"]["global"]["clock_on_header"]
|
||||||
)
|
)
|
||||||
with ScrollableContainer():
|
with ScrollableContainer():
|
||||||
yield Horizontal( # 顶部的状态
|
yield Horizontal(
|
||||||
Vertical(
|
Vertical(
|
||||||
Label(f"当前日时间戳: {timer.get_daystamp()}"),
|
Label(_("Current daystamp: {ds}").format(ds=timer.get_daystamp())),
|
||||||
Label(
|
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(
|
Label(
|
||||||
f"默认算法设置: {config_var.get()['interface']['global']['algorithm']}",
|
_("Default algorithm: {algo}").format(algo=config_var.get()['interface']['global']['algorithm']),
|
||||||
),
|
),
|
||||||
classes="left",
|
classes="left",
|
||||||
),
|
),
|
||||||
Vertical(
|
Vertical(
|
||||||
Label(f"已加载 {len(self.repos)} 个单元集"),
|
Label(_("Loaded {n} repo(s)").format(n=len(self.repos))),
|
||||||
Label(
|
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(
|
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""),
|
Label(f""),
|
||||||
classes="right",
|
classes="right",
|
||||||
),
|
),
|
||||||
id="header",
|
id="header",
|
||||||
)
|
)
|
||||||
yield ListView(id="repo_list", classes="repo-list") # 单元集选择
|
yield ListView(id="repo_list", classes="repo-list")
|
||||||
from heurams.services.attic import Attic
|
from heurams.services.attic import Attic
|
||||||
|
|
||||||
a = Attic("ana", {"totaltime": 0, "openpuzzles": 0, "puzzles_err": 0})
|
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(
|
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",
|
id="analysis",
|
||||||
) # 版本信息
|
) # Version info
|
||||||
yield Footer()
|
yield Footer()
|
||||||
|
|
||||||
@on(events.ScreenResume)
|
@on(events.ScreenResume)
|
||||||
@@ -140,9 +146,28 @@ class DashboardScreen(Screen):
|
|||||||
repo.preview["review"] += 1
|
repo.preview["review"] += 1
|
||||||
# initial_time = min(initial_time, e.)
|
# initial_time = min(initial_time, e.)
|
||||||
repo.need_review = timer.get_daystamp() >= repo.nearest_review_time
|
repo.need_review = timer.get_daystamp() >= repo.nearest_review_time
|
||||||
repo.prompt = f"""{repo.manifest['title']} \\[{repo.config['algorithm']}]
|
repo.prompt = _(
|
||||||
[d]进度: {repo.progress['touched']}/{repo.progress['total']} ({round(repo.progress['touched']/repo.progress['total']*100, 1)}%)[/d]
|
"""{title} [{algo}]
|
||||||
[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]"""
|
[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:
|
def on_mount(self) -> None:
|
||||||
"""挂载组件时初始化"""
|
"""挂载组件时初始化"""
|
||||||
@@ -160,8 +185,7 @@ class DashboardScreen(Screen):
|
|||||||
repo_list_widget.append(
|
repo_list_widget.append(
|
||||||
ListItem(
|
ListItem(
|
||||||
Static(
|
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",
|
id="not-found",
|
||||||
)
|
)
|
||||||
@@ -175,7 +199,7 @@ class DashboardScreen(Screen):
|
|||||||
list_item = ListItem(
|
list_item = ListItem(
|
||||||
*[Label(line) for line in r.prompt.splitlines()],
|
*[Label(line) for line in r.prompt.splitlines()],
|
||||||
Button(
|
Button(
|
||||||
f"开始学习",
|
_("Start Learning"),
|
||||||
flat=True,
|
flat=True,
|
||||||
variant="primary",
|
variant="primary",
|
||||||
id=f"slaunch_repo_{r.manifest['package']}",
|
id=f"slaunch_repo_{r.manifest['package']}",
|
||||||
@@ -210,7 +234,6 @@ class DashboardScreen(Screen):
|
|||||||
|
|
||||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
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
|
if event.button.id.startswith("slaunch_repo_"): # type: ignore
|
||||||
from .preparation import launch
|
from .preparation import launch
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""收藏夹管理器界面"""
|
"""Favorites manager screen"""
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -21,6 +21,7 @@ from textual.widgets import (
|
|||||||
|
|
||||||
from textual import events, on
|
from textual import events, on
|
||||||
from heurams.context import config_var
|
from heurams.context import config_var
|
||||||
|
from heurams.i18n import _
|
||||||
from heurams.kernel.repolib import Repo
|
from heurams.kernel.repolib import Repo
|
||||||
from heurams.services.favorite_service import FavoriteItem, favorite_manager
|
from heurams.services.favorite_service import FavoriteItem, favorite_manager
|
||||||
from heurams.services.logger import get_logger
|
from heurams.services.logger import get_logger
|
||||||
@@ -29,12 +30,12 @@ logger = get_logger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class FavoriteManagerScreen(Screen):
|
class FavoriteManagerScreen(Screen):
|
||||||
"""收藏夹管理器屏幕"""
|
"""Favorites manager screen"""
|
||||||
|
|
||||||
SUB_TITLE = "收藏夹"
|
SUB_TITLE = _("Favorites")
|
||||||
|
|
||||||
BINDINGS = [
|
BINDINGS = [
|
||||||
("q", "go_back", "返回"),
|
("q", "go_back", _("Back")),
|
||||||
("d", "toggle_dark", ""),
|
("d", "toggle_dark", ""),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -49,12 +50,12 @@ class FavoriteManagerScreen(Screen):
|
|||||||
self._load_favorites()
|
self._load_favorites()
|
||||||
|
|
||||||
def _load_favorites(self) -> None:
|
def _load_favorites(self) -> None:
|
||||||
"""加载收藏列表"""
|
"""Load favorites list"""
|
||||||
self.favorites = favorite_manager.get_all()
|
self.favorites = favorite_manager.get_all()
|
||||||
logger.debug("加载 %d 个收藏项", len(self.favorites))
|
logger.info("Loaded %d favorites", len(self.favorites))
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
"""组合界面组件"""
|
"""Compose UI components"""
|
||||||
|
|
||||||
if config_var.get()["interface"]["global"]["show_header"]:
|
if config_var.get()["interface"]["global"]["show_header"]:
|
||||||
yield Header(
|
yield Header(
|
||||||
@@ -62,10 +63,10 @@ class FavoriteManagerScreen(Screen):
|
|||||||
)
|
)
|
||||||
with ScrollableContainer(id="favorites-container"):
|
with ScrollableContainer(id="favorites-container"):
|
||||||
if not self.favorites:
|
if not self.favorites:
|
||||||
yield Label("暂无收藏", classes="empty-label")
|
yield Label(_("No favorites"), classes="empty-label")
|
||||||
yield Static("使用 * 键在记忆界面中添加收藏.")
|
yield Static(_("Press * in the memorization screen to add favorites."))
|
||||||
else:
|
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 ListView(id="favorites-list")
|
||||||
yield Footer()
|
yield Footer()
|
||||||
|
|
||||||
@@ -83,41 +84,41 @@ class FavoriteManagerScreen(Screen):
|
|||||||
list_view.append(self._create_favorite_item(fav)) # type: ignore
|
list_view.append(self._create_favorite_item(fav)) # type: ignore
|
||||||
|
|
||||||
def _encode_favorite_key(self, repo_path: str, ident: str) -> str:
|
def _encode_favorite_key(self, repo_path: str, ident: str) -> str:
|
||||||
"""编码仓库路径和标识符为安全的按钮 ID 部分"""
|
"""Encode repo path and identifier into a safe button ID part"""
|
||||||
# 使用 \x00 分隔两部分, 然后进行 base64 编码
|
# Use \x00 as separator between the two parts, then base64 encode
|
||||||
combined = f"{repo_path}\x00{ident}"
|
combined = f"{repo_path}\x00{ident}"
|
||||||
encoded = base64.urlsafe_b64encode(combined.encode()).decode()
|
encoded = base64.urlsafe_b64encode(combined.encode()).decode()
|
||||||
# 去掉填充的等号
|
# Strip padding
|
||||||
return encoded.rstrip("=")
|
return encoded.rstrip("=")
|
||||||
|
|
||||||
def _decode_favorite_key(self, key: str) -> tuple[str, str]:
|
def _decode_favorite_key(self, key: str) -> tuple[str, str]:
|
||||||
"""解码按钮 ID 部分为仓库路径和标识符"""
|
"""Decode button ID part back to repo path and identifier"""
|
||||||
# 补全等号以使长度是4的倍数
|
# Pad to make length multiple of 4
|
||||||
padded = key + "=" * ((4 - len(key) % 4) % 4)
|
padded = key + "=" * ((4 - len(key) % 4) % 4)
|
||||||
decoded = base64.urlsafe_b64decode(padded.encode()).decode()
|
decoded = base64.urlsafe_b64decode(padded.encode()).decode()
|
||||||
repo_path, ident = decoded.split("\x00", 1)
|
repo_path, ident = decoded.split("\x00", 1)
|
||||||
return repo_path, ident
|
return repo_path, ident
|
||||||
|
|
||||||
def _create_favorite_item(self, fav: FavoriteItem) -> ListItem:
|
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)
|
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
|
title = repo_info.get("title", fav.repo_path) if repo_info else fav.repo_path
|
||||||
added_time = self._format_time(fav.added)
|
added_time = self._format_time(fav.added)
|
||||||
|
|
||||||
# 构建显示文本
|
# Build display text
|
||||||
display_text = f"{fav.ident}\n"
|
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:
|
if fav.tags:
|
||||||
display_text += f"{', '.join(fav.tags)}"
|
display_text += f"{', '.join(fav.tags)}"
|
||||||
|
|
||||||
# 创建安全的按钮 ID
|
# Create safe button ID
|
||||||
button_key = self._encode_favorite_key(fav.repo_path, fav.ident)
|
button_key = self._encode_favorite_key(fav.repo_path, fav.ident)
|
||||||
# 创建列表项, 包含移除按钮
|
# Create list item with remove button
|
||||||
container = Horizontal(
|
container = Horizontal(
|
||||||
Label(display_text, classes="favorite-content"),
|
Label(display_text, classes="favorite-content"),
|
||||||
Button(
|
Button(
|
||||||
"移除",
|
_("Remove"),
|
||||||
id=f"remove-{button_key}",
|
id=f"remove-{button_key}",
|
||||||
variant="error",
|
variant="error",
|
||||||
flat=True,
|
flat=True,
|
||||||
@@ -128,21 +129,21 @@ class FavoriteManagerScreen(Screen):
|
|||||||
return ListItem(container)
|
return ListItem(container)
|
||||||
|
|
||||||
def _get_repo_info(self, repo_path: str, fav: FavoriteItem) -> Optional[dict]:
|
def _get_repo_info(self, repo_path: str, fav: FavoriteItem) -> Optional[dict]:
|
||||||
"""获取仓库信息(标题、原子内容预览)"""
|
"""Get repo info (title, atom content preview)"""
|
||||||
try:
|
try:
|
||||||
data_repo = Path(config_var.get()["global"]["paths"]["data"]) / "repo"
|
data_repo = Path(config_var.get()["global"]["paths"]["data"]) / "repo"
|
||||||
repo_dir = data_repo / repo_path
|
repo_dir = data_repo / repo_path
|
||||||
if not repo_dir.exists():
|
if not repo_dir.exists():
|
||||||
logger.warning("仓库目录不存在: %s", repo_dir)
|
logger.warning("Repo directory does not exist: %s", repo_dir)
|
||||||
return None
|
return None
|
||||||
repo = Repo.from_repodir(repo_dir)
|
repo = Repo.from_repodir(repo_dir)
|
||||||
# 获取原子内容预览
|
# Get atom content preview
|
||||||
content_preview = ""
|
content_preview = ""
|
||||||
payload = repo.payload
|
payload = repo.payload
|
||||||
# 查找对应 ident 的 payload 条目
|
# Find the payload entry matching ident
|
||||||
for ident_key, content in payload:
|
for ident_key, content in payload:
|
||||||
if ident_key == fav.ident:
|
if ident_key == fav.ident:
|
||||||
# 截断过长的内容
|
# Truncate long content
|
||||||
if isinstance(content, dict) and "content" in content:
|
if isinstance(content, dict) and "content" in content:
|
||||||
text = content["content"]
|
text = content["content"]
|
||||||
else:
|
else:
|
||||||
@@ -157,53 +158,53 @@ class FavoriteManagerScreen(Screen):
|
|||||||
"content_preview": content_preview,
|
"content_preview": content_preview,
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("获取仓库信息失败: %s", e)
|
logger.error("Failed to get repo info: %s", e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _format_time(self, timestamp: int) -> str:
|
def _format_time(self, timestamp: int) -> str:
|
||||||
"""格式化时间戳"""
|
"""Format timestamp as datetime string"""
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
dt = datetime.fromtimestamp(timestamp)
|
dt = datetime.fromtimestamp(timestamp)
|
||||||
return dt.strftime("%Y-%m-%d %H:%M")
|
return dt.strftime("%Y-%m-%d %H:%M")
|
||||||
|
|
||||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
"""处理按钮点击事件"""
|
"""Handle button press event"""
|
||||||
button_id = event.button.id
|
button_id = event.button.id
|
||||||
if button_id and button_id.startswith("remove-"):
|
if button_id and button_id.startswith("remove-"):
|
||||||
# 提取编码后的键
|
# Extract encoded key
|
||||||
key = button_id[7:] # 去掉 "remove-" 前缀
|
key = button_id[7:] # Remove "remove-" prefix
|
||||||
try:
|
try:
|
||||||
repo_path, ident = self._decode_favorite_key(key)
|
repo_path, ident = self._decode_favorite_key(key)
|
||||||
self._remove_favorite(repo_path, ident)
|
self._remove_favorite(repo_path, ident)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("解析按钮 ID 失败: %s", e)
|
logger.error("Failed to parse button ID: %s", e)
|
||||||
self.app.notify("操作失败: 无效的按钮标识", severity="error")
|
self.app.notify(_("Operation failed: invalid button identifier"), severity="error")
|
||||||
|
|
||||||
def _remove_favorite(self, repo_path: str, ident: str) -> None:
|
def _remove_favorite(self, repo_path: str, ident: str) -> None:
|
||||||
"""移除收藏项"""
|
"""Remove a favorite item"""
|
||||||
if favorite_manager.remove(repo_path, ident):
|
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()
|
self._load_favorites()
|
||||||
# 刷新界面
|
# Refresh UI
|
||||||
self._refresh_list()
|
self._refresh_list()
|
||||||
else:
|
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:
|
def _refresh_list(self) -> None:
|
||||||
"""刷新列表显示"""
|
"""Refresh the list display"""
|
||||||
container = self.query_one("#favorites-container")
|
container = self.query_one("#favorites-container")
|
||||||
# 清空容器
|
# Clear container
|
||||||
for child in container.children:
|
for child in container.children:
|
||||||
child.remove()
|
child.remove()
|
||||||
# 重新组合
|
# Re-compose
|
||||||
if not self.favorites:
|
if not self.favorites:
|
||||||
container.mount(Label("暂无收藏", classes="empty-label"))
|
container.mount(Label(_("No favorites"), classes="empty-label"))
|
||||||
container.mount(Static("使用 * 键在记忆界面中添加收藏。"))
|
container.mount(Static(_("Press * in the memorization screen to add favorites.")))
|
||||||
else:
|
else:
|
||||||
container.mount(
|
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")
|
list_view = ListView(id="favorites-list")
|
||||||
container.mount(list_view)
|
container.mount(list_view)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""队列式记忆工作界面"""
|
"""Queue-based memorization screen"""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -12,6 +12,7 @@ from textual import events, on
|
|||||||
|
|
||||||
import heurams.kernel.particles as pt
|
import heurams.kernel.particles as pt
|
||||||
from heurams.context import config_var, rootdir
|
from heurams.context import config_var, rootdir
|
||||||
|
from heurams.i18n import _
|
||||||
from heurams.kernel.reactor import *
|
from heurams.kernel.reactor import *
|
||||||
from heurams.services.favorite_service import favorite_manager
|
from heurams.services.favorite_service import favorite_manager
|
||||||
from heurams.services.logger import get_logger
|
from heurams.services.logger import get_logger
|
||||||
@@ -24,11 +25,11 @@ logger = get_logger(__name__)
|
|||||||
|
|
||||||
class MemScreen(Screen):
|
class MemScreen(Screen):
|
||||||
BINDINGS = [
|
BINDINGS = [
|
||||||
("q", "go_back_notif", "返回"),
|
("q", "go_back_notif", _("Back")),
|
||||||
("p", "prev", "查看上一个"),
|
("p", "prev", _("Previous")),
|
||||||
("d", "toggle_dark", ""),
|
("d", "toggle_dark", ""),
|
||||||
("v", "play_voice", "朗读"),
|
("v", "play_voice", _("Read Aloud")),
|
||||||
("*", "toggle_favorite", "收藏"),
|
("*", "toggle_favorite", _("Favorite")),
|
||||||
("r", "resume_mark"),
|
("r", "resume_mark"),
|
||||||
("Q", "go_back"),
|
("Q", "go_back"),
|
||||||
("n", "block_prompt"),
|
("n", "block_prompt"),
|
||||||
@@ -36,12 +37,12 @@ class MemScreen(Screen):
|
|||||||
("z", "block_prompt"),
|
("z", "block_prompt"),
|
||||||
]
|
]
|
||||||
|
|
||||||
SUB_TITLE = "学习中"
|
SUB_TITLE = _("Learning")
|
||||||
CSS_PATH = rootdir / "interface" / "css" / "screens" / "memoqueue.tcss"
|
CSS_PATH = rootdir / "interface" / "css" / "screens" / "memoqueue.tcss"
|
||||||
|
|
||||||
if config_var.get()["interface"]["global"]["quick_pass"]:
|
if config_var.get()["interface"]["global"]["quick_pass"]:
|
||||||
BINDINGS.append(("k", "quick_pass", "正确应答"))
|
BINDINGS.append(("k", "quick_pass", _("Correct")))
|
||||||
BINDINGS.append(("f", "quick_fail", "错误应答"))
|
BINDINGS.append(("f", "quick_fail", _("Incorrect")))
|
||||||
|
|
||||||
rating = reactive(-1)
|
rating = reactive(-1)
|
||||||
|
|
||||||
@@ -80,7 +81,7 @@ class MemScreen(Screen):
|
|||||||
yield Footer()
|
yield Footer()
|
||||||
|
|
||||||
def update_state(self):
|
def update_state(self):
|
||||||
"""更新状态机"""
|
"""Update state machine"""
|
||||||
self.procession: Procession = self.router.current_procession() # type: ignore
|
self.procession: Procession = self.router.current_procession() # type: ignore
|
||||||
self.atom: pt.Atom = self.procession.current_atom # 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
|
atom=self.atom, alia=puzzle["alia"] # type: ignore
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"调度展开出错: {e}")
|
logger.error(f"Failed to expand puzzle: {e}")
|
||||||
return Static(f"无法生成谜题 {e}")
|
return Static(_("Failed to generate puzzle: {e}").format(e=e))
|
||||||
|
|
||||||
def _get_progress_text(self):
|
def _get_progress_text(self):
|
||||||
s = ""
|
s = ""
|
||||||
if self.repo is not None:
|
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"[{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:
|
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
|
return s
|
||||||
|
|
||||||
def update_display(self):
|
def update_display(self):
|
||||||
@@ -120,7 +121,7 @@ class MemScreen(Screen):
|
|||||||
progress_widget.update(self._get_progress_text()) # type: ignore
|
progress_widget.update(self._get_progress_text()) # type: ignore
|
||||||
|
|
||||||
def mount_puzzle(self):
|
def mount_puzzle(self):
|
||||||
"""挂载当前谜题组件"""
|
"""Mount current puzzle widget"""
|
||||||
if self.procession.route == RouterState.FINISHED:
|
if self.procession.route == RouterState.FINISHED:
|
||||||
self.mount_finished_widget()
|
self.mount_finished_widget()
|
||||||
return
|
return
|
||||||
@@ -130,7 +131,7 @@ class MemScreen(Screen):
|
|||||||
container.mount(self.puzzle_widget())
|
container.mount(self.puzzle_widget())
|
||||||
|
|
||||||
def mount_finished_widget(self):
|
def mount_finished_widget(self):
|
||||||
"""挂载已完成组件"""
|
"""Mount finished widget"""
|
||||||
a = Attic("ana", {"finished": 0})
|
a = Attic("ana", {"finished": 0})
|
||||||
a.data["finished"] += 1
|
a.data["finished"] += 1
|
||||||
container = self.query_one("#puzzle_container")
|
container = self.query_one("#puzzle_container")
|
||||||
@@ -153,7 +154,7 @@ class MemScreen(Screen):
|
|||||||
self.run_worker(self.play_voice, exclusive=True, thread=True)
|
self.run_worker(self.play_voice, exclusive=True, thread=True)
|
||||||
|
|
||||||
def play_voice(self):
|
def play_voice(self):
|
||||||
"""朗读当前内容"""
|
"""Read current content aloud"""
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from heurams.services.audio_service import play_by_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(config_var.get()["global"]["paths"]["data"]) / "cache" / "voice"
|
||||||
path = path / f"{get_md5(self.atom.registry['nucleon']["tts_text"])}.wav"
|
path = path / f"{get_md5(self.atom.registry['nucleon']["tts_text"])}.wav"
|
||||||
logger.debug(str(path))
|
|
||||||
if path.exists():
|
if path.exists():
|
||||||
play_by_path(path)
|
play_by_path(path)
|
||||||
else:
|
else:
|
||||||
@@ -205,7 +205,7 @@ class MemScreen(Screen):
|
|||||||
if not self.atom.registry["runtime"]["locked"]:
|
if not self.atom.registry["runtime"]["locked"]:
|
||||||
if not self.atom.registry["electron"].is_activated():
|
if not self.atom.registry["electron"].is_activated():
|
||||||
self.atom.registry["electron"].activate()
|
self.atom.registry["electron"].activate()
|
||||||
logger.debug(f"激活原子 {self.atom}")
|
logger.debug(f"Activated atom: {self.atom}")
|
||||||
self.atom.lock(1)
|
self.atom.lock(1)
|
||||||
self.atom.minimize(5)
|
self.atom.minimize(5)
|
||||||
else:
|
else:
|
||||||
@@ -228,7 +228,7 @@ class MemScreen(Screen):
|
|||||||
self.expander = self.procession.get_expander()
|
self.expander = self.procession.get_expander()
|
||||||
|
|
||||||
def action_go_back_notif(self):
|
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):
|
def action_go_back(self):
|
||||||
self.app.pop_screen()
|
self.app.pop_screen()
|
||||||
@@ -240,44 +240,44 @@ class MemScreen(Screen):
|
|||||||
self.rating = 3
|
self.rating = 3
|
||||||
|
|
||||||
def _get_repo_rel_path(self) -> str:
|
def _get_repo_rel_path(self) -> str:
|
||||||
"""获取仓库相对路径(相对于 data/repo)"""
|
"""Get repo relative path (relative to data/repo)"""
|
||||||
if self.repo is None:
|
if self.repo is None:
|
||||||
return ""
|
return ""
|
||||||
# self.repo.source 是 Path 对象, 指向仓库目录
|
# self.repo.source is the Path object pointing to the repo directory
|
||||||
repo_full_path = self.repo.source
|
repo_full_path = self.repo.source
|
||||||
data_repo_path = Path(config_var.get()["global"]["paths"]["data"]) / "repo"
|
data_repo_path = Path(config_var.get()["global"]["paths"]["data"]) / "repo"
|
||||||
try:
|
try:
|
||||||
rel_path = repo_full_path.relative_to(data_repo_path)
|
rel_path = repo_full_path.relative_to(data_repo_path)
|
||||||
return str(rel_path)
|
return str(rel_path)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# 如果不在 data/repo 下, 则返回完整路径(字符串形式)
|
# If not under data/repo, return the full path as string
|
||||||
return str(repo_full_path)
|
return str(repo_full_path)
|
||||||
|
|
||||||
def _is_current_atom_favorited(self) -> bool:
|
def _is_current_atom_favorited(self) -> bool:
|
||||||
"""检查当前原子是否已收藏"""
|
"""Check if current atom is favorited"""
|
||||||
if self.repo is None:
|
if self.repo is None:
|
||||||
return False
|
return False
|
||||||
repo_path = self._get_repo_rel_path()
|
repo_path = self._get_repo_rel_path()
|
||||||
return favorite_manager.has(repo_path, self.atom.ident)
|
return favorite_manager.has(repo_path, self.atom.ident)
|
||||||
|
|
||||||
def action_toggle_favorite(self):
|
def action_toggle_favorite(self):
|
||||||
"""切换收藏状态"""
|
"""Toggle favorite status"""
|
||||||
if self.repo is None:
|
if self.repo is None:
|
||||||
self.app.notify("无法收藏:未关联仓库", severity="error")
|
self.app.notify(_("Cannot favorite: no repo associated"), severity="error")
|
||||||
return
|
return
|
||||||
repo_path = self._get_repo_rel_path()
|
repo_path = self._get_repo_rel_path()
|
||||||
ident = self.atom.ident
|
ident = self.atom.ident
|
||||||
if favorite_manager.has(repo_path, ident):
|
if favorite_manager.has(repo_path, ident):
|
||||||
favorite_manager.remove(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:
|
else:
|
||||||
favorite_manager.add(repo_path, ident)
|
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()
|
self.update_display()
|
||||||
|
|
||||||
def action_block_prompt(self):
|
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):
|
def action_resume_mark(self):
|
||||||
from heurams.services.attic import Attic
|
from heurams.services.attic import Attic
|
||||||
@@ -286,4 +286,4 @@ class MemScreen(Screen):
|
|||||||
a = Attic("ana")
|
a = Attic("ana")
|
||||||
l = a.data["last"]
|
l = a.data["last"]
|
||||||
a.data["last"] = time.time()
|
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 textual.widgets import Button, Label, ListItem, ListView, Static
|
||||||
|
|
||||||
|
|
||||||
|
from heurams.i18n import _
|
||||||
from heurams.services.logger import get_logger
|
from heurams.services.logger import get_logger
|
||||||
|
|
||||||
from .favmgr import FavoriteManagerScreen
|
from .favmgr import FavoriteManagerScreen
|
||||||
@@ -12,36 +13,33 @@ logger = get_logger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class NavigatorScreen(ModalScreen):
|
class NavigatorScreen(ModalScreen):
|
||||||
"""导航器模态窗口"""
|
"""Navigator modal screen"""
|
||||||
|
|
||||||
BINDINGS = [
|
BINDINGS = [
|
||||||
("q", "go_back", "返回"),
|
("q", "go_back", _("Back")),
|
||||||
("escape", "go_back", "返回"),
|
("escape", "go_back", _("Back")),
|
||||||
("n", "go_back", "切换"),
|
("n", "go_back", _("Switch")),
|
||||||
]
|
]
|
||||||
|
|
||||||
SCREENS = [
|
SCREENS = [
|
||||||
("仪表盘", "dashboard"),
|
(_("Dashboard"), "dashboard"),
|
||||||
# ("创建仓库", "repo_creator"),
|
(_("Cache Manager"), "precache_all"),
|
||||||
("缓存管理器", "precache_all"),
|
(_("Favorites"), FavoriteManagerScreen),
|
||||||
("收藏夹", FavoriteManagerScreen),
|
(_("Settings Page"), "setting"),
|
||||||
("设置页面", "setting"),
|
(_("Sync Tool"), "synctool"),
|
||||||
# ("调试日志", "logviewer"),
|
(_("About"), "about"),
|
||||||
("同步工具", "synctool"),
|
|
||||||
("关于此软件", "about"),
|
|
||||||
# ("仓库编辑器", "repo_editor"),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
OTHERS = [
|
OTHERS = [
|
||||||
("退出程序", "self.app.exit()"),
|
(_("Exit"), "self.app.exit()"),
|
||||||
("项目主页", "webbrowser.open('https://ams.pluv27.top')"),
|
(_("Project Homepage"), "webbrowser.open('https://ams.pluv27.top')"),
|
||||||
]
|
]
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
"""组合界面组件"""
|
"""Compose UI components"""
|
||||||
with Grid(id="dialog"):
|
with Grid(id="dialog"):
|
||||||
yield Label(
|
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",
|
classes="title-label",
|
||||||
)
|
)
|
||||||
yield ListView(
|
yield ListView(
|
||||||
@@ -49,9 +47,9 @@ class NavigatorScreen(ModalScreen):
|
|||||||
id="nav-list",
|
id="nav-list",
|
||||||
classes="nav-list-view",
|
classes="nav-list-view",
|
||||||
)
|
)
|
||||||
yield Static("按下回车以完成切换\n所有会话将被保存")
|
yield Static(_("Press Enter to switch\nAll sessions will be saved"))
|
||||||
yield Button(
|
yield Button(
|
||||||
"关闭 (n)",
|
_("Close (n)"),
|
||||||
id="close_button",
|
id="close_button",
|
||||||
variant="primary",
|
variant="primary",
|
||||||
classes="close-button",
|
classes="close-button",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""缓存工具界面"""
|
"""Cache tool screen"""
|
||||||
|
|
||||||
import pathlib
|
import pathlib
|
||||||
|
|
||||||
@@ -13,14 +13,15 @@ from textual import events, on
|
|||||||
import heurams.kernel.particles as pt
|
import heurams.kernel.particles as pt
|
||||||
import heurams.services.hasher as hasher
|
import heurams.services.hasher as hasher
|
||||||
from heurams.context import *
|
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"]
|
paths = config_var.get()["global"]["paths"]
|
||||||
cache_dir = pathlib.Path(paths.get("cache", paths["data"] + "/cache")) / "voice"
|
cache_dir = pathlib.Path(paths.get("cache", paths["data"] + "/cache")) / "voice"
|
||||||
|
|
||||||
|
|
||||||
def human_size(bytes_num: int) -> str:
|
def human_size(bytes_num: int) -> str:
|
||||||
"""将字节数格式化为人类可读的字符串"""
|
"""Format byte count as human-readable string"""
|
||||||
for unit in ["B", "KB", "MB", "GB", "TB"]:
|
for unit in ["B", "KB", "MB", "GB", "TB"]:
|
||||||
if bytes_num < 1024.0:
|
if bytes_num < 1024.0:
|
||||||
return f"{bytes_num:.2f} {unit}"
|
return f"{bytes_num:.2f} {unit}"
|
||||||
@@ -29,18 +30,18 @@ def human_size(bytes_num: int) -> str:
|
|||||||
|
|
||||||
|
|
||||||
class PrecachingScreen(Screen):
|
class PrecachingScreen(Screen):
|
||||||
"""预缓存音频文件屏幕
|
"""Audio file pre-caching screen
|
||||||
|
|
||||||
缓存记忆单元音频文件, 全部(默认) 或部分记忆单元(可选参数传入)
|
Cache memory unit audio files, all (default) or some memory units (optional params)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
nucleons (list): 可选列表, 仅包含 Nucleon 对象
|
nucleons (list): Optional list containing Nucleon objects only
|
||||||
desc (list): 可选字符串, 包含对此次调用的文字描述
|
desc (list): Optional string containing description of this call
|
||||||
"""
|
"""
|
||||||
|
|
||||||
SUB_TITLE = "缓存管理器"
|
SUB_TITLE = _("Cache Manager")
|
||||||
BINDINGS = [
|
BINDINGS = [
|
||||||
("q", "go_back", "返回"),
|
("q", "go_back", _("Back")),
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, nucleons: list = [], desc: str = ""):
|
def __init__(self, nucleons: list = [], desc: str = ""):
|
||||||
@@ -67,7 +68,7 @@ class PrecachingScreen(Screen):
|
|||||||
self._update_cache_stats()
|
self._update_cache_stats()
|
||||||
|
|
||||||
def _get_total_units(self) -> int:
|
def _get_total_units(self) -> int:
|
||||||
"""获取所有仓库的总单元数"""
|
"""Get total units across all repos"""
|
||||||
from heurams.context import config_var
|
from heurams.context import config_var
|
||||||
from heurams.kernel.repolib import Repo
|
from heurams.kernel.repolib import Repo
|
||||||
|
|
||||||
@@ -89,7 +90,7 @@ class PrecachingScreen(Screen):
|
|||||||
shim.set_term_title(f"{self.app.TITLE} - {self.SUB_TITLE}")
|
shim.set_term_title(f"{self.app.TITLE} - {self.SUB_TITLE}")
|
||||||
|
|
||||||
def _update_cache_stats(self) -> None:
|
def _update_cache_stats(self) -> None:
|
||||||
"""更新缓存统计信息"""
|
"""Update cache statistics"""
|
||||||
total_size = 0
|
total_size = 0
|
||||||
file_count = 0
|
file_count = 0
|
||||||
cached_units = 0
|
cached_units = 0
|
||||||
@@ -117,21 +118,25 @@ class PrecachingScreen(Screen):
|
|||||||
show_clock=config_var.get()["interface"]["global"]["clock_on_header"]
|
show_clock=config_var.get()["interface"]["global"]["clock_on_header"]
|
||||||
)
|
)
|
||||||
with ScrollableContainer(id="precache_container"):
|
with ScrollableContainer(id="precache_container"):
|
||||||
yield Label("[b]音频预缓存[/b]", classes="title-label")
|
yield Label(_("[b]Audio Pre-cache[/b]"), classes="title-label")
|
||||||
with Container():
|
with Container():
|
||||||
yield Static(
|
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",
|
classes="cache-usage-text",
|
||||||
)
|
)
|
||||||
if self.nucleons:
|
if self.nucleons:
|
||||||
yield Static(
|
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(
|
yield Static(
|
||||||
f"单元数量: {len(self.nucleons)}", classes="target-info"
|
_("Unit count: {n}").format(n=len(self.nucleons)), classes="target-info"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
yield Static("目标: 所有单元", classes="target-info")
|
yield Static(_("Target: all units"), classes="target-info")
|
||||||
|
|
||||||
yield Static(id="status", classes="status-info")
|
yield Static(id="status", classes="status-info")
|
||||||
yield Static(id="current_item", classes="current-item")
|
yield Static(id="current_item", classes="current-item")
|
||||||
@@ -139,69 +144,72 @@ class PrecachingScreen(Screen):
|
|||||||
with Horizontal(classes="button-group"):
|
with Horizontal(classes="button-group"):
|
||||||
if not self.is_precaching:
|
if not self.is_precaching:
|
||||||
yield Button(
|
yield Button(
|
||||||
"开始预缓存", id="start_precache", variant="primary"
|
_("Start Pre-cache"), id="start_precache", variant="primary"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
yield Button(
|
yield Button(
|
||||||
"取消预缓存", id="cancel_precache", variant="error"
|
_("Cancel Pre-cache"), id="cancel_precache", variant="error"
|
||||||
)
|
)
|
||||||
yield Button("清空缓存", id="clear_cache", variant="warning")
|
yield Button(_("Clear Cache"), id="clear_cache", variant="warning")
|
||||||
yield Button("返回", id="go_back", variant="default")
|
yield Button(_("Back"), id="go_back", variant="default")
|
||||||
with Container(classes="cache-info"):
|
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(
|
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(
|
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(
|
yield Button(
|
||||||
"刷新", id="refresh_cache_stats", variant="default", flat=True
|
_("Refresh"), id="refresh_cache_stats", variant="default", flat=True
|
||||||
)
|
)
|
||||||
yield Static("若您离开此界面, 未完成的缓存进程会自动停止.")
|
yield Static(_("If you leave this screen, ongoing cache processes will stop automatically."))
|
||||||
yield Static('缓存程序支持 "断点续传".')
|
yield Static(_('Cache supports "resume from break".'))
|
||||||
|
|
||||||
yield Footer()
|
yield Footer()
|
||||||
|
|
||||||
def on_mount(self):
|
def on_mount(self):
|
||||||
"""挂载时初始化状态"""
|
"""Initialise state on mount"""
|
||||||
self.update_status("就绪", "等待开始...")
|
self.update_status(_("Ready"), _("Waiting to start..."))
|
||||||
self._update_cache_display()
|
self._update_cache_display()
|
||||||
|
|
||||||
def update_status(self, status, current_item="", progress=None):
|
def update_status(self, status, current_item="", progress=None):
|
||||||
"""更新状态显示"""
|
"""Update status display"""
|
||||||
status_widget = self.query_one("#status", Static)
|
status_widget = self.query_one("#status", Static)
|
||||||
item_widget = self.query_one("#current_item", Static)
|
item_widget = self.query_one("#current_item", Static)
|
||||||
progress_bar = self.query_one("#progress_bar", ProgressBar)
|
progress_bar = self.query_one("#progress_bar", ProgressBar)
|
||||||
|
|
||||||
status_widget.update(f"状态: {status}")
|
status_widget.update(_("Status: {s}").format(s=status))
|
||||||
item_widget.update(f"当前项目: {current_item}" if current_item else "")
|
item_widget.update(_("Current item: {item}").format(item=current_item) if current_item else "")
|
||||||
|
|
||||||
if progress is not None:
|
if progress is not None:
|
||||||
progress_bar.progress = progress
|
progress_bar.progress = progress
|
||||||
progress_bar.advance(0) # 刷新显示
|
progress_bar.advance(0) # Refresh display
|
||||||
|
|
||||||
def _update_cache_display(self) -> None:
|
def _update_cache_display(self) -> None:
|
||||||
"""更新缓存信息显示"""
|
"""Update cache info display"""
|
||||||
# 更新统计信息
|
# Update stats
|
||||||
self._update_cache_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_count_widget = self.query_one(".cache-count", Static)
|
||||||
cache_size_widget = self.query_one(".cache-size", Static)
|
cache_size_widget = self.query_one(".cache-size", Static)
|
||||||
cache_usage_text = self.query_one(".cache-usage-text", Static)
|
cache_usage_text = self.query_one(".cache-usage-text", Static)
|
||||||
if cache_count_widget:
|
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:
|
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:
|
if cache_usage_text:
|
||||||
cache_usage_text.update(
|
cache_usage_text.update(
|
||||||
f"缓存率: {self.cache_stats.get('cache_rate', 0):.1f}% "
|
_("Cache rate: {rate:.1f}% ({cached} / {total} units)").format(
|
||||||
f"(已缓存 {self.cache_stats.get('cached_units', 0)} / {self.cache_stats.get('total_units', 0)} 个单元)"
|
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):
|
def precache_by_text(self, text: str):
|
||||||
"""预缓存单段文本的音频"""
|
"""Pre-cache audio for a single text string"""
|
||||||
|
|
||||||
cache_dir.mkdir(parents=True, exist_ok=True)
|
cache_dir.mkdir(parents=True, exist_ok=True)
|
||||||
cache_file = cache_dir / f"{hasher.get_md5(text)}.wav"
|
cache_file = cache_dir / f"{hasher.get_md5(text)}.wav"
|
||||||
@@ -212,21 +220,21 @@ class PrecachingScreen(Screen):
|
|||||||
convertor(text, cache_file)
|
convertor(text, cache_file)
|
||||||
return 1
|
return 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"预缓存失败 '{text}': {e}")
|
print(f"Pre-cache failed '{text}': {e}")
|
||||||
return 0
|
return 0
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
def precache_by_nucleon(self, nucleon: pt.Nucleon):
|
def precache_by_nucleon(self, nucleon: pt.Nucleon):
|
||||||
"""依据 Nucleon 缓存"""
|
"""Cache based on Nucleon"""
|
||||||
ret = self.precache_by_text(nucleon["tts_text"])
|
ret = self.precache_by_text(nucleon["tts_text"])
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def precache_by_list(self, nucleons: list):
|
def precache_by_list(self, nucleons: list):
|
||||||
"""依据 Nucleons 列表缓存"""
|
"""Cache based on Nucleons list"""
|
||||||
for idx, nucleon in enumerate(nucleons):
|
for idx, nucleon in enumerate(nucleons):
|
||||||
# print(f"PROC: {nucleon}")
|
# print(f"PROC: {nucleon}")
|
||||||
worker = get_current_worker()
|
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
|
return False
|
||||||
text = nucleon["tts_text"]
|
text = nucleon["tts_text"]
|
||||||
# self.current_item = text[:30] + "..." if len(text) > 50 else text
|
# self.current_item = text[:30] + "..." if len(text) > 50 else text
|
||||||
@@ -236,12 +244,12 @@ class PrecachingScreen(Screen):
|
|||||||
# print(self.total)
|
# print(self.total)
|
||||||
progress = int((self.processed / self.total) * 100) if self.total > 0 else 0
|
progress = int((self.processed / self.total) * 100) if self.total > 0 else 0
|
||||||
# print(progress)
|
# 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)
|
ret = self.precache_by_nucleon(nucleon)
|
||||||
if not ret:
|
if not ret:
|
||||||
self.update_status(
|
self.update_status(
|
||||||
"出错",
|
_("Error"),
|
||||||
f"处理失败, 跳过: {self.current_item}",
|
_("Failed, skipping: {item}").format(item=self.current_item),
|
||||||
)
|
)
|
||||||
import time
|
import time
|
||||||
|
|
||||||
@@ -286,7 +294,7 @@ class PrecachingScreen(Screen):
|
|||||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
event.stop()
|
event.stop()
|
||||||
if event.button.id == "start_precache" and not self.is_precaching:
|
if event.button.id == "start_precache" and not self.is_precaching:
|
||||||
# 开始预缓存
|
# Start pre-cache
|
||||||
if self.nucleons:
|
if self.nucleons:
|
||||||
self.precache_worker = self.run_worker(
|
self.precache_worker = self.run_worker(
|
||||||
self.precache_by_nucleons,
|
self.precache_by_nucleons,
|
||||||
@@ -303,32 +311,32 @@ class PrecachingScreen(Screen):
|
|||||||
)
|
)
|
||||||
|
|
||||||
elif event.button.id == "cancel_precache" and self.is_precaching:
|
elif event.button.id == "cancel_precache" and self.is_precaching:
|
||||||
# 取消预缓存
|
# Cancel pre-cache
|
||||||
if self.precache_worker:
|
if self.precache_worker:
|
||||||
self.precache_worker.cancel()
|
self.precache_worker.cancel()
|
||||||
self.is_precaching = False
|
self.is_precaching = False
|
||||||
self.processed = 0
|
self.processed = 0
|
||||||
self.progress = 0
|
self.progress = 0
|
||||||
self.update_status("已取消", "预缓存操作被用户取消", 0)
|
self.update_status(_("Cancelled"), _("Pre-cache cancelled by user"), 0)
|
||||||
|
|
||||||
elif event.button.id == "clear_cache":
|
elif event.button.id == "clear_cache":
|
||||||
# 清空缓存
|
# Clear cache
|
||||||
try:
|
try:
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
shutil.rmtree(cache_dir, ignore_errors=True)
|
shutil.rmtree(cache_dir, ignore_errors=True)
|
||||||
self.update_status("已清空", "音频缓存已清空", 0)
|
self.update_status(_("Cleared"), _("Audio cache cleared"), 0)
|
||||||
self._update_cache_display() # 更新缓存统计显示
|
self._update_cache_display() # Update cache stats display
|
||||||
except Exception as e:
|
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.cancel_flag = 1
|
||||||
self.processed = 0
|
self.processed = 0
|
||||||
self.progress = 0
|
self.progress = 0
|
||||||
|
|
||||||
elif event.button.id == "refresh_cache_stats":
|
elif event.button.id == "refresh_cache_stats":
|
||||||
# 刷新缓存统计信息
|
# Refresh cache stats
|
||||||
self._update_cache_display()
|
self._update_cache_display()
|
||||||
self.app.notify("缓存信息已刷新", severity="information")
|
self.app.notify(_("Cache info refreshed"), severity="information")
|
||||||
elif event.button.id == "go_back":
|
elif event.button.id == "go_back":
|
||||||
self.action_go_back()
|
self.action_go_back()
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""记忆准备界面"""
|
"""Memorization preparation screen"""
|
||||||
|
|
||||||
from textual.app import ComposeResult
|
from textual.app import ComposeResult
|
||||||
from textual.containers import ScrollableContainer, Horizontal
|
from textual.containers import ScrollableContainer, Horizontal
|
||||||
@@ -19,6 +19,7 @@ from textual import events, on
|
|||||||
import heurams.kernel.particles as pt
|
import heurams.kernel.particles as pt
|
||||||
from heurams.context import *
|
from heurams.context import *
|
||||||
from heurams.context import config_var
|
from heurams.context import config_var
|
||||||
|
from heurams.i18n import _
|
||||||
from heurams.kernel.repolib import *
|
from heurams.kernel.repolib import *
|
||||||
from heurams.kernel.algorithms import algorithms
|
from heurams.kernel.algorithms import algorithms
|
||||||
from heurams.services.logger import get_logger
|
from heurams.services.logger import get_logger
|
||||||
@@ -28,11 +29,11 @@ logger = get_logger(__name__)
|
|||||||
|
|
||||||
class PreparationScreen(Screen):
|
class PreparationScreen(Screen):
|
||||||
|
|
||||||
SUB_TITLE = "准备记忆集"
|
SUB_TITLE = _("Prepare Repository")
|
||||||
|
|
||||||
BINDINGS = [
|
BINDINGS = [
|
||||||
("q", "go_back", "返回"),
|
("q", "go_back", _("Back")),
|
||||||
("p", "precache", "缓存"),
|
("p", "precache", _("Cache")),
|
||||||
("d", "toggle_dark", ""),
|
("d", "toggle_dark", ""),
|
||||||
("0,1,2,3", "app.push_screen('about')", ""),
|
("0,1,2,3", "app.push_screen('about')", ""),
|
||||||
]
|
]
|
||||||
@@ -61,29 +62,40 @@ class PreparationScreen(Screen):
|
|||||||
)
|
)
|
||||||
with ScrollableContainer(id="main_container"):
|
with ScrollableContainer(id="main_container"):
|
||||||
yield Markdown(
|
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(
|
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(
|
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(
|
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",
|
id="schnum_label",
|
||||||
)
|
)
|
||||||
|
|
||||||
yield Horizontal(
|
yield Horizontal(
|
||||||
Button(
|
Button(
|
||||||
"开始记忆",
|
_("Start Memorizing"),
|
||||||
id="start_memorizing_button",
|
id="start_memorizing_button",
|
||||||
variant="primary",
|
variant="primary",
|
||||||
classes="btn",
|
classes="btn",
|
||||||
),
|
),
|
||||||
Button(
|
Button(
|
||||||
"管理缓存",
|
_("Manage Cache"),
|
||||||
id="precache_button",
|
id="precache_button",
|
||||||
variant="success",
|
variant="success",
|
||||||
classes="btn",
|
classes="btn",
|
||||||
@@ -99,14 +111,6 @@ class PreparationScreen(Screen):
|
|||||||
yield Static(i, classes="unit-statline")
|
yield Static(i, classes="unit-statline")
|
||||||
yield Footer()
|
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):
|
def load_data(self):
|
||||||
self.scheduled_num = self.repo.config["scheduled_num"]
|
self.scheduled_num = self.repo.config["scheduled_num"]
|
||||||
content = ""
|
content = ""
|
||||||
@@ -154,7 +158,6 @@ class PreparationScreen(Screen):
|
|||||||
|
|
||||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
event.stop()
|
event.stop()
|
||||||
logger.debug("按下按钮")
|
|
||||||
if event.button.id == "start_memorizing_button":
|
if event.button.id == "start_memorizing_button":
|
||||||
launch(repo=self.repo, app=self.app, scheduled_num=self.scheduled_num)
|
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.app import ComposeResult
|
||||||
from textual.containers import ScrollableContainer, Horizontal
|
from textual.containers import ScrollableContainer, Horizontal
|
||||||
@@ -16,6 +16,7 @@ from textual.widgets import (
|
|||||||
from textual import events, on
|
from textual import events, on
|
||||||
|
|
||||||
from heurams.context import *
|
from heurams.context import *
|
||||||
|
from heurams.i18n import _
|
||||||
from heurams.kernel.particles import *
|
from heurams.kernel.particles import *
|
||||||
from heurams.kernel.repolib import *
|
from heurams.kernel.repolib import *
|
||||||
from heurams.services.logger import get_logger
|
from heurams.services.logger import get_logger
|
||||||
@@ -26,12 +27,12 @@ logger = get_logger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class SettingScreen(Screen):
|
class SettingScreen(Screen):
|
||||||
"""设置页面屏幕"""
|
"""Settings screen"""
|
||||||
|
|
||||||
SUB_TITLE = "设置"
|
SUB_TITLE = _("Settings")
|
||||||
BINDINGS = [
|
BINDINGS = [
|
||||||
("q", "go_back", "返回"),
|
("q", "go_back", _("Back")),
|
||||||
("s", "go_back", "设置"),
|
("s", "go_back", _("Settings")),
|
||||||
]
|
]
|
||||||
CSS_PATH = rootdir / "interface" / "css" / "screens" / "setting.tcss"
|
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}")
|
shim.set_term_title(f"{self.app.TITLE} - {self.SUB_TITLE}")
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
"""组合界面组件"""
|
"""Compose UI components"""
|
||||||
if config_var.get()["interface"]["global"]["show_header"]:
|
if config_var.get()["interface"]["global"]["show_header"]:
|
||||||
yield Header(
|
yield Header(
|
||||||
show_clock=config_var.get()["interface"]["global"]["clock_on_header"]
|
show_clock=config_var.get()["interface"]["global"]["clock_on_header"]
|
||||||
)
|
)
|
||||||
with ScrollableContainer():
|
with ScrollableContainer():
|
||||||
yield Label("[b]设置页面[/b]")
|
yield Label("[b]" + _("Settings") + "[/b]")
|
||||||
for i in config_var.get():
|
for i in config_var.get():
|
||||||
if i.startswith("_"):
|
if i.startswith("_"):
|
||||||
continue
|
continue
|
||||||
@@ -67,7 +68,7 @@ class SettingScreen(Screen):
|
|||||||
title=i + f'\n[d]{config_var.get().get(f"_{i}_desc", "")}[/d]',
|
title=i + f'\n[d]{config_var.get().get(f"_{i}_desc", "")}[/d]',
|
||||||
)
|
)
|
||||||
yield Label(
|
yield Label(
|
||||||
"退出页面时, 所作的更改会立即保存, 但仍建议重启软件以确保新的配置得到应用",
|
_("Changes are saved immediately when you leave this page, but restart is recommended to ensure the new configuration is applied."),
|
||||||
classes="foot",
|
classes="foot",
|
||||||
)
|
)
|
||||||
yield Footer()
|
yield Footer()
|
||||||
@@ -138,7 +139,7 @@ class SettingScreen(Screen):
|
|||||||
Label(i + f'\n[d]{parent.get(f"_{i}_desc", "")}[/d]'),
|
Label(i + f'\n[d]{parent.get(f"_{i}_desc", "")}[/d]'),
|
||||||
Input(
|
Input(
|
||||||
value=str(parent[i]),
|
value=str(parent[i]),
|
||||||
placeholder="要求一个浮点数",
|
placeholder=_("Requires a float"),
|
||||||
type="number",
|
type="number",
|
||||||
id=domize(f"{parent_epath}.{i}"),
|
id=domize(f"{parent_epath}.{i}"),
|
||||||
),
|
),
|
||||||
@@ -151,7 +152,7 @@ class SettingScreen(Screen):
|
|||||||
Label(i + f'\n[d]{parent.get(f"_{i}_desc", "")}[/d]'),
|
Label(i + f'\n[d]{parent.get(f"_{i}_desc", "")}[/d]'),
|
||||||
Input(
|
Input(
|
||||||
value=parent[i],
|
value=parent[i],
|
||||||
placeholder="要求一个字符串",
|
placeholder=_("Requires a string"),
|
||||||
type="text",
|
type="text",
|
||||||
id=domize(f"{parent_epath}.{i}"),
|
id=domize(f"{parent_epath}.{i}"),
|
||||||
),
|
),
|
||||||
@@ -176,7 +177,7 @@ class SettingScreen(Screen):
|
|||||||
Label(i + f'\n[d]{parent.get(f"_{i}_desc", "")}[/d]'),
|
Label(i + f'\n[d]{parent.get(f"_{i}_desc", "")}[/d]'),
|
||||||
Input(
|
Input(
|
||||||
value=str(parent[i]),
|
value=str(parent[i]),
|
||||||
placeholder="要求一个整数",
|
placeholder=_("Requires an integer"),
|
||||||
type="integer",
|
type="integer",
|
||||||
id=domize(f"{parent_epath}.{i}"),
|
id=domize(f"{parent_epath}.{i}"),
|
||||||
),
|
),
|
||||||
@@ -186,9 +187,9 @@ class SettingScreen(Screen):
|
|||||||
elif isinstance(parent[i], list):
|
elif isinstance(parent[i], list):
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
lst.append(Label("未知类型"))
|
lst.append(Label(_("Unknown type")))
|
||||||
return lst
|
return lst
|
||||||
return [Label("无子项")]
|
return [Label(_("No sub-items"))]
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
"""挂载组件时初始化"""
|
"""挂载组件时初始化"""
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""同步工具界面"""
|
"""Sync tool screen"""
|
||||||
|
|
||||||
import pathlib
|
import pathlib
|
||||||
import time
|
import time
|
||||||
@@ -12,11 +12,12 @@ from textual.worker import get_current_worker
|
|||||||
from textual import events, on
|
from textual import events, on
|
||||||
|
|
||||||
from heurams.context import *
|
from heurams.context import *
|
||||||
|
from heurams.i18n import _
|
||||||
|
|
||||||
|
|
||||||
class SyncScreen(Screen):
|
class SyncScreen(Screen):
|
||||||
|
|
||||||
BINDINGS = [("q", "go_back", "返回")]
|
BINDINGS = [("q", "go_back", _("Back"))]
|
||||||
|
|
||||||
def __init__(self, nucleons: list = [], desc: str = ""):
|
def __init__(self, nucleons: list = [], desc: str = ""):
|
||||||
super().__init__(name=None, id=None, classes=None)
|
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"]
|
show_clock=config_var.get()["interface"]["global"]["clock_on_header"]
|
||||||
)
|
)
|
||||||
with ScrollableContainer(id="sync_container"):
|
with ScrollableContainer(id="sync_container"):
|
||||||
# 标题和连接状态
|
# Title and connection status
|
||||||
yield Static("同步工具", classes="title")
|
yield Static(_("Sync Tool"), classes="title")
|
||||||
yield Static("", id="status_label", classes="status")
|
yield Static("", id="status_label", classes="status")
|
||||||
|
|
||||||
# 配置信息
|
# Config info
|
||||||
yield Static(f"同步协议: {config_var.get()['services']['sync']}")
|
yield Static(_("Sync protocol: {proto}").format(proto=config_var.get()['services']['sync']))
|
||||||
yield Static("服务器配置:", classes="section_title")
|
yield Static(_("Server Configuration:"), classes="section_title")
|
||||||
with Horizontal(classes="config_info"):
|
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")
|
yield Static("", id="server_url", classes="config_value")
|
||||||
with Horizontal(classes="config_info"):
|
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")
|
yield Static("", id="remote_path", classes="config_value")
|
||||||
|
|
||||||
with Horizontal(classes="control_buttons"):
|
with Horizontal(classes="control_buttons"):
|
||||||
yield Button("测试连接", id="test_connection", variant="primary")
|
yield Button(_("Test Connection"), id="test_connection", variant="primary")
|
||||||
yield Button("开始同步", id="start_sync", variant="success")
|
yield Button(_("Start Sync"), id="start_sync", variant="success")
|
||||||
yield Button("暂停", id="pause_sync", variant="warning", disabled=True)
|
yield Button(_("Pause"), id="pause_sync", variant="warning", disabled=True)
|
||||||
yield Button("取消", id="cancel_sync", variant="error", 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 ProgressBar(id="progress_bar", show_percentage=True, total=100)
|
||||||
yield Static("", id="progress_label", classes="progress_text")
|
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 Static("", id="log_output", classes="log_output")
|
||||||
|
|
||||||
yield Footer()
|
yield Footer()
|
||||||
|
|
||||||
def on_mount(self):
|
def on_mount(self):
|
||||||
"""挂载时初始化状态"""
|
"""Initialise state on mount"""
|
||||||
self.update_ui_from_config()
|
self.update_ui_from_config()
|
||||||
self.log_message("同步工具已启动")
|
self.log_message(_("Sync tool started"))
|
||||||
|
|
||||||
def update_ui_from_config(self):
|
def update_ui_from_config(self):
|
||||||
"""更新 UI 显示配置信息"""
|
"""Update UI with config info"""
|
||||||
try:
|
try:
|
||||||
sync_cfg: dict = config_var.get()["providers"]["sync"]["webdav"]
|
sync_cfg: dict = config_var.get()["providers"]["sync"]["webdav"]
|
||||||
# 更新服务器 URL
|
# Update server URL
|
||||||
url = sync_cfg.get("url", "未配置")
|
url = sync_cfg.get("url", _("Not configured"))
|
||||||
url_widget = self.query_one("#server_url")
|
url_widget = self.query_one("#server_url")
|
||||||
url_widget.update(url) # type: ignore
|
url_widget.update(url) # type: ignore
|
||||||
# 更新远程路径
|
# Update remote path
|
||||||
remote_path = sync_cfg.get("remote_path", "/")
|
remote_path = sync_cfg.get("remote_path", "/")
|
||||||
path_widget = self.query_one("#remote_path")
|
path_widget = self.query_one("#remote_path")
|
||||||
path_widget.update(remote_path) # type: ignore
|
path_widget.update(remote_path) # type: ignore
|
||||||
|
|
||||||
# 更新状态标签
|
# Update status label
|
||||||
status_widget = self.query_one("#status_label")
|
status_widget = self.query_one("#status_label")
|
||||||
if self.sync_service and self.sync_service.client:
|
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")
|
status_widget.add_class("ready")
|
||||||
else:
|
else:
|
||||||
status_widget.update("❌ 同步服务未配置或未启用") # type: ignore
|
status_widget.update(_("❌ Sync service not configured or not enabled")) # type: ignore
|
||||||
status_widget.add_class("error")
|
status_widget.add_class("error")
|
||||||
|
|
||||||
except Exception as e:
|
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):
|
def update_status(self, status, current_item="", progress=None):
|
||||||
"""更新状态显示"""
|
"""Update status display"""
|
||||||
try:
|
try:
|
||||||
status_widget = self.query_one("#status_label")
|
status_widget = self.query_one("#status_label")
|
||||||
status_widget.update(status) # type: ignore
|
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
|
progress_label.update(f"{progress}% - {current_item}" if current_item else f"{progress}%") # type: ignore
|
||||||
|
|
||||||
except Exception as e:
|
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):
|
def log_message(self, message: str, is_error: bool = False):
|
||||||
"""添加日志消息并更新显示"""
|
"""Add log message and update display"""
|
||||||
timestamp = time.strftime("%H:%M:%S")
|
timestamp = time.strftime("%H:%M:%S")
|
||||||
prefix = "[ERROR]" if is_error else "[INFO]"
|
prefix = "[ERROR]" if is_error else "[INFO]"
|
||||||
log_line = f"{timestamp} {prefix} {message}"
|
log_line = f"{timestamp} {prefix} {message}"
|
||||||
|
|
||||||
self.log_messages.append(log_line)
|
self.log_messages.append(log_line)
|
||||||
# 保持日志行数不超过最大值
|
# Keep log lines under max
|
||||||
if len(self.log_messages) > self.max_log_lines:
|
if len(self.log_messages) > self.max_log_lines:
|
||||||
self.log_messages = self.log_messages[-self.max_log_lines :]
|
self.log_messages = self.log_messages[-self.max_log_lines :]
|
||||||
|
|
||||||
# 更新日志显示
|
# Update log display
|
||||||
try:
|
try:
|
||||||
log_widget = self.query_one("#log_output")
|
log_widget = self.query_one("#log_output")
|
||||||
log_widget.update("\n".join(self.log_messages)) # type: ignore
|
log_widget.update("\n".join(self.log_messages)) # type: ignore
|
||||||
except Exception:
|
except Exception:
|
||||||
pass # 如果组件未就绪, 忽略错误
|
pass # Ignore if widget not ready
|
||||||
|
|
||||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
"""处理按钮点击事件"""
|
"""Handle button press events"""
|
||||||
button_id = event.button.id
|
button_id = event.button.id
|
||||||
|
|
||||||
if button_id == "test_connection":
|
if button_id == "test_connection":
|
||||||
@@ -148,124 +149,133 @@ class SyncScreen(Screen):
|
|||||||
event.stop()
|
event.stop()
|
||||||
|
|
||||||
def test_connection(self):
|
def test_connection(self):
|
||||||
"""测试 WebDAV 服务器连接"""
|
"""Test WebDAV server connection"""
|
||||||
if not self.sync_service:
|
if not self.sync_service:
|
||||||
self.log_message("同步服务未初始化, 请检查配置", is_error=True)
|
self.log_message(_("Sync service not initialised, please check configuration"), is_error=True)
|
||||||
self.update_status("❌ 同步服务未初始化")
|
self.update_status(_("❌ Sync service not initialised"))
|
||||||
return
|
return
|
||||||
|
|
||||||
self.log_message("正在测试 WebDAV 连接...")
|
self.log_message(_("Testing WebDAV connection..."))
|
||||||
self.update_status("正在测试连接...")
|
self.update_status(_("Testing connection..."))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
success = self.sync_service.test_connection()
|
success = self.sync_service.test_connection()
|
||||||
if success:
|
if success:
|
||||||
self.log_message("连接测试成功")
|
self.log_message(_("Connection test successful"))
|
||||||
self.update_status("✅ 连接正常")
|
self.update_status(_("✅ Connection OK"))
|
||||||
else:
|
else:
|
||||||
self.log_message("连接测试失败", is_error=True)
|
self.log_message(_("Connection test failed"), is_error=True)
|
||||||
self.update_status("❌ 连接失败")
|
self.update_status(_("❌ Connection failed"))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log_message(f"连接测试异常: {e}", is_error=True)
|
self.log_message(_("Connection test error: {error}").format(error=e), is_error=True)
|
||||||
self.update_status("❌ 连接异常")
|
self.update_status(_("❌ Connection error"))
|
||||||
|
|
||||||
def start_sync(self):
|
def start_sync(self):
|
||||||
"""开始同步"""
|
"""Start syncing"""
|
||||||
if not self.sync_service:
|
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
|
return
|
||||||
|
|
||||||
if self.is_syncing:
|
if self.is_syncing:
|
||||||
self.log_message("同步已在进行中", is_error=True)
|
self.log_message(_("Sync already in progress"), is_error=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
self.is_syncing = True
|
self.is_syncing = True
|
||||||
self.is_paused = False
|
self.is_paused = False
|
||||||
self.update_button_states()
|
self.update_button_states()
|
||||||
|
|
||||||
self.log_message("开始同步数据...")
|
self.log_message(_("Starting data sync..."))
|
||||||
self.update_status("正在同步...", progress=0)
|
self.update_status(_("Syncing..."), progress=0)
|
||||||
|
|
||||||
# 启动后台同步任务
|
# Start background sync task
|
||||||
self.run_worker(self.perform_sync, thread=True)
|
self.run_worker(self.perform_sync, thread=True)
|
||||||
|
|
||||||
def perform_sync(self):
|
def perform_sync(self):
|
||||||
"""执行同步任务(在后台线程中运行)"""
|
"""Execute sync task (runs in background thread)"""
|
||||||
worker = get_current_worker()
|
worker = get_current_worker()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 获取需要同步的本地目录
|
# Get local directories to sync
|
||||||
from heurams.context import config_var
|
from heurams.context import config_var
|
||||||
|
|
||||||
config = config_var.get()
|
config = config_var.get()
|
||||||
paths = config.get("paths", {})
|
paths = config.get("paths", {})
|
||||||
|
|
||||||
# 同步 nucleon 目录
|
# Sync nucleon directory
|
||||||
nucleon_dir = pathlib.Path(paths.get("nucleon_dir", "./data/nucleon"))
|
nucleon_dir = pathlib.Path(paths.get("nucleon_dir", "./data/nucleon"))
|
||||||
if nucleon_dir.exists():
|
if nucleon_dir.exists():
|
||||||
self.log_message(f"同步 nucleon 目录: {nucleon_dir}")
|
self.log_message(_("Syncing nucleon directory: {dir}").format(dir=nucleon_dir))
|
||||||
self.update_status(f"同步 nucleon 目录...", progress=10)
|
self.update_status(_("Syncing nucleon directory..."), progress=10)
|
||||||
|
|
||||||
result = self.sync_service.sync_directory(nucleon_dir) # type: ignore
|
result = self.sync_service.sync_directory(nucleon_dir) # type: ignore
|
||||||
if result.get("success"):
|
if result.get("success"):
|
||||||
self.log_message(
|
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:
|
else:
|
||||||
self.log_message(
|
self.log_message(
|
||||||
f"nucleon 同步失败: {result.get('error', '未知错误')}",
|
_("nucleon sync failed: {err}").format(err=result.get('error', _('Unknown error'))),
|
||||||
is_error=True,
|
is_error=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 同步 electron 目录
|
# Sync electron directory
|
||||||
electron_dir = pathlib.Path(paths.get("electron_dir", "./data/electron"))
|
electron_dir = pathlib.Path(paths.get("electron_dir", "./data/electron"))
|
||||||
if electron_dir.exists():
|
if electron_dir.exists():
|
||||||
self.log_message(f"同步 electron 目录: {electron_dir}")
|
self.log_message(_("Syncing electron directory: {dir}").format(dir=electron_dir))
|
||||||
self.update_status(f"同步 electron 目录...", progress=60)
|
self.update_status(_("Syncing electron directory..."), progress=60)
|
||||||
|
|
||||||
result = self.sync_service.sync_directory(electron_dir) # type: ignore
|
result = self.sync_service.sync_directory(electron_dir) # type: ignore
|
||||||
if result.get("success"):
|
if result.get("success"):
|
||||||
self.log_message(
|
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:
|
else:
|
||||||
self.log_message(
|
self.log_message(
|
||||||
f"electron 同步失败: {result.get('error', '未知错误')}",
|
_("electron sync failed: {err}").format(err=result.get('error', _('Unknown error'))),
|
||||||
is_error=True,
|
is_error=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 同步 orbital 目录(如果存在)
|
# Sync orbital directory (if exists)
|
||||||
orbital_dir = pathlib.Path(paths.get("orbital_dir", "./data/orbital"))
|
orbital_dir = pathlib.Path(paths.get("orbital_dir", "./data/orbital"))
|
||||||
if orbital_dir.exists():
|
if orbital_dir.exists():
|
||||||
self.log_message(f"同步 orbital 目录: {orbital_dir}")
|
self.log_message(_("Syncing orbital directory: {dir}").format(dir=orbital_dir))
|
||||||
self.update_status(f"同步 orbital 目录...", progress=80)
|
self.update_status(_("Syncing orbital directory..."), progress=80)
|
||||||
|
|
||||||
result = self.sync_service.sync_directory(orbital_dir) # type: ignore
|
result = self.sync_service.sync_directory(orbital_dir) # type: ignore
|
||||||
if result.get("success"):
|
if result.get("success"):
|
||||||
self.log_message(
|
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:
|
else:
|
||||||
self.log_message(
|
self.log_message(
|
||||||
f"orbital 同步失败: {result.get('error', '未知错误')}",
|
_("orbital sync failed: {err}").format(err=result.get('error', _('Unknown error'))),
|
||||||
is_error=True,
|
is_error=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 同步完成
|
# Sync complete
|
||||||
self.update_status("同步完成", progress=100)
|
self.update_status(_("Sync complete"), progress=100)
|
||||||
self.log_message("所有目录同步完成")
|
self.log_message(_("All directories synced"))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log_message(f"同步过程中发生错误: {e}", is_error=True)
|
self.log_message(_("Error during sync: {error}").format(error=e), is_error=True)
|
||||||
self.update_status("同步失败")
|
self.update_status(_("Sync failed"))
|
||||||
finally:
|
finally:
|
||||||
# 重置同步状态
|
# Reset sync state
|
||||||
self.is_syncing = False
|
self.is_syncing = False
|
||||||
self.is_paused = False
|
self.is_paused = False
|
||||||
self.update_button_states() # type: ignore
|
self.update_button_states() # type: ignore
|
||||||
|
|
||||||
def pause_sync(self):
|
def pause_sync(self):
|
||||||
"""暂停同步"""
|
"""Pause sync"""
|
||||||
if not self.is_syncing:
|
if not self.is_syncing:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -273,14 +283,14 @@ class SyncScreen(Screen):
|
|||||||
self.update_button_states()
|
self.update_button_states()
|
||||||
|
|
||||||
if self.is_paused:
|
if self.is_paused:
|
||||||
self.log_message("同步已暂停")
|
self.log_message(_("Sync paused"))
|
||||||
self.update_status("同步已暂停")
|
self.update_status(_("Sync paused"))
|
||||||
else:
|
else:
|
||||||
self.log_message("同步已恢复")
|
self.log_message(_("Sync resumed"))
|
||||||
self.update_status("正在同步...")
|
self.update_status(_("Syncing..."))
|
||||||
|
|
||||||
def cancel_sync(self):
|
def cancel_sync(self):
|
||||||
"""取消同步"""
|
"""Cancel sync"""
|
||||||
if not self.is_syncing:
|
if not self.is_syncing:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -288,11 +298,11 @@ class SyncScreen(Screen):
|
|||||||
self.is_paused = False
|
self.is_paused = False
|
||||||
self.update_button_states()
|
self.update_button_states()
|
||||||
|
|
||||||
self.log_message("同步已取消")
|
self.log_message(_("Sync cancelled"))
|
||||||
self.update_status("同步已取消")
|
self.update_status(_("Sync cancelled"))
|
||||||
|
|
||||||
def update_button_states(self):
|
def update_button_states(self):
|
||||||
"""更新按钮状态"""
|
"""Update button states"""
|
||||||
try:
|
try:
|
||||||
start_button = self.query_one("#start_sync")
|
start_button = self.query_one("#start_sync")
|
||||||
pause_button = self.query_one("#pause_sync")
|
pause_button = self.query_one("#pause_sync")
|
||||||
@@ -302,14 +312,14 @@ class SyncScreen(Screen):
|
|||||||
start_button.disabled = True
|
start_button.disabled = True
|
||||||
pause_button.disabled = False
|
pause_button.disabled = False
|
||||||
cancel_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:
|
else:
|
||||||
start_button.disabled = False
|
start_button.disabled = False
|
||||||
pause_button.disabled = True
|
pause_button.disabled = True
|
||||||
cancel_button.disabled = True
|
cancel_button.disabled = True
|
||||||
|
|
||||||
except Exception as e:
|
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):
|
def action_go_back(self):
|
||||||
self.app.pop_screen()
|
self.app.pop_screen()
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from textual.widgets import Button, Label, Static
|
|||||||
|
|
||||||
import heurams.kernel.particles as pt
|
import heurams.kernel.particles as pt
|
||||||
|
|
||||||
|
from heurams.i18n import _
|
||||||
from .base_puzzle_widget import BasePuzzleWidget
|
from .base_puzzle_widget import BasePuzzleWidget
|
||||||
|
|
||||||
|
|
||||||
@@ -32,49 +33,49 @@ class BasicEvaluation(BasePuzzleWidget):
|
|||||||
|
|
||||||
class RatingChanged(Message):
|
class RatingChanged(Message):
|
||||||
def __init__(self, rating: int) -> None:
|
def __init__(self, rating: int) -> None:
|
||||||
self.rating = rating # 评分值 (0-5)
|
self.rating = rating # Rating value (0-5)
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
# 反馈映射表
|
# Feedback mapping
|
||||||
feedback_mapping = {
|
feedback_mapping = {
|
||||||
"feedback_5": {"rating": 5, "text": "完美回想"},
|
"feedback_5": {"rating": 5, "text": _("Perfect recall")},
|
||||||
"feedback_4": {"rating": 4, "text": "犹豫后正确"},
|
"feedback_4": {"rating": 4, "text": _("Correct after hesitation")},
|
||||||
"feedback_3": {"rating": 3, "text": "困难地正确"},
|
"feedback_3": {"rating": 3, "text": _("Correct with difficulty")},
|
||||||
"feedback_2": {"rating": 2, "text": "错误但熟悉"},
|
"feedback_2": {"rating": 2, "text": _("Wrong but familiar")},
|
||||||
"feedback_1": {"rating": 1, "text": "错误且不熟"},
|
"feedback_1": {"rating": 1, "text": _("Wrong and unfamiliar")},
|
||||||
"feedback_0": {"rating": 0, "text": "完全空白"},
|
"feedback_0": {"rating": 0, "text": _("Complete blank")},
|
||||||
}
|
}
|
||||||
|
|
||||||
def compose(self):
|
def compose(self):
|
||||||
# 显示主要内容
|
# Show main content
|
||||||
yield Label(self.atom.registry["nucleon"]["content"], id="main")
|
yield Label(self.atom.registry["nucleon"]["content"], id="main")
|
||||||
|
|
||||||
# 显示评估说明(可选)
|
# Show instruction (optional)
|
||||||
yield Static("请评估你对这个内容的记忆程度: ", classes="instruction")
|
yield Static(_("Evaluate how well you remember this content: "), classes="instruction")
|
||||||
|
|
||||||
# 按钮容器
|
# Button container
|
||||||
with ScrollableContainer(id="button_container"):
|
with ScrollableContainer(id="button_container"):
|
||||||
btn = {}
|
btn = {}
|
||||||
btn["5"] = Button(
|
btn["5"] = Button(
|
||||||
"完美回想", variant="success", id="feedback_5", classes="choice"
|
_("Perfect recall"), variant="success", id="feedback_5", classes="choice"
|
||||||
)
|
)
|
||||||
btn["4"] = Button(
|
btn["4"] = Button(
|
||||||
"犹豫后正确", variant="success", id="feedback_4", classes="choice"
|
_("Correct after hesitation"), variant="success", id="feedback_4", classes="choice"
|
||||||
)
|
)
|
||||||
btn["3"] = Button(
|
btn["3"] = Button(
|
||||||
"困难地正确", variant="warning", id="feedback_3", classes="choice"
|
_("Correct with difficulty"), variant="warning", id="feedback_3", classes="choice"
|
||||||
)
|
)
|
||||||
btn["2"] = Button(
|
btn["2"] = Button(
|
||||||
"错误但熟悉", variant="warning", id="feedback_2", classes="choice"
|
_("Wrong but familiar"), variant="warning", id="feedback_2", classes="choice"
|
||||||
)
|
)
|
||||||
btn["1"] = Button(
|
btn["1"] = Button(
|
||||||
"错误且不熟", variant="error", id="feedback_1", classes="choice"
|
_("Wrong and unfamiliar"), variant="error", id="feedback_1", classes="choice"
|
||||||
)
|
)
|
||||||
btn["0"] = Button(
|
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["5"], btn["4"])
|
||||||
yield Horizontal(btn["3"], btn["2"])
|
yield Horizontal(btn["3"], btn["2"])
|
||||||
yield Horizontal(btn["1"], btn["0"])
|
yield Horizontal(btn["1"], btn["0"])
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from textual.events import Key
|
|||||||
import heurams.kernel.particles as pt
|
import heurams.kernel.particles as pt
|
||||||
import heurams.kernel.puzzles as pz
|
import heurams.kernel.puzzles as pz
|
||||||
from heurams.services.hasher import hash
|
from heurams.services.hasher import hash
|
||||||
|
from heurams.i18n import _
|
||||||
from heurams.services.logger import get_logger
|
from heurams.services.logger import get_logger
|
||||||
|
|
||||||
from .base_puzzle_widget import BasePuzzleWidget
|
from .base_puzzle_widget import BasePuzzleWidget
|
||||||
@@ -79,7 +80,6 @@ class ClozePuzzle(BasePuzzleWidget):
|
|||||||
c += 1
|
c += 1
|
||||||
self.hashmap[h] = i
|
self.hashmap[h] = i
|
||||||
btnid = f"sel000-{h}"
|
btnid = f"sel000-{h}"
|
||||||
logger.debug(f"建立按钮 {btnid}")
|
|
||||||
self.btn_shortcuts[f"{c}"] = btnid
|
self.btn_shortcuts[f"{c}"] = btnid
|
||||||
btns.append(Button(f"{i}", id=f"{btnid}", classes="cloze-option-btn"))
|
btns.append(Button(f"{i}", id=f"{btnid}", classes="cloze-option-btn"))
|
||||||
for i in range((len(btns) + 1) // 2):
|
for i in range((len(btns) + 1) // 2):
|
||||||
@@ -89,7 +89,7 @@ class ClozePuzzle(BasePuzzleWidget):
|
|||||||
yield btns[i]
|
yield btns[i]
|
||||||
s.focus()
|
s.focus()
|
||||||
|
|
||||||
yield Button("退格", id="delete")
|
yield Button(_("Backspace"), id="delete")
|
||||||
self.btn_shortcuts[f"0"] = "delete"
|
self.btn_shortcuts[f"0"] = "delete"
|
||||||
self.btn_shortcuts[f"backspace"] = "delete"
|
self.btn_shortcuts[f"backspace"] = "delete"
|
||||||
self.btn_shortcuts[f"delete"] = "delete"
|
self.btn_shortcuts[f"delete"] = "delete"
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
from textual.widget import Widget
|
from textual.widget import Widget
|
||||||
from textual.widgets import Button, Label
|
from textual.widgets import Button, Label
|
||||||
|
|
||||||
|
from heurams.i18n import _
|
||||||
|
|
||||||
|
|
||||||
class Finished(Widget):
|
class Finished(Widget):
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -26,9 +28,9 @@ class Finished(Widget):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def compose(self):
|
def compose(self):
|
||||||
yield Label("本次记忆进程结束", id="finished_msg")
|
yield Label(_("This memorization session is finished"), id="finished_msg")
|
||||||
yield Label(f"算法数据{'已保存' if self.is_saved else "未能保存"}")
|
yield Label(_("Algorithm data {}").format(_("saved") if self.is_saved else _("not saved")))
|
||||||
yield Button("返回上一级", flat=True, id="back-to-menu")
|
yield Button(_("Back to Menu"), flat=True, id="back-to-menu")
|
||||||
|
|
||||||
def on_button_pressed(self, event):
|
def on_button_pressed(self, event):
|
||||||
button_id = event.button.id
|
button_id = event.button.id
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# 单项选择题
|
# Multiple-choice puzzle
|
||||||
from typing import TypedDict
|
from typing import TypedDict
|
||||||
|
|
||||||
from textual.containers import ScrollableContainer
|
from textual.containers import ScrollableContainer
|
||||||
@@ -8,6 +8,7 @@ from textual.widgets import Button, Label
|
|||||||
import heurams.kernel.particles as pt
|
import heurams.kernel.particles as pt
|
||||||
import heurams.kernel.puzzles as pz
|
import heurams.kernel.puzzles as pz
|
||||||
from heurams.services.hasher import hash
|
from heurams.services.hasher import hash
|
||||||
|
from heurams.i18n import _
|
||||||
from heurams.services.logger import get_logger
|
from heurams.services.logger import get_logger
|
||||||
from textual.events import Key
|
from textual.events import Key
|
||||||
from .base_puzzle_widget import BasePuzzleWidget
|
from .base_puzzle_widget import BasePuzzleWidget
|
||||||
@@ -65,15 +66,10 @@ class MCQPuzzle(BasePuzzleWidget):
|
|||||||
|
|
||||||
def compose(self):
|
def compose(self):
|
||||||
setting: Setting = self.atom.registry["nucleon"]["puzzles"][self.alia]
|
setting: Setting = self.atom.registry["nucleon"]["puzzles"][self.alia]
|
||||||
if len(self.inputlist) > len(self.puzzle.options):
|
current_options = self.puzzle.options[len(self.inputlist)]
|
||||||
logger.debug("ERR IDX")
|
yield Label(setting["primary"], id="sentence")
|
||||||
logger.debug(self.inputlist)
|
yield Label(self.puzzle.wording[len(self.inputlist)], id="puzzle")
|
||||||
logger.debug(self.puzzle.options)
|
yield Label(_("Current input: {input}").format(input=self.inputlist), id="inputpreview")
|
||||||
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")
|
|
||||||
|
|
||||||
# 渲染当前问题的选项
|
# 渲染当前问题的选项
|
||||||
c = 0
|
c = 0
|
||||||
@@ -85,21 +81,19 @@ class MCQPuzzle(BasePuzzleWidget):
|
|||||||
h = str(hash(i))
|
h = str(hash(i))
|
||||||
self.hashmap[h] = i
|
self.hashmap[h] = i
|
||||||
btnid = f"sel{str(self.cursor).zfill(3)}-{h}"
|
btnid = f"sel{str(self.cursor).zfill(3)}-{h}"
|
||||||
logger.debug(f"建立按钮 {btnid}")
|
|
||||||
self.btn_shortcuts[f"{c}"] = f"{btnid}"
|
self.btn_shortcuts[f"{c}"] = f"{btnid}"
|
||||||
yield Button(f"[{c}] " + i, id=f"{btnid}")
|
yield Button(f"[{c}] " + i, id=f"{btnid}")
|
||||||
s.focus()
|
s.focus()
|
||||||
yield Button("退格", id="delete")
|
yield Button(_("Backspace"), id="delete")
|
||||||
|
|
||||||
self.btn_shortcuts["0"] = f"delete"
|
self.btn_shortcuts["0"] = f"delete"
|
||||||
self.btn_shortcuts["delete"] = f"delete"
|
self.btn_shortcuts["delete"] = f"delete"
|
||||||
self.btn_shortcuts["backspace"] = f"delete"
|
self.btn_shortcuts["backspace"] = f"delete"
|
||||||
|
|
||||||
def update_display(self, error=0):
|
def update_display(self, error=0):
|
||||||
# 更新预览标签
|
# Update preview label
|
||||||
preview = self.query_one("#inputpreview")
|
preview = self.query_one("#inputpreview")
|
||||||
preview.update(f"当前输入: {self.inputlist}") # type: ignore
|
preview.update(_("Current input: {input}").format(input=self.inputlist)) # type: ignore
|
||||||
logger.debug("已经更新预览标签")
|
|
||||||
# 更新问题标签
|
# 更新问题标签
|
||||||
puzzle_label = self.query_one("#puzzle")
|
puzzle_label = self.query_one("#puzzle")
|
||||||
current_question_index = len(self.inputlist)
|
current_question_index = len(self.inputlist)
|
||||||
@@ -122,7 +116,7 @@ class MCQPuzzle(BasePuzzleWidget):
|
|||||||
# 选项选择处理
|
# 选项选择处理
|
||||||
answer_text = self.hashmap[button_id[7:]] # type: ignore
|
answer_text = self.hashmap[button_id[7:]] # type: ignore
|
||||||
self.inputlist.append(answer_text)
|
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):
|
if len(self.inputlist) >= len(self.puzzle.answer):
|
||||||
is_correct = self.inputlist == self.puzzle.answer
|
is_correct = self.inputlist == self.puzzle.answer
|
||||||
@@ -143,7 +137,6 @@ class MCQPuzzle(BasePuzzleWidget):
|
|||||||
def refresh_buttons(self):
|
def refresh_buttons(self):
|
||||||
"""刷新按钮显示(用于题目切换)"""
|
"""刷新按钮显示(用于题目切换)"""
|
||||||
# 移除所有选项按钮
|
# 移除所有选项按钮
|
||||||
logger.debug("刷新按钮")
|
|
||||||
self.cursor += 1
|
self.cursor += 1
|
||||||
container = self.query_one("#btn-container")
|
container = self.query_one("#btn-container")
|
||||||
buttons_to_remove = [
|
buttons_to_remove = [
|
||||||
@@ -153,7 +146,6 @@ class MCQPuzzle(BasePuzzleWidget):
|
|||||||
]
|
]
|
||||||
container.focus()
|
container.focus()
|
||||||
for button in buttons_to_remove:
|
for button in buttons_to_remove:
|
||||||
logger.info(button)
|
|
||||||
container.remove_children("#" + button.id) # type: ignore
|
container.remove_children("#" + button.id) # type: ignore
|
||||||
|
|
||||||
# 添加当前题目的选项按钮
|
# 添加当前题目的选项按钮
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
from textual.widget import Widget
|
from textual.widget import Widget
|
||||||
from textual.widgets import Button, Label
|
from textual.widgets import Button, Label
|
||||||
|
|
||||||
|
from heurams.i18n import _
|
||||||
|
|
||||||
|
|
||||||
class Placeholder(Widget):
|
class Placeholder(Widget):
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -23,8 +25,8 @@ class Placeholder(Widget):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def compose(self):
|
def compose(self):
|
||||||
yield Label("示例标签", id="testlabel")
|
yield Label(_("Sample Label"), id="testlabel")
|
||||||
yield Button("示例按钮", id="testbtn", classes="choice")
|
yield Button(_("Sample Button"), id="testbtn", classes="choice")
|
||||||
|
|
||||||
def on_button_pressed(self, event):
|
def on_button_pressed(self, event):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from textual.widget import Widget
|
|||||||
from textual.widgets import Button, Label, Markdown, Static
|
from textual.widgets import Button, Label, Markdown, Static
|
||||||
|
|
||||||
import heurams.kernel.particles as pt
|
import heurams.kernel.particles as pt
|
||||||
|
from heurams.i18n import _
|
||||||
from heurams.services.logger import get_logger
|
from heurams.services.logger import get_logger
|
||||||
|
|
||||||
from .base_puzzle_widget import BasePuzzleWidget
|
from .base_puzzle_widget import BasePuzzleWidget
|
||||||
@@ -86,7 +87,7 @@ class Recognition(BasePuzzleWidget):
|
|||||||
for item in cfg["secondary"]:
|
for item in cfg["secondary"]:
|
||||||
if isinstance(item, list):
|
if isinstance(item, list):
|
||||||
for j in item:
|
for j in item:
|
||||||
yield Markdown(f"### 笔记: {j}") # TODO ANNOTATION
|
yield Markdown(_("### Note: {note}").format(note=j)) # TODO ANNOTATION
|
||||||
continue
|
continue
|
||||||
if isinstance(item, Dict):
|
if isinstance(item, Dict):
|
||||||
total = ""
|
total = ""
|
||||||
@@ -97,7 +98,7 @@ class Recognition(BasePuzzleWidget):
|
|||||||
yield Markdown(item)
|
yield Markdown(item)
|
||||||
|
|
||||||
with Center() as c:
|
with Center() as c:
|
||||||
with Button("我已知晓", id="ok") as b:
|
with Button(_("I know this"), id="ok") as b:
|
||||||
b.focus()
|
b.focus()
|
||||||
|
|
||||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
|
|||||||
@@ -44,38 +44,21 @@ class BaseAlgorithm:
|
|||||||
cls, algodata: dict, feedback: int = 5, is_new_activation: bool = False
|
cls, algodata: dict, feedback: int = 5, is_new_activation: bool = False
|
||||||
) -> None:
|
) -> None:
|
||||||
"""迭代记忆数据"""
|
"""迭代记忆数据"""
|
||||||
logger.debug(
|
|
||||||
"BaseAlgorithm.revisor 被调用, algodata keys: %s, feedback: %d, is_new_activation: %s",
|
|
||||||
list(algodata.keys()) if algodata else [],
|
|
||||||
feedback,
|
|
||||||
is_new_activation,
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_due(cls, algodata) -> int:
|
def is_due(cls, algodata) -> int:
|
||||||
"""是否应该复习"""
|
"""是否应该复习"""
|
||||||
logger.debug(
|
|
||||||
"BaseAlgorithm.is_due 被调用, algodata keys: %s",
|
|
||||||
list(algodata.keys()) if algodata else [],
|
|
||||||
)
|
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_rating(cls, algodata) -> str:
|
def get_rating(cls, algodata) -> str:
|
||||||
"""获取评分信息"""
|
"""获取评分信息"""
|
||||||
logger.debug(
|
|
||||||
"BaseAlgorithm.rate 被调用, algodata keys: %s",
|
|
||||||
list(algodata.keys()) if algodata else [],
|
|
||||||
)
|
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def nextdate(cls, algodata) -> int:
|
def nextdate(cls, algodata) -> int:
|
||||||
"""获取下一次记忆时间戳"""
|
"""获取下一次记忆时间戳"""
|
||||||
logger.debug(
|
|
||||||
"BaseAlgorithm.nextdate 被调用, algodata keys: %s",
|
|
||||||
list(algodata.keys()) if algodata else [],
|
|
||||||
)
|
|
||||||
return -1
|
return -1
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ def _get_global_scheduler():
|
|||||||
with open(_SCHEDULER_STATE_FILE, "r", encoding="utf-8") as f:
|
with open(_SCHEDULER_STATE_FILE, "r", encoding="utf-8") as f:
|
||||||
return Scheduler.from_json(f.read())
|
return Scheduler.from_json(f.read())
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warning("FSRS Scheduler 状态文件加载失败, 创建新实例")
|
logger.warning("No former FSRS Scheduler file founded, creating new instance")
|
||||||
return Scheduler()
|
return Scheduler()
|
||||||
|
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ def _save_global_scheduler(scheduler):
|
|||||||
with open(_SCHEDULER_STATE_FILE, "w", encoding="utf-8") as f:
|
with open(_SCHEDULER_STATE_FILE, "w", encoding="utf-8") as f:
|
||||||
f.write(data)
|
f.write(data)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("FSRS Scheduler 状态保存失败")
|
logger.error("Failed to persist FSRS Scheduler state")
|
||||||
|
|
||||||
|
|
||||||
def _feedback_to_rating(feedback: int) -> Rating:
|
def _feedback_to_rating(feedback: int) -> Rating:
|
||||||
@@ -176,14 +176,9 @@ class FSRSAlgorithm(BaseAlgorithm):
|
|||||||
feedback (int): 0-5 的记忆保留率量化参数
|
feedback (int): 0-5 的记忆保留率量化参数
|
||||||
is_new_activation: 是否为全新激活(重置为初始状态)
|
is_new_activation: 是否为全新激活(重置为初始状态)
|
||||||
"""
|
"""
|
||||||
logger.debug(
|
|
||||||
"FSRS.revisor 开始, feedback: %d, is_new_activation: %s",
|
|
||||||
feedback,
|
|
||||||
is_new_activation,
|
|
||||||
)
|
|
||||||
|
|
||||||
if feedback == -1:
|
if feedback == -1:
|
||||||
logger.debug("feedback 为 -1, 跳过更新")
|
logger.debug("feedback = -1, update skipped")
|
||||||
return
|
return
|
||||||
|
|
||||||
scheduler = _get_global_scheduler()
|
scheduler = _get_global_scheduler()
|
||||||
@@ -191,7 +186,7 @@ class FSRSAlgorithm(BaseAlgorithm):
|
|||||||
|
|
||||||
if is_new_activation:
|
if is_new_activation:
|
||||||
card = Card()
|
card = Card()
|
||||||
logger.debug("新激活, 创建新 Card")
|
logger.debug("New activation, create new Card")
|
||||||
else:
|
else:
|
||||||
card = cls._algodata_to_card(algodata)
|
card = cls._algodata_to_card(algodata)
|
||||||
|
|
||||||
@@ -206,7 +201,7 @@ class FSRSAlgorithm(BaseAlgorithm):
|
|||||||
algodata[cls.algo_name]["rept"] += 1
|
algodata[cls.algo_name]["rept"] += 1
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"FSRS.revisor 完成: stability=%s, difficulty=%s, state=%s, " "next_date=%d",
|
"FSRS.revisor finished: stability=%s, difficulty=%s, state=%s, " "next_date=%d",
|
||||||
card.stability,
|
card.stability,
|
||||||
card.difficulty,
|
card.difficulty,
|
||||||
card.state,
|
card.state,
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class NSP0Algorithm(BaseAlgorithm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if feedback == -1:
|
if feedback == -1:
|
||||||
logger.debug("feedback 为 -1, 跳过更新")
|
logger.debug("feedback = -1, update skipped")
|
||||||
return
|
return
|
||||||
algodata[cls.algo_name]["interval"] = 1 if feedback <= 3 else float("inf")
|
algodata[cls.algo_name]["interval"] = 1 if feedback <= 3 else float("inf")
|
||||||
if not algodata[cls.algo_name]["important"]:
|
if not algodata[cls.algo_name]["important"]:
|
||||||
@@ -65,7 +65,7 @@ class NSP0Algorithm(BaseAlgorithm):
|
|||||||
algodata[cls.algo_name]["last_modify"] = timer.get_timestamp()
|
algodata[cls.algo_name]["last_modify"] = timer.get_timestamp()
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"更新日期: last_date=%d, next_date=%d, last_modify=%f",
|
"Update date: last_date=%d, next_date=%d, last_modify=%f",
|
||||||
algodata[cls.algo_name]["last_date"],
|
algodata[cls.algo_name]["last_date"],
|
||||||
algodata[cls.algo_name]["next_date"],
|
algodata[cls.algo_name]["next_date"],
|
||||||
algodata[cls.algo_name]["last_modify"],
|
algodata[cls.algo_name]["last_modify"],
|
||||||
|
|||||||
@@ -614,7 +614,7 @@ def _get_global_sm():
|
|||||||
with open(_GLOBAL_STATE_FILE, "r", encoding="utf-8") as f:
|
with open(_GLOBAL_STATE_FILE, "r", encoding="utf-8") as f:
|
||||||
return SM.load(json.load(f))
|
return SM.load(json.load(f))
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warning("SM-15M 全局状态文件加载失败, 创建新实例")
|
logger.warning("Failed to load SM-15M global state file, creating new instance")
|
||||||
sm = SM()
|
sm = SM()
|
||||||
_save_global_sm(sm)
|
_save_global_sm(sm)
|
||||||
return sm
|
return sm
|
||||||
@@ -626,7 +626,7 @@ def _save_global_sm(sm):
|
|||||||
with open(_GLOBAL_STATE_FILE, "w", encoding="utf-8") as f:
|
with open(_GLOBAL_STATE_FILE, "w", encoding="utf-8") as f:
|
||||||
json.dump(sm.data(), f, indent=2)
|
json.dump(sm.data(), f, indent=2)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("SM-15M 全局状态保存失败")
|
logger.error("Failed to save SM-15M global state")
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -758,7 +758,7 @@ class SM15MAlgorithm(BaseAlgorithm):
|
|||||||
cls, algodata: dict, feedback: int = 5, is_new_activation: bool = False
|
cls, algodata: dict, feedback: int = 5, is_new_activation: bool = False
|
||||||
):
|
):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"SM-15M.revisor 开始, feedback=%d, is_new_activation=%s",
|
"SM-15M.revisor, feedback=%d, is_new_activation=%s",
|
||||||
feedback,
|
feedback,
|
||||||
is_new_activation,
|
is_new_activation,
|
||||||
)
|
)
|
||||||
@@ -788,7 +788,7 @@ class SM15MAlgorithm(BaseAlgorithm):
|
|||||||
algodata[cls.algo_name]["rept"] += 1
|
algodata[cls.algo_name]["rept"] += 1
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"SM-15M.revisor 完成: repetition=%d, of=%.4f, next_date=%d",
|
"SM-15M.revisor: repetition=%d, of=%.4f, next_date=%d",
|
||||||
item.repetition,
|
item.repetition,
|
||||||
item.of,
|
item.of,
|
||||||
algodata[cls.algo_name]["next_date"],
|
algodata[cls.algo_name]["next_date"],
|
||||||
|
|||||||
@@ -45,13 +45,13 @@ class SM2Algorithm(BaseAlgorithm):
|
|||||||
quality (int): 记忆保留率量化参数
|
quality (int): 记忆保留率量化参数
|
||||||
"""
|
"""
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"SM2.revisor 开始, feedback: %d, is_new_activation: %s",
|
"SM2.revisor, feedback: %d, is_new_activation: %s",
|
||||||
feedback,
|
feedback,
|
||||||
is_new_activation,
|
is_new_activation,
|
||||||
)
|
)
|
||||||
|
|
||||||
if feedback == -1:
|
if feedback == -1:
|
||||||
logger.debug("feedback 为 -1, 跳过更新")
|
logger.debug("feedback = -1, update skipped")
|
||||||
return
|
return
|
||||||
|
|
||||||
algodata[cls.algo_name]["efactor"] = algodata[cls.algo_name]["efactor"] + (
|
algodata[cls.algo_name]["efactor"] = algodata[cls.algo_name]["efactor"] + (
|
||||||
@@ -60,7 +60,7 @@ class SM2Algorithm(BaseAlgorithm):
|
|||||||
algodata[cls.algo_name]["efactor"] = max(
|
algodata[cls.algo_name]["efactor"] = max(
|
||||||
1.3, algodata[cls.algo_name]["efactor"]
|
1.3, algodata[cls.algo_name]["efactor"]
|
||||||
)
|
)
|
||||||
logger.debug("更新 efactor: %f", algodata[cls.algo_name]["efactor"])
|
logger.debug("Update efactor: %f", algodata[cls.algo_name]["efactor"])
|
||||||
|
|
||||||
if feedback < 3:
|
if feedback < 3:
|
||||||
algodata[cls.algo_name]["rept"] = 0
|
algodata[cls.algo_name]["rept"] = 0
|
||||||
@@ -68,28 +68,28 @@ class SM2Algorithm(BaseAlgorithm):
|
|||||||
logger.debug("feedback < 3, 重置 rept 和 interval")
|
logger.debug("feedback < 3, 重置 rept 和 interval")
|
||||||
else:
|
else:
|
||||||
algodata[cls.algo_name]["rept"] += 1
|
algodata[cls.algo_name]["rept"] += 1
|
||||||
logger.debug("递增 rept: %d", algodata[cls.algo_name]["rept"])
|
logger.debug("Increase rept: %d", algodata[cls.algo_name]["rept"])
|
||||||
|
|
||||||
algodata[cls.algo_name]["real_rept"] += 1
|
algodata[cls.algo_name]["real_rept"] += 1
|
||||||
logger.debug("递增 real_rept: %d", algodata[cls.algo_name]["real_rept"])
|
logger.debug("Increase real_rept: %d", algodata[cls.algo_name]["real_rept"])
|
||||||
|
|
||||||
if is_new_activation:
|
if is_new_activation:
|
||||||
algodata[cls.algo_name]["rept"] = 0
|
algodata[cls.algo_name]["rept"] = 0
|
||||||
algodata[cls.algo_name]["efactor"] = 2.5
|
algodata[cls.algo_name]["efactor"] = 2.5
|
||||||
logger.debug("新激活, 重置 rept 和 efactor")
|
logger.debug("New activation, reset rept and efactor")
|
||||||
|
|
||||||
if algodata[cls.algo_name]["rept"] == 0:
|
if algodata[cls.algo_name]["rept"] == 0:
|
||||||
algodata[cls.algo_name]["interval"] = 1
|
algodata[cls.algo_name]["interval"] = 1
|
||||||
logger.debug("rept=0, 设置 interval=1")
|
logger.debug("rept=0, set interval=1")
|
||||||
elif algodata[cls.algo_name]["rept"] == 1:
|
elif algodata[cls.algo_name]["rept"] == 1:
|
||||||
algodata[cls.algo_name]["interval"] = 6
|
algodata[cls.algo_name]["interval"] = 6
|
||||||
logger.debug("rept=1, 设置 interval=6")
|
logger.debug("rept=1, set interval=6")
|
||||||
else:
|
else:
|
||||||
algodata[cls.algo_name]["interval"] = round(
|
algodata[cls.algo_name]["interval"] = round(
|
||||||
algodata[cls.algo_name]["interval"] * algodata[cls.algo_name]["efactor"]
|
algodata[cls.algo_name]["interval"] * algodata[cls.algo_name]["efactor"]
|
||||||
)
|
)
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"rept>1, 计算 interval: %d", algodata[cls.algo_name]["interval"]
|
"rept>1, providing interval: %d", algodata[cls.algo_name]["interval"]
|
||||||
)
|
)
|
||||||
|
|
||||||
algodata[cls.algo_name]["last_date"] = timer.get_daystamp()
|
algodata[cls.algo_name]["last_date"] = timer.get_daystamp()
|
||||||
@@ -99,7 +99,7 @@ class SM2Algorithm(BaseAlgorithm):
|
|||||||
algodata[cls.algo_name]["last_modify"] = timer.get_timestamp()
|
algodata[cls.algo_name]["last_modify"] = timer.get_timestamp()
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"更新日期: last_date=%d, next_date=%d, last_modify=%f",
|
"Update date: last_date=%d, next_date=%d, last_modify=%f",
|
||||||
algodata[cls.algo_name]["last_date"],
|
algodata[cls.algo_name]["last_date"],
|
||||||
algodata[cls.algo_name]["next_date"],
|
algodata[cls.algo_name]["next_date"],
|
||||||
algodata[cls.algo_name]["last_modify"],
|
algodata[cls.algo_name]["last_modify"],
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ class Atom:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def lock(self, locked=-1):
|
def lock(self, locked=-1):
|
||||||
logger.debug(f"锁定参数 {locked}")
|
logger.debug(f"Lock atom: {locked}")
|
||||||
"""锁定, 效果等同于 self.registry['runtime']['locked'] = locked 或者返回是否锁定"""
|
"""锁定, 效果等同于 self.registry['runtime']['locked'] = locked 或者返回是否锁定"""
|
||||||
if locked == 1:
|
if locked == 1:
|
||||||
self.registry["runtime"]["locked"] = True
|
self.registry["runtime"]["locked"] = True
|
||||||
@@ -80,13 +80,13 @@ class Atom:
|
|||||||
PuzzleWidget 的 handler 除了测试, 严禁直接执行 Electron 的 revisor 函数, 否则造成逻辑混乱
|
PuzzleWidget 的 handler 除了测试, 严禁直接执行 Electron 的 revisor 函数, 否则造成逻辑混乱
|
||||||
"""
|
"""
|
||||||
if self.registry["runtime"]["locked"]:
|
if self.registry["runtime"]["locked"]:
|
||||||
logger.debug(f"允许总评分: {self.registry['runtime']['min_rate']}")
|
logger.debug(f"Rating allowed: {self.registry['runtime']['min_rate']}")
|
||||||
self.registry["electron"].revisor(
|
self.registry["electron"].revisor(
|
||||||
self.registry["runtime"]["min_rate"],
|
self.registry["runtime"]["min_rate"],
|
||||||
is_new_activation=self.registry["runtime"]["new_activation"],
|
is_new_activation=self.registry["runtime"]["new_activation"],
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
logger.debug("禁止总评分")
|
logger.debug("Rating disallowed")
|
||||||
|
|
||||||
def __getitem__(self, key):
|
def __getitem__(self, key):
|
||||||
return self.registry[key]
|
return self.registry[key]
|
||||||
|
|||||||
@@ -8,9 +8,7 @@ class BasePuzzle:
|
|||||||
"""谜题基类"""
|
"""谜题基类"""
|
||||||
|
|
||||||
def refresh(self):
|
def refresh(self):
|
||||||
logger.debug("BasePuzzle.refresh 被调用(未实现)")
|
raise NotImplementedError("Method refresh not implemented")
|
||||||
raise NotImplementedError("谜题对象未实现 refresh 方法")
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
logger.debug("BasePuzzle.__str__ 被调用")
|
return f"Puzzle: {type(self).__name__}"
|
||||||
return f"谜题: {type(self).__name__}"
|
|
||||||
|
|||||||
@@ -27,20 +27,20 @@ class ClozePuzzle(BasePuzzle):
|
|||||||
self.wording = "填空题 - 尚未刷新谜题"
|
self.wording = "填空题 - 尚未刷新谜题"
|
||||||
self.answer = ["填空题 - 尚未刷新谜题"]
|
self.answer = ["填空题 - 尚未刷新谜题"]
|
||||||
self.delimiter = delimiter
|
self.delimiter = delimiter
|
||||||
logger.debug("ClozePuzzle 初始化完成")
|
logger.debug("ClozePuzzle inited")
|
||||||
|
|
||||||
def refresh(self): # 刷新谜题
|
def refresh(self): # 刷新谜题
|
||||||
logger.debug("ClozePuzzle.refresh 开始")
|
logger.debug("ClozePuzzle.refresh")
|
||||||
placeholder = "___SLASH___"
|
placeholder = "___SLASH___"
|
||||||
tmp_text = self.text.replace(self.delimiter, placeholder)
|
tmp_text = self.text.replace(self.delimiter, placeholder)
|
||||||
words = tmp_text.split(placeholder)
|
words = tmp_text.split(placeholder)
|
||||||
if not words:
|
if not words:
|
||||||
logger.warning("ClozePuzzle.refresh: 无单词可处理")
|
logger.warning("ClozePuzzle.refresh: nothing to proceed")
|
||||||
return
|
return
|
||||||
words = [word for word in words if word]
|
words = [word for word in words if word]
|
||||||
logger.debug("ClozePuzzle.refresh: 分割出 %d 个单词", len(words))
|
logger.debug("ClozePuzzle.refresh: %d words splited", len(words))
|
||||||
num_blanks = min(max(1, len(words) // self.min_denominator), len(words))
|
num_blanks = min(max(1, len(words) // self.min_denominator), len(words))
|
||||||
logger.debug("ClozePuzzle.refresh: 需要生成 %d 个填空", num_blanks)
|
logger.debug("ClozePuzzle.refresh: %d blank required", num_blanks)
|
||||||
indices_to_blank = random.sample(range(len(words)), num_blanks)
|
indices_to_blank = random.sample(range(len(words)), num_blanks)
|
||||||
indices_to_blank.sort()
|
indices_to_blank.sort()
|
||||||
blanked_words = list(words)
|
blanked_words = list(words)
|
||||||
@@ -50,8 +50,7 @@ class ClozePuzzle(BasePuzzle):
|
|||||||
answer.append(words[index])
|
answer.append(words[index])
|
||||||
self.answer = answer
|
self.answer = answer
|
||||||
self.wording = "".join(blanked_words)
|
self.wording = "".join(blanked_words)
|
||||||
logger.debug("ClozePuzzle.refresh 完成, 生成 %d 个填空", len(answer))
|
logger.debug("ClozePuzzle.refresh, %d blanks generated", len(answer))
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
logger.debug("ClozePuzzle.__str__ 被调用")
|
|
||||||
return f"{self.wording}\n{str(self.answer)}"
|
return f"{self.wording}\n{str(self.answer)}"
|
||||||
|
|||||||
@@ -65,8 +65,8 @@ class MCQPuzzle(BasePuzzle):
|
|||||||
jammer: 传入的干扰项列表
|
jammer: 传入的干扰项列表
|
||||||
"""
|
"""
|
||||||
# 合并正确答案和传入的干扰项, 并去重
|
# 合并正确答案和传入的干扰项, 并去重
|
||||||
logger.debug(f"答案映射: {self.mapping}, {type(self.mapping)}")
|
logger.debug(f"Answer table: {self.mapping}, {type(self.mapping)}")
|
||||||
logger.debug(f"干扰项: {jammer}, {type(jammer)}")
|
logger.debug(f"Jammers: {jammer}, {type(jammer)}")
|
||||||
unique_jammers = set(jammer + list(self.mapping.values()))
|
unique_jammers = set(jammer + list(self.mapping.values()))
|
||||||
self.jammer = list(unique_jammers)
|
self.jammer = list(unique_jammers)
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ class MCQPuzzle(BasePuzzle):
|
|||||||
Raises:
|
Raises:
|
||||||
ValueError: 当mapping为空时不会抛出异常, 但会设置空谜题状态
|
ValueError: 当mapping为空时不会抛出异常, 但会设置空谜题状态
|
||||||
"""
|
"""
|
||||||
logger.debug("MCQPuzzle.refresh 开始, mapping size=%d", len(self.mapping))
|
logger.debug("MCQPuzzle.refresh, mapping size=%d", len(self.mapping))
|
||||||
if not self.mapping:
|
if not self.mapping:
|
||||||
self._set_empty_puzzle()
|
self._set_empty_puzzle()
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ class RecognitionPuzzle(BasePuzzle):
|
|||||||
"""识别占位符"""
|
"""识别占位符"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
logger.debug("RecognitionPuzzle.__init__")
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
def refresh(self):
|
def refresh(self):
|
||||||
logger.debug("RecognitionPuzzle.refresh(空实现)")
|
pass
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class Expander(Machine):
|
|||||||
self.puzzles_inf = list()
|
self.puzzles_inf = list()
|
||||||
self.min_ratings = []
|
self.min_ratings = []
|
||||||
for item, possibility in orbital_schedule: # type: ignore
|
for item, possibility in orbital_schedule: # type: ignore
|
||||||
logger.debug(f"开始处理: {item}")
|
logger.debug(f"Process: {item}")
|
||||||
|
|
||||||
puzzle = puz.puzzles[orbital_puzzles[item]["__origin__"]]
|
puzzle = puz.puzzles[orbital_puzzles[item]["__origin__"]]
|
||||||
|
|
||||||
|
|||||||
@@ -14,12 +14,6 @@ class Procession(Machine):
|
|||||||
"""队列: 标识单次记忆流程"""
|
"""队列: 标识单次记忆流程"""
|
||||||
|
|
||||||
def __init__(self, atoms: list, route_state: RouterState, name_: str = ""):
|
def __init__(self, atoms: list, route_state: RouterState, name_: str = ""):
|
||||||
logger.debug(
|
|
||||||
"Procession.__init__: 原子数量=%d, route=%s, name='%s'",
|
|
||||||
len(atoms),
|
|
||||||
route_state.value,
|
|
||||||
name_,
|
|
||||||
)
|
|
||||||
self.current_atom: pt.Atom | None
|
self.current_atom: pt.Atom | None
|
||||||
self.atoms = atoms
|
self.atoms = atoms
|
||||||
self.current_atom = atoms[0] if atoms else None
|
self.current_atom = atoms[0] if atoms else None
|
||||||
@@ -52,65 +46,48 @@ class Procession(Machine):
|
|||||||
initial=ProcessionState.ACTIVE.value,
|
initial=ProcessionState.ACTIVE.value,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug("Procession 初始化完成, 队列长度=%d", len(self.atoms))
|
|
||||||
|
|
||||||
def on_active(self):
|
def on_active(self):
|
||||||
"""进入active状态时的回调"""
|
"""进入active状态时的回调"""
|
||||||
logger.debug("Procession 进入 active 状态")
|
pass
|
||||||
|
|
||||||
def on_finished(self):
|
def on_finished(self):
|
||||||
"""进入FINISHED状态时的回调"""
|
"""进入FINISHED状态时的回调"""
|
||||||
logger.debug("Procession 进入 FINISHED 状态")
|
pass
|
||||||
|
|
||||||
def forward(self, step=1):
|
def forward(self, step=1):
|
||||||
"""将记忆原子指针向前移动并依情况更新原子(返回 1)或完成队列(返回 0)"""
|
"""将记忆原子指针向前移动并依情况更新原子(返回 1)或完成队列(返回 0)"""
|
||||||
logger.debug("Procession.forward: step=%d, 当前 cursor=%d", step, self.cursor)
|
|
||||||
self.cursor += step
|
self.cursor += step
|
||||||
if self.cursor >= len(self.atoms):
|
if self.cursor >= len(self.atoms):
|
||||||
if self.state != ProcessionState.FINISHED.value:
|
if self.state != ProcessionState.FINISHED.value:
|
||||||
self.finish() # 触发状态转换
|
self.finish() # 触发状态转换
|
||||||
logger.debug("Procession 已完成")
|
|
||||||
else:
|
else:
|
||||||
if self.state != ProcessionState.ACTIVE.value:
|
if self.state != ProcessionState.ACTIVE.value:
|
||||||
self.restart() # 确保在active状态
|
self.restart() # 确保在active状态
|
||||||
self.current_atom = self.atoms[self.cursor]
|
self.current_atom = self.atoms[self.cursor]
|
||||||
logger.debug("cursor 更新为: %d", self.cursor)
|
|
||||||
logger.debug(
|
|
||||||
"当前原子更新为: %s",
|
|
||||||
self.current_atom.ident if self.current_atom else "None",
|
|
||||||
)
|
|
||||||
|
|
||||||
def append(self, atom=None):
|
def append(self, atom=None):
|
||||||
"""追加(回忆失败的)原子(默认为当前原子)到队列末端"""
|
"""追加(回忆失败的)原子(默认为当前原子)到队列末端"""
|
||||||
if atom is None:
|
if atom is None:
|
||||||
atom = self.current_atom
|
atom = self.current_atom
|
||||||
logger.debug("Procession.append: atom=%s", atom.ident if atom else "None")
|
|
||||||
|
|
||||||
if not self.atoms or self.atoms[-1] != atom or len(self) <= 1:
|
if not self.atoms or self.atoms[-1] != atom or len(self) <= 1:
|
||||||
self.atoms.append(atom)
|
self.atoms.append(atom)
|
||||||
logger.debug("原子已追加到队列, 新队列长度=%d", len(self.atoms))
|
|
||||||
else:
|
|
||||||
logger.debug("原子未追加(重复或队列长度<=1)")
|
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
if not self.atoms:
|
if not self.atoms:
|
||||||
return 0
|
return 0
|
||||||
length = len(self.atoms) - self.cursor
|
length = len(self.atoms) - self.cursor
|
||||||
logger.debug("Procession.__len__: 剩余长度=%d", length)
|
|
||||||
return length
|
return length
|
||||||
|
|
||||||
def process(self):
|
def process(self):
|
||||||
logger.debug("Procession.process: cursor=%d", self.cursor)
|
|
||||||
return self.cursor
|
return self.cursor
|
||||||
|
|
||||||
def total_length(self):
|
def total_length(self):
|
||||||
total = len(self.atoms)
|
total = len(self.atoms)
|
||||||
logger.debug("Procession.total_length: %d", total)
|
|
||||||
return total
|
return total
|
||||||
|
|
||||||
def is_empty(self):
|
def is_empty(self):
|
||||||
empty = len(self.atoms) == 0
|
empty = len(self.atoms) == 0
|
||||||
logger.debug("Procession.is_empty: %s", empty)
|
|
||||||
return empty
|
return empty
|
||||||
|
|
||||||
def get_expander(self):
|
def get_expander(self):
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class Router(Machine):
|
|||||||
"""全局调度阶段路由器"""
|
"""全局调度阶段路由器"""
|
||||||
|
|
||||||
def __init__(self, atoms: list[pt.Atom]) -> None:
|
def __init__(self, atoms: list[pt.Atom]) -> None:
|
||||||
logger.debug(f"Router.__init__: 原子数量={len(atoms)}")
|
logger.debug(f"number of atoms={len(atoms)}")
|
||||||
|
|
||||||
self.atoms = atoms
|
self.atoms = atoms
|
||||||
new_atoms = list()
|
new_atoms = list()
|
||||||
@@ -26,7 +26,7 @@ class Router(Machine):
|
|||||||
else:
|
else:
|
||||||
old_atoms.append(i)
|
old_atoms.append(i)
|
||||||
|
|
||||||
logger.debug(f"新原子数量={len(new_atoms)}, 旧原子数量={len(old_atoms)}")
|
logger.debug(f"number of new atoms={len(new_atoms)}, number of old atoms={len(old_atoms)}")
|
||||||
|
|
||||||
self.processions = list()
|
self.processions = list()
|
||||||
"""路由中的所有队列"""
|
"""路由中的所有队列"""
|
||||||
@@ -35,17 +35,14 @@ class Router(Machine):
|
|||||||
self.processions.append(
|
self.processions.append(
|
||||||
Procession(old_atoms, RouterState.QUICK_REVIEW, "初始复习")
|
Procession(old_atoms, RouterState.QUICK_REVIEW, "初始复习")
|
||||||
)
|
)
|
||||||
logger.debug("创建初始复习 Procession")
|
|
||||||
|
|
||||||
if len(new_atoms):
|
if len(new_atoms):
|
||||||
self.processions.append(
|
self.processions.append(
|
||||||
Procession(new_atoms, RouterState.RECOGNITION, "新记忆")
|
Procession(new_atoms, RouterState.RECOGNITION, "新记忆")
|
||||||
)
|
)
|
||||||
logger.debug("创建新记忆 Procession")
|
|
||||||
|
|
||||||
self.processions.append(Procession(atoms, RouterState.FINAL_REVIEW, "总体复习"))
|
self.processions.append(Procession(atoms, RouterState.FINAL_REVIEW, "总体复习"))
|
||||||
logger.debug("创建总体复习 Procession")
|
logger.debug("Router inited, number of processions =%d", len(self.processions))
|
||||||
logger.debug("Router 初始化完成, processions 数量=%d", len(self.processions))
|
|
||||||
|
|
||||||
# 设置transitions状态机
|
# 设置transitions状态机
|
||||||
states = [
|
states = [
|
||||||
@@ -91,29 +88,27 @@ class Router(Machine):
|
|||||||
|
|
||||||
def on_unsure(self):
|
def on_unsure(self):
|
||||||
"""进入UNSURE状态时的回调"""
|
"""进入UNSURE状态时的回调"""
|
||||||
logger.debug("Router 进入 UNSURE 状态")
|
pass
|
||||||
|
|
||||||
def on_quick_review(self):
|
def on_quick_review(self):
|
||||||
"""进入QUICK_REVIEW状态时的回调"""
|
"""进入QUICK_REVIEW状态时的回调"""
|
||||||
logger.debug("Router 进入 QUICK_REVIEW 状态")
|
pass
|
||||||
|
|
||||||
def on_recognition(self):
|
def on_recognition(self):
|
||||||
"""进入RECOGNITION状态时的回调"""
|
"""进入RECOGNITION状态时的回调"""
|
||||||
logger.debug("Router 进入 RECOGNITION 状态")
|
pass
|
||||||
|
|
||||||
def on_final_review(self):
|
def on_final_review(self):
|
||||||
"""进入FINAL_REVIEW状态时的回调"""
|
"""进入FINAL_REVIEW状态时的回调"""
|
||||||
logger.debug("Router 进入 FINAL_REVIEW 状态")
|
pass
|
||||||
|
|
||||||
def on_finished(self):
|
def on_finished(self):
|
||||||
"""进入FINISHED状态时的回调"""
|
"""进入FINISHED状态时的回调"""
|
||||||
for i in self.atoms:
|
for i in self.atoms:
|
||||||
i.lock(1)
|
i.lock(1)
|
||||||
i.revise()
|
i.revise()
|
||||||
logger.debug("Router 进入 FINISHED 状态")
|
|
||||||
|
|
||||||
def current_procession(self):
|
def current_procession(self):
|
||||||
logger.debug("Router.current_procession 被调用")
|
|
||||||
for i in self.processions:
|
for i in self.processions:
|
||||||
i: Procession
|
i: Procession
|
||||||
if i.state != ProcessionState.FINISHED.value:
|
if i.state != ProcessionState.FINISHED.value:
|
||||||
@@ -125,12 +120,10 @@ class Router(Machine):
|
|||||||
elif i.route == RouterState.FINAL_REVIEW:
|
elif i.route == RouterState.FINAL_REVIEW:
|
||||||
self.to_final_review()
|
self.to_final_review()
|
||||||
|
|
||||||
logger.debug("找到未完成的 Procession: route=%s", i.route)
|
|
||||||
return i
|
return i
|
||||||
|
|
||||||
# 所有Procession都已完成
|
# 所有Procession都已完成
|
||||||
self.to_finished()
|
self.to_finished()
|
||||||
logger.debug("所有 Procession 已完成, 状态设置为 FINISHED")
|
|
||||||
return Procession([AtomPlaceholder()], RouterState.FINISHED)
|
return Procession([AtomPlaceholder()], RouterState.FINISHED)
|
||||||
|
|
||||||
def __repr__(self, style="pipe", ends="\n"):
|
def __repr__(self, style="pipe", ends="\n"):
|
||||||
|
|||||||
@@ -20,7 +20,4 @@ class ProcessionState(Enum):
|
|||||||
|
|
||||||
class ExpanderState(Enum):
|
class ExpanderState(Enum):
|
||||||
EXAMMODE = "exammode"
|
EXAMMODE = "exammode"
|
||||||
RETRONLY = "retronly"
|
RETRONLY = "retronly"
|
||||||
|
|
||||||
|
|
||||||
logger.debug("状态枚举定义已加载")
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[General]
|
||||||
|
LangCode=zh_CN
|
||||||
|
TargetLangCode=zh_CN
|
||||||
Binary file not shown.
@@ -0,0 +1,670 @@
|
|||||||
|
# Japanese translations for HeurAMS.
|
||||||
|
# Copyright (C) 2026 Wang Zhiyu
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: heurams 0.5.1\n"
|
||||||
|
"Language: ja\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||||
|
|
||||||
|
|
||||||
|
msgid "Welcome to the basic user interface!"
|
||||||
|
msgstr "基本ユーザーインターフェースへようこそ!"
|
||||||
|
|
||||||
|
msgid "Loading config and context... "
|
||||||
|
msgstr "設定とコンテキストを読み込み中… "
|
||||||
|
|
||||||
|
msgid "Done! ({time}ms)"
|
||||||
|
msgstr "完了!({time}ms)"
|
||||||
|
|
||||||
|
msgid "Loading UI framework... "
|
||||||
|
msgstr "UI フレームワークを読み込み中… "
|
||||||
|
|
||||||
|
msgid "Loading UI layout... "
|
||||||
|
msgstr "UI レイアウトを読み込み中… "
|
||||||
|
|
||||||
|
msgid "Component directory: {path}"
|
||||||
|
msgstr "コンポーネントディレクトリ: {path}"
|
||||||
|
|
||||||
|
msgid "Working directory: {path}"
|
||||||
|
msgstr "作業ディレクトリ: {path}"
|
||||||
|
|
||||||
|
msgid "Pre-work total: {time}ms"
|
||||||
|
msgstr "前処理合計: {time}ms"
|
||||||
|
|
||||||
|
msgid "Heuristic Auxiliary Memorizing Scheduler"
|
||||||
|
msgstr "ヒューリスティック補助記憶スケジューラ"
|
||||||
|
|
||||||
|
msgid "Quit"
|
||||||
|
msgstr "終了"
|
||||||
|
|
||||||
|
msgid "Theme"
|
||||||
|
msgstr "テーマ"
|
||||||
|
|
||||||
|
msgid "Navigate"
|
||||||
|
msgstr "ナビゲート"
|
||||||
|
|
||||||
|
msgid "Settings"
|
||||||
|
msgstr "設定"
|
||||||
|
|
||||||
|
msgid "About"
|
||||||
|
msgstr "このソフトについて"
|
||||||
|
|
||||||
|
msgid "HeurAMS {ver} - Heuristic Auxiliary Memorizing Scheduler"
|
||||||
|
msgstr "HeurAMS {ver} - ヒューリスティック補助記憶スケジューラ"
|
||||||
|
|
||||||
|
msgid "Listening address"
|
||||||
|
msgstr "リッスンアドレス"
|
||||||
|
|
||||||
|
msgid "Listening port"
|
||||||
|
msgstr "リッスンポート"
|
||||||
|
|
||||||
|
msgid "Development mode hot reload"
|
||||||
|
msgstr "開発モードのホットリロード"
|
||||||
|
|
||||||
|
msgid "Show this help message"
|
||||||
|
msgstr "このヘルプを表示"
|
||||||
|
|
||||||
|
msgid "unifront API service started: http://{host}:{port}"
|
||||||
|
msgstr "unifront API サービス開始: http://{host}:{port}"
|
||||||
|
|
||||||
|
msgid "Show the version and exit."
|
||||||
|
msgstr "バージョンを表示して終了。"
|
||||||
|
|
||||||
|
msgid "Explicitly specify locale (defaults to LANG env)."
|
||||||
|
msgstr "UI ロケール(例: en_US, ja)。デフォルトは LANG 環境変数。"
|
||||||
|
|
||||||
|
msgid "Launch the built-in user interface (TUI)"
|
||||||
|
msgstr "内蔵 UI(TUI)を起動"
|
||||||
|
|
||||||
|
msgid "Launch the API service (unifront)"
|
||||||
|
msgstr "API サービス(unifront)を起動"
|
||||||
|
|
||||||
|
msgid "Dashboard"
|
||||||
|
msgstr "ダッシュボード"
|
||||||
|
|
||||||
|
msgid "Back"
|
||||||
|
msgstr "戻る"
|
||||||
|
|
||||||
|
msgid "Current daystamp: {ds}"
|
||||||
|
msgstr "現在の日付スタンプ: {ds}"
|
||||||
|
|
||||||
|
msgid "Timezone offset: UTC+{offset}"
|
||||||
|
msgstr "タイムゾーンオフセット: UTC+{offset}"
|
||||||
|
|
||||||
|
msgid "Default algorithm: {algo}"
|
||||||
|
msgstr "デフォルトアルゴリズム: {algo}"
|
||||||
|
|
||||||
|
msgid "Loaded {n} repo(s)"
|
||||||
|
msgstr "リポジトリ {n} 個を読み込み"
|
||||||
|
|
||||||
|
msgid "Total {n} unit(s)"
|
||||||
|
msgstr "ユニット合計 {n}"
|
||||||
|
|
||||||
|
msgid "Activated {n} unit(s)"
|
||||||
|
msgstr "アクティブ {n} ユニット"
|
||||||
|
|
||||||
|
msgid "Version {ver}-{stage}"
|
||||||
|
msgstr "バージョン {ver}-{stage}"
|
||||||
|
|
||||||
|
msgid "Processed {puzzles} puzzles in {time}s, accuracy {accuracy}, speed {speed} puzzle(s)/s"
|
||||||
|
msgstr "{time} 秒で {puzzles} 問処理, 正解率 {accuracy}, 速度 {speed} 問/秒"
|
||||||
|
|
||||||
|
msgid "N/A"
|
||||||
|
msgstr "N/A"
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"No repo directories found in {path}.\n"
|
||||||
|
"Please import a repo and restart, or create a new one."
|
||||||
|
msgstr ""
|
||||||
|
"{path} にリポジトリが見つかりません。\n"
|
||||||
|
"リポジトリをインポートして再起動するか、新規作成してください。"
|
||||||
|
|
||||||
|
msgid "Start Learning"
|
||||||
|
msgstr "学習開始"
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"{title} [{algo}]\n"
|
||||||
|
" [d]Progress: {touched}/{total} ({pct}%)[/d]\n"
|
||||||
|
" [d]{status}[/d]"
|
||||||
|
msgstr ""
|
||||||
|
"{title} [{algo}]\n"
|
||||||
|
" [d]進捗: {touched}/{total} ({pct}%)[/d]\n"
|
||||||
|
" [d]{status}[/d]"
|
||||||
|
|
||||||
|
msgid "Due: {review}R + {new}U"
|
||||||
|
msgstr "期限: {review}R + {new}U"
|
||||||
|
|
||||||
|
msgid "Not started: 0R + {new}U"
|
||||||
|
msgstr "未開始: 0R + {new}U"
|
||||||
|
|
||||||
|
msgid "Up to date"
|
||||||
|
msgstr "最新"
|
||||||
|
|
||||||
|
msgid "[b]About & Version Info[/b]"
|
||||||
|
msgstr "[b]バージョン情報[/b]"
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"# About HeurAMS\n"
|
||||||
|
"\n"
|
||||||
|
"Main library version: `{ver}-python` \n"
|
||||||
|
"UI frontend: `Textual TUI (Basic UI)` \n"
|
||||||
|
"UI version: `{ver}` \n"
|
||||||
|
"API codename: `{codename}` \n"
|
||||||
|
"\n"
|
||||||
|
"> A heuristic auxiliary memorizing scheduler based on heuristic algorithms and cognitive science theories, designed to help users memorize and plan learning more efficiently. \n"
|
||||||
|
"> 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. \n"
|
||||||
|
"\n"
|
||||||
|
"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. \n"
|
||||||
|
"\n"
|
||||||
|
"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. \n"
|
||||||
|
"\n"
|
||||||
|
"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. \n"
|
||||||
|
"\n"
|
||||||
|
"Developers: \n"
|
||||||
|
"- Wang Zhiyu ([@pluvium27](https://github.com/pluvium27)): Project initiator and lead developer \n"
|
||||||
|
"\n"
|
||||||
|
"Special thanks to the following individuals and groups; their algorithms and theories form the cornerstone of the current software algorithms: \n"
|
||||||
|
"\n"
|
||||||
|
"- [Piotr A. Woźniak](https://supermemo.guru/wiki/Piotr_Wozniak): SM-2 algorithm and SM-15 algorithm theory \n"
|
||||||
|
"- [Jarrett Ye](https://github.com/L-M-Sherlock): FSRS algorithm and spaced repetition theory references \n"
|
||||||
|
"- [Kazuaki Tanida](https://github.com/slaypni): CoffeeScript reverse implementation of SM-15 algorithm \n"
|
||||||
|
"- [Open Spaced Repetition](https://github.com/open-spaced-repetition): FSRS algorithm underlying implementation \n"
|
||||||
|
"\n"
|
||||||
|
"# Runtime Environment\n"
|
||||||
|
"\n"
|
||||||
|
"Python interpreter version: {python_version} \n"
|
||||||
|
"Python interpreter path: {executable} \n"
|
||||||
|
"Textual framework version: {textual_version} \n"
|
||||||
|
"Terminal emulator: {terminal_info} \n"
|
||||||
|
"Operating system version: {os_version} \n"
|
||||||
|
"Disk free space: {disk_usage} \n"
|
||||||
|
"\n"
|
||||||
|
"When reporting issues, please copy this information into the issue description and attach `heurams.log` as an attachment to help developers locate the error."
|
||||||
|
msgstr ""
|
||||||
|
"# HeurAMS について\n"
|
||||||
|
"\n"
|
||||||
|
"メインライブラリバージョン: `{ver}-python` \n"
|
||||||
|
"UI フロントエンド: `Textual TUI(基本 UI)` \n"
|
||||||
|
"UI バージョン: `{ver}` \n"
|
||||||
|
"API コードネーム: `{codename}` \n"
|
||||||
|
"\n"
|
||||||
|
"> ヒューリスティックアルゴリズムと認知科学理論に基づく補助記憶スケジューラ。 \n"
|
||||||
|
"> オープンで拡張可能な間隔反復実験プラットフォーム。 \n"
|
||||||
|
"\n"
|
||||||
|
"プロジェクトHP: https://ams.pluv27.top \n"
|
||||||
|
"\n"
|
||||||
|
"GNU AGPL-3.0 で公開。ローカル API 呼び出しの例外条項付き。 \n"
|
||||||
|
"\n"
|
||||||
|
"開発者: \n"
|
||||||
|
"- Wang Zhiyu ([@pluvium27](https://github.com/pluvium27)) \n"
|
||||||
|
"\n"
|
||||||
|
"謝辞: \n"
|
||||||
|
"- [Piotr A. Woźniak](https://supermemo.guru/wiki/Piotr_Wozniak): SM-2, SM-15 \n"
|
||||||
|
"- [Jarrett Ye](https://github.com/L-M-Sherlock): FSRS \n"
|
||||||
|
"- [Kazuaki Tanida](https://github.com/slaypni): SM-15 CoffeeScript 実装 \n"
|
||||||
|
"- [Open Spaced Repetition](https://github.com/open-spaced-repetition): FSRS 実装 \n"
|
||||||
|
"\n"
|
||||||
|
"# 実行環境\n"
|
||||||
|
"\n"
|
||||||
|
"Python: {python_version} \n"
|
||||||
|
"Python パス: {executable} \n"
|
||||||
|
"Textual: {textual_version} \n"
|
||||||
|
"端末: {terminal_info} \n"
|
||||||
|
"OS: {os_version} \n"
|
||||||
|
"空き容量: {disk_usage} \n"
|
||||||
|
"\n"
|
||||||
|
"問題報告時はこの情報を添付してください。"
|
||||||
|
|
||||||
|
msgid "Back to Main"
|
||||||
|
msgstr "メインに戻る"
|
||||||
|
|
||||||
|
msgid "Unknown"
|
||||||
|
msgstr "不明"
|
||||||
|
|
||||||
|
msgid "Requires a float"
|
||||||
|
msgstr "浮動小数点数が必要"
|
||||||
|
|
||||||
|
msgid "Requires a string"
|
||||||
|
msgstr "文字列が必要"
|
||||||
|
|
||||||
|
msgid "Requires an integer"
|
||||||
|
msgstr "整数が必要"
|
||||||
|
|
||||||
|
msgid "Unknown type"
|
||||||
|
msgstr "不明な型"
|
||||||
|
|
||||||
|
msgid "No sub-items"
|
||||||
|
msgstr "サブ項目なし"
|
||||||
|
|
||||||
|
msgid "Changes are saved immediately when you leave this page, but restart is recommended to ensure the new configuration is applied."
|
||||||
|
msgstr "このページを離れると変更は即保存されますが、確実に適用するには再起動を推奨します。"
|
||||||
|
|
||||||
|
msgid "Previous"
|
||||||
|
msgstr "前へ"
|
||||||
|
|
||||||
|
msgid "Read Aloud"
|
||||||
|
msgstr "読み上げ"
|
||||||
|
|
||||||
|
msgid "Favorite"
|
||||||
|
msgstr "お気に入り"
|
||||||
|
|
||||||
|
msgid "Learning"
|
||||||
|
msgstr "学習中"
|
||||||
|
|
||||||
|
msgid "Correct"
|
||||||
|
msgstr "正解"
|
||||||
|
|
||||||
|
msgid "Incorrect"
|
||||||
|
msgstr "不正解"
|
||||||
|
|
||||||
|
msgid "Failed to generate puzzle: {e}"
|
||||||
|
msgstr "パズル生成失敗: {e}"
|
||||||
|
|
||||||
|
msgid "Favorited"
|
||||||
|
msgstr "お気に入り済み"
|
||||||
|
|
||||||
|
msgid "Not favorited"
|
||||||
|
msgstr "未お気に入り"
|
||||||
|
|
||||||
|
msgid "Previous: {ident}"
|
||||||
|
msgstr "前: {ident}"
|
||||||
|
|
||||||
|
msgid "Are you sure? Press uppercase Q to go back."
|
||||||
|
msgstr "よろしいですか? 大文字 Q で戻ります。"
|
||||||
|
|
||||||
|
msgid "Cannot favorite: no repo associated"
|
||||||
|
msgstr "お気に入り不可: リポジトリが未関連"
|
||||||
|
|
||||||
|
msgid "Unfavorited: {ident}"
|
||||||
|
msgstr "お気に入り解除: {ident}"
|
||||||
|
|
||||||
|
msgid "Favorited: {ident}"
|
||||||
|
msgstr "お気に入り登録: {ident}"
|
||||||
|
|
||||||
|
msgid "This function is not available during memorization. Please finish or go back first."
|
||||||
|
msgstr "この機能は記憶中は使用できません。終了または戻ってからお試しください。"
|
||||||
|
|
||||||
|
msgid "Time resume corrected: {old} -> {new}"
|
||||||
|
msgstr "時間再開補正: {old} -> {new}"
|
||||||
|
|
||||||
|
msgid " [Debug Connected]"
|
||||||
|
msgstr " [デバッグ接続済]"
|
||||||
|
|
||||||
|
msgid "Favorites"
|
||||||
|
msgstr "お気に入り"
|
||||||
|
|
||||||
|
msgid "No favorites"
|
||||||
|
msgstr "お気に入りなし"
|
||||||
|
|
||||||
|
msgid "Press * in the memorization screen to add favorites."
|
||||||
|
msgstr "記憶画面で * キーを押すとお気に入りに追加。"
|
||||||
|
|
||||||
|
msgid "Total {n} favorite(s)"
|
||||||
|
msgstr "お気に入り合計 {n}"
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
" [d]Added: {time}\n"
|
||||||
|
" From {title}[/d]"
|
||||||
|
msgstr ""
|
||||||
|
" [d]追加: {time}\n"
|
||||||
|
" 出典: {title}[/d]"
|
||||||
|
|
||||||
|
msgid "Remove"
|
||||||
|
msgstr "削除"
|
||||||
|
|
||||||
|
msgid "Operation failed: invalid button identifier"
|
||||||
|
msgstr "操作失敗: 無効なボタンID"
|
||||||
|
|
||||||
|
msgid "Removed favorite: {ident}"
|
||||||
|
msgstr "お気に入り削除: {ident}"
|
||||||
|
|
||||||
|
msgid "Failed to remove: {ident}"
|
||||||
|
msgstr "削除失敗: {ident}"
|
||||||
|
|
||||||
|
msgid "Switch"
|
||||||
|
msgstr "切替"
|
||||||
|
|
||||||
|
msgid "Cache Manager"
|
||||||
|
msgstr "キャッシュ管理"
|
||||||
|
|
||||||
|
msgid "Settings Page"
|
||||||
|
msgstr "設定ページ"
|
||||||
|
|
||||||
|
msgid "Sync Tool"
|
||||||
|
msgstr "同期ツール"
|
||||||
|
|
||||||
|
msgid "Exit"
|
||||||
|
msgstr "終了"
|
||||||
|
|
||||||
|
msgid "Project Homepage"
|
||||||
|
msgstr "プロジェクトHP"
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"[b]Select a function to navigate to\n"
|
||||||
|
"or a memorization session instance[/b]\n"
|
||||||
|
"\n"
|
||||||
|
"Tips will be displayed here"
|
||||||
|
msgstr ""
|
||||||
|
"[b]移動先の機能または\n"
|
||||||
|
"記憶セッションを選択[/b]\n"
|
||||||
|
"\n"
|
||||||
|
"ヒントがここに表示されます"
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Press Enter to switch\n"
|
||||||
|
"All sessions will be saved"
|
||||||
|
msgstr ""
|
||||||
|
"Enter で切替\n"
|
||||||
|
"全セッションは保存されます"
|
||||||
|
|
||||||
|
msgid "Close (n)"
|
||||||
|
msgstr "閉じる (n)"
|
||||||
|
|
||||||
|
msgid "Prepare Repository"
|
||||||
|
msgstr "リポジトリ準備"
|
||||||
|
|
||||||
|
msgid "Cache"
|
||||||
|
msgstr "キャッシュ"
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"**Ready**: `{title}`\n"
|
||||||
|
""
|
||||||
|
msgstr ""
|
||||||
|
"**準備完了**: `{title}`\n"
|
||||||
|
""
|
||||||
|
|
||||||
|
msgid "Repo path: {path}"
|
||||||
|
msgstr "リポジトリパス: {path}"
|
||||||
|
|
||||||
|
msgid "Progress: {touched}/{total} [{pct}%]"
|
||||||
|
msgstr "進捗: {touched}/{total} [{pct}%]"
|
||||||
|
|
||||||
|
msgid "Scheduling algorithm: {algo} {desc}"
|
||||||
|
msgstr "スケジュールアルゴリズム: {algo} {desc}"
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Study count: {total} = {review} [d][Review][/d] + {new} [d][New][/d]\n"
|
||||||
|
""
|
||||||
|
msgstr ""
|
||||||
|
"学習数: {total} = {review} [d][復習][/d] + {new} [d][新規][/d]\n"
|
||||||
|
""
|
||||||
|
|
||||||
|
msgid "Start Memorizing"
|
||||||
|
msgstr "記憶開始"
|
||||||
|
|
||||||
|
msgid "Manage Cache"
|
||||||
|
msgstr "キャッシュ管理"
|
||||||
|
|
||||||
|
msgid "Sync protocol: {proto}"
|
||||||
|
msgstr "同期プロトコル: {proto}"
|
||||||
|
|
||||||
|
msgid "Server Configuration:"
|
||||||
|
msgstr "サーバー設定:"
|
||||||
|
|
||||||
|
msgid "Remote server:"
|
||||||
|
msgstr "リモートサーバー:"
|
||||||
|
|
||||||
|
msgid "Remote path:"
|
||||||
|
msgstr "リモートパス:"
|
||||||
|
|
||||||
|
msgid "Test Connection"
|
||||||
|
msgstr "接続テスト"
|
||||||
|
|
||||||
|
msgid "Start Sync"
|
||||||
|
msgstr "同期開始"
|
||||||
|
|
||||||
|
msgid "Pause"
|
||||||
|
msgstr "一時停止"
|
||||||
|
|
||||||
|
msgid "Cancel"
|
||||||
|
msgstr "キャンセル"
|
||||||
|
|
||||||
|
msgid "Sync Progress"
|
||||||
|
msgstr "同期進捗"
|
||||||
|
|
||||||
|
msgid "Sync Log"
|
||||||
|
msgstr "同期ログ"
|
||||||
|
|
||||||
|
msgid "Not configured"
|
||||||
|
msgstr "未設定"
|
||||||
|
|
||||||
|
msgid "Sync service ready"
|
||||||
|
msgstr "同期サービス準備完了"
|
||||||
|
|
||||||
|
msgid "Sync service not configured or not enabled"
|
||||||
|
msgstr "同期サービスが未設定または無効"
|
||||||
|
|
||||||
|
msgid "Failed to update UI: {error}"
|
||||||
|
msgstr "UI 更新失敗: {error}"
|
||||||
|
|
||||||
|
msgid "Failed to update status: {error}"
|
||||||
|
msgstr "ステータス更新失敗: {error}"
|
||||||
|
|
||||||
|
msgid "Sync service not initialised, please check configuration"
|
||||||
|
msgstr "同期サービスが未初期化。設定を確認してください。"
|
||||||
|
|
||||||
|
msgid "Sync service not initialised"
|
||||||
|
msgstr "同期サービス未初期化"
|
||||||
|
|
||||||
|
msgid "Testing WebDAV connection..."
|
||||||
|
msgstr "WebDAV 接続テスト中…"
|
||||||
|
|
||||||
|
msgid "Testing connection..."
|
||||||
|
msgstr "接続テスト中…"
|
||||||
|
|
||||||
|
msgid "Connection test successful"
|
||||||
|
msgstr "接続テスト成功"
|
||||||
|
|
||||||
|
msgid "Connection OK"
|
||||||
|
msgstr "接続正常"
|
||||||
|
|
||||||
|
msgid "Connection test failed"
|
||||||
|
msgstr "接続テスト失敗"
|
||||||
|
|
||||||
|
msgid "Connection failed"
|
||||||
|
msgstr "接続失敗"
|
||||||
|
|
||||||
|
msgid "Connection test error: {error}"
|
||||||
|
msgstr "接続テストエラー: {error}"
|
||||||
|
|
||||||
|
msgid "Connection error"
|
||||||
|
msgstr "接続エラー"
|
||||||
|
|
||||||
|
msgid "Sync service not initialised, cannot start sync"
|
||||||
|
msgstr "同期サービス未初期化のため開始不可"
|
||||||
|
|
||||||
|
msgid "Sync already in progress"
|
||||||
|
msgstr "同期は既に実行中"
|
||||||
|
|
||||||
|
msgid "Starting data sync..."
|
||||||
|
msgstr "データ同期開始…"
|
||||||
|
|
||||||
|
msgid "Syncing..."
|
||||||
|
msgstr "同期中…"
|
||||||
|
|
||||||
|
msgid "Syncing nucleon directory: {dir}"
|
||||||
|
msgstr "nucleon ディレクトリ同期中: {dir}"
|
||||||
|
|
||||||
|
msgid "Syncing electron directory: {dir}"
|
||||||
|
msgstr "electron ディレクトリ同期中: {dir}"
|
||||||
|
|
||||||
|
msgid "Syncing orbital directory: {dir}"
|
||||||
|
msgstr "orbital ディレクトリ同期中: {dir}"
|
||||||
|
|
||||||
|
msgid "nucleon sync complete: uploaded {up}, downloaded {down}"
|
||||||
|
msgstr "nucleon 同期完了: アップロード {up}, ダウンロード {down}"
|
||||||
|
|
||||||
|
msgid "electron sync complete: uploaded {up}, downloaded {down}"
|
||||||
|
msgstr "electron 同期完了: アップロード {up}, ダウンロード {down}"
|
||||||
|
|
||||||
|
msgid "orbital sync complete: uploaded {up}, downloaded {down}"
|
||||||
|
msgstr "orbital 同期完了: アップロード {up}, ダウンロード {down}"
|
||||||
|
|
||||||
|
msgid "Sync complete"
|
||||||
|
msgstr "同期完了"
|
||||||
|
|
||||||
|
msgid "All directories synced"
|
||||||
|
msgstr "全ディレクトリ同期完了"
|
||||||
|
|
||||||
|
msgid "Error during sync: {error}"
|
||||||
|
msgstr "同期エラー: {error}"
|
||||||
|
|
||||||
|
msgid "Sync failed"
|
||||||
|
msgstr "同期失敗"
|
||||||
|
|
||||||
|
msgid "Sync paused"
|
||||||
|
msgstr "同期一時停止"
|
||||||
|
|
||||||
|
msgid "Sync resumed"
|
||||||
|
msgstr "同期再開"
|
||||||
|
|
||||||
|
msgid "Sync cancelled"
|
||||||
|
msgstr "同期キャンセル"
|
||||||
|
|
||||||
|
msgid "Resume"
|
||||||
|
msgstr "再開"
|
||||||
|
|
||||||
|
msgid "Failed to update button state: {error}"
|
||||||
|
msgstr "ボタン状態更新失敗: {error}"
|
||||||
|
|
||||||
|
msgid "Unknown error"
|
||||||
|
msgstr "不明なエラー"
|
||||||
|
|
||||||
|
msgid "[b]Audio Pre-cache[/b]"
|
||||||
|
msgstr "[b]音声プリキャッシュ[/b]"
|
||||||
|
|
||||||
|
msgid "Cache rate: {rate:.1f}% ({cached} / {total} units)"
|
||||||
|
msgstr "キャッシュ率: {rate:.1f}% ({cached} / {total} ユニット)"
|
||||||
|
|
||||||
|
msgid "Target units from: [b]{desc}[/b]"
|
||||||
|
msgstr "対象ユニット: [b]{desc}[/b]"
|
||||||
|
|
||||||
|
msgid "Unit count: {n}"
|
||||||
|
msgstr "ユニット数: {n}"
|
||||||
|
|
||||||
|
msgid "Target: all units"
|
||||||
|
msgstr "対象: 全ユニット"
|
||||||
|
|
||||||
|
msgid "Start Pre-cache"
|
||||||
|
msgstr "プリキャッシュ開始"
|
||||||
|
|
||||||
|
msgid "Cancel Pre-cache"
|
||||||
|
msgstr "プリキャッシュ中止"
|
||||||
|
|
||||||
|
msgid "Clear Cache"
|
||||||
|
msgstr "キャッシュ消去"
|
||||||
|
|
||||||
|
msgid "Cache path: {path}"
|
||||||
|
msgstr "キャッシュパス: {path}"
|
||||||
|
|
||||||
|
msgid "Files: {n}"
|
||||||
|
msgstr "ファイル数: {n}"
|
||||||
|
|
||||||
|
msgid "Total size: {size}"
|
||||||
|
msgstr "合計サイズ: {size}"
|
||||||
|
|
||||||
|
msgid "Refresh"
|
||||||
|
msgstr "更新"
|
||||||
|
|
||||||
|
msgid "If you leave this screen, ongoing cache processes will stop automatically."
|
||||||
|
msgstr "この画面を離れるとキャッシュ処理は自動停止します。"
|
||||||
|
|
||||||
|
msgid "Cache supports \"resume from break\"."
|
||||||
|
msgstr "中断再開に対応しています。"
|
||||||
|
|
||||||
|
msgid "Ready"
|
||||||
|
msgstr "準備完了"
|
||||||
|
|
||||||
|
msgid "Waiting to start..."
|
||||||
|
msgstr "開始待機中…"
|
||||||
|
|
||||||
|
msgid "Status: {s}"
|
||||||
|
msgstr "ステータス: {s}"
|
||||||
|
|
||||||
|
msgid "Current item: {item}"
|
||||||
|
msgstr "現在の項目: {item}"
|
||||||
|
|
||||||
|
msgid "Processing ({i}/{total})"
|
||||||
|
msgstr "処理中 ({i}/{total})"
|
||||||
|
|
||||||
|
msgid "Error"
|
||||||
|
msgstr "エラー"
|
||||||
|
|
||||||
|
msgid "Failed, skipping: {item}"
|
||||||
|
msgstr "失敗、スキップ: {item}"
|
||||||
|
|
||||||
|
msgid "Cancelled"
|
||||||
|
msgstr "キャンセル済"
|
||||||
|
|
||||||
|
msgid "Pre-cache cancelled by user"
|
||||||
|
msgstr "ユーザーがプリキャッシュをキャンセル"
|
||||||
|
|
||||||
|
msgid "Cleared"
|
||||||
|
msgstr "消去済"
|
||||||
|
|
||||||
|
msgid "Audio cache cleared"
|
||||||
|
msgstr "音声キャッシュ消去済"
|
||||||
|
|
||||||
|
msgid "Failed to clear cache: {error}"
|
||||||
|
msgstr "キャッシュ消去失敗: {error}"
|
||||||
|
|
||||||
|
msgid "Cache info refreshed"
|
||||||
|
msgstr "キャッシュ情報更新完了"
|
||||||
|
|
||||||
|
msgid "This memorization session is finished"
|
||||||
|
msgstr "記憶セッション終了"
|
||||||
|
|
||||||
|
msgid "Algorithm data {}"
|
||||||
|
msgstr "アルゴリズムデータ{}"
|
||||||
|
|
||||||
|
msgid "saved"
|
||||||
|
msgstr "保存済"
|
||||||
|
|
||||||
|
msgid "not saved"
|
||||||
|
msgstr "未保存"
|
||||||
|
|
||||||
|
msgid "Back to Menu"
|
||||||
|
msgstr "メニューに戻る"
|
||||||
|
|
||||||
|
msgid "Perfect recall"
|
||||||
|
msgstr "完全に想起"
|
||||||
|
|
||||||
|
msgid "Correct after hesitation"
|
||||||
|
msgstr "ためらい後正解"
|
||||||
|
|
||||||
|
msgid "Correct with difficulty"
|
||||||
|
msgstr "困難だが正解"
|
||||||
|
|
||||||
|
msgid "Wrong but familiar"
|
||||||
|
msgstr "間違えたが見覚えあり"
|
||||||
|
|
||||||
|
msgid "Wrong and unfamiliar"
|
||||||
|
msgstr "間違えて見覚えなし"
|
||||||
|
|
||||||
|
msgid "Complete blank"
|
||||||
|
msgstr "まったく想起不可"
|
||||||
|
|
||||||
|
msgid "Evaluate how well you remember this content: "
|
||||||
|
msgstr "この内容の記憶程度を評価: "
|
||||||
|
|
||||||
|
msgid "### Note: {note}"
|
||||||
|
msgstr "### メモ: {note}"
|
||||||
|
|
||||||
|
msgid "I know this"
|
||||||
|
msgstr "分かっています"
|
||||||
|
|
||||||
|
msgid "Current input: {input}"
|
||||||
|
msgstr "現在の入力: {input}"
|
||||||
|
|
||||||
|
msgid "Backspace"
|
||||||
|
msgstr "BS"
|
||||||
|
|
||||||
|
msgid "Sample Label"
|
||||||
|
msgstr "サンプルラベル"
|
||||||
|
|
||||||
|
msgid "Sample Button"
|
||||||
|
msgstr "サンプルボタン"
|
||||||
Binary file not shown.
@@ -0,0 +1,669 @@
|
|||||||
|
# Russian translations for HeurAMS.
|
||||||
|
# Copyright (C) 2026 Wang Zhiyu
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: heurams 0.5.1\n"
|
||||||
|
"Language: ru\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||||
|
|
||||||
|
|
||||||
|
msgid "Welcome to the basic user interface!"
|
||||||
|
msgstr "Добро пожаловать в интерфейс!"
|
||||||
|
|
||||||
|
msgid "Loading config and context... "
|
||||||
|
msgstr "Загрузка конфигурации… "
|
||||||
|
|
||||||
|
msgid "Done! ({time}ms)"
|
||||||
|
msgstr "Готово! ({time}ms)"
|
||||||
|
|
||||||
|
msgid "Loading UI framework... "
|
||||||
|
msgstr "Загрузка UI… "
|
||||||
|
|
||||||
|
msgid "Loading UI layout... "
|
||||||
|
msgstr "Загрузка разметки… "
|
||||||
|
|
||||||
|
msgid "Component directory: {path}"
|
||||||
|
msgstr "Директория: {path}"
|
||||||
|
|
||||||
|
msgid "Working directory: {path}"
|
||||||
|
msgstr "Рабочая директория: {path}"
|
||||||
|
|
||||||
|
msgid "Pre-work total: {time}ms"
|
||||||
|
msgstr "Всего: {time}ms"
|
||||||
|
|
||||||
|
msgid "Heuristic Auxiliary Memorizing Scheduler"
|
||||||
|
msgstr "Эвристический планировщик запоминания"
|
||||||
|
|
||||||
|
msgid "Quit"
|
||||||
|
msgstr "Выход"
|
||||||
|
|
||||||
|
msgid "Theme"
|
||||||
|
msgstr "Тема"
|
||||||
|
|
||||||
|
msgid "Navigate"
|
||||||
|
msgstr "Навигация"
|
||||||
|
|
||||||
|
msgid "Settings"
|
||||||
|
msgstr "Настройки"
|
||||||
|
|
||||||
|
msgid "About"
|
||||||
|
msgstr "О программе"
|
||||||
|
|
||||||
|
msgid "HeurAMS {ver} - Heuristic Auxiliary Memorizing Scheduler"
|
||||||
|
msgstr "HeurAMS {ver} - Эвристический планировщик"
|
||||||
|
|
||||||
|
msgid "Listening address"
|
||||||
|
msgstr "Адрес"
|
||||||
|
|
||||||
|
msgid "Listening port"
|
||||||
|
msgstr "Порт"
|
||||||
|
|
||||||
|
msgid "Development mode hot reload"
|
||||||
|
msgstr "Горячая перезагрузка (dev)"
|
||||||
|
|
||||||
|
msgid "Show this help message"
|
||||||
|
msgstr "Показать справку"
|
||||||
|
|
||||||
|
msgid "unifront API service started: http://{host}:{port}"
|
||||||
|
msgstr "API запущен: http://{host}:{port}"
|
||||||
|
|
||||||
|
msgid "Show the version and exit."
|
||||||
|
msgstr "Показать версию и выйти."
|
||||||
|
|
||||||
|
msgid "Explicitly specify locale (defaults to LANG env)."
|
||||||
|
msgstr "Локаль (напр. en_US, ru). По умолчанию из LANG."
|
||||||
|
|
||||||
|
msgid "Launch the built-in user interface (TUI)"
|
||||||
|
msgstr "Запустить TUI"
|
||||||
|
|
||||||
|
msgid "Launch the API service (unifront)"
|
||||||
|
msgstr "Запустить API"
|
||||||
|
|
||||||
|
msgid "Dashboard"
|
||||||
|
msgstr "Панель"
|
||||||
|
|
||||||
|
msgid "Back"
|
||||||
|
msgstr "Назад"
|
||||||
|
|
||||||
|
msgid "Current daystamp: {ds}"
|
||||||
|
msgstr "Дата: {ds}"
|
||||||
|
|
||||||
|
msgid "Timezone offset: UTC+{offset}"
|
||||||
|
msgstr "Смещение: UTC+{offset}"
|
||||||
|
|
||||||
|
msgid "Default algorithm: {algo}"
|
||||||
|
msgstr "Алгоритм: {algo}"
|
||||||
|
|
||||||
|
msgid "Loaded {n} repo(s)"
|
||||||
|
msgstr "Загружено: {n}"
|
||||||
|
|
||||||
|
msgid "Total {n} unit(s)"
|
||||||
|
msgstr "Всего单元: {n}"
|
||||||
|
|
||||||
|
msgid "Activated {n} unit(s)"
|
||||||
|
msgstr "Активно: {n}"
|
||||||
|
|
||||||
|
msgid "Version {ver}-{stage}"
|
||||||
|
msgstr "Версия {ver}-{stage}"
|
||||||
|
|
||||||
|
msgid "Processed {puzzles} puzzles in {time}s, accuracy {accuracy}, speed {speed} puzzle(s)/s"
|
||||||
|
msgstr "Обработано {puzzles} задач за {time}с, точность {accuracy}, скорость {speed} задач/с"
|
||||||
|
|
||||||
|
msgid "N/A"
|
||||||
|
msgstr "Н/Д"
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"No repo directories found in {path}.\n"
|
||||||
|
"Please import a repo and restart, or create a new one."
|
||||||
|
msgstr ""
|
||||||
|
"Репозитории не найдены в {path}.\n"
|
||||||
|
"Импортируйте или создайте новый."
|
||||||
|
|
||||||
|
msgid "Start Learning"
|
||||||
|
msgstr "Начать"
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"{title} [{algo}]\n"
|
||||||
|
" [d]Progress: {touched}/{total} ({pct}%)[/d]\n"
|
||||||
|
" [d]{status}[/d]"
|
||||||
|
msgstr ""
|
||||||
|
"{title} [{algo}]\n"
|
||||||
|
" [d]Прогресс: {touched}/{total} ({pct}%)[/d]\n"
|
||||||
|
" [d]{status}[/d]"
|
||||||
|
|
||||||
|
msgid "Due: {review}R + {new}U"
|
||||||
|
msgstr "К сроку: {review}R + {new}U"
|
||||||
|
|
||||||
|
msgid "Not started: 0R + {new}U"
|
||||||
|
msgstr "Не начато: 0R + {new}U"
|
||||||
|
|
||||||
|
msgid "Up to date"
|
||||||
|
msgstr "Актуально"
|
||||||
|
|
||||||
|
msgid "[b]About & Version Info[/b]"
|
||||||
|
msgstr "[b]О программе[/b]"
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"# About HeurAMS\n"
|
||||||
|
"\n"
|
||||||
|
"Main library version: `{ver}-python` \n"
|
||||||
|
"UI frontend: `Textual TUI (Basic UI)` \n"
|
||||||
|
"UI version: `{ver}` \n"
|
||||||
|
"API codename: `{codename}` \n"
|
||||||
|
"\n"
|
||||||
|
"> A heuristic auxiliary memorizing scheduler based on heuristic algorithms and cognitive science theories, designed to help users memorize and plan learning more efficiently. \n"
|
||||||
|
"> 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. \n"
|
||||||
|
"\n"
|
||||||
|
"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. \n"
|
||||||
|
"\n"
|
||||||
|
"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. \n"
|
||||||
|
"\n"
|
||||||
|
"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. \n"
|
||||||
|
"\n"
|
||||||
|
"Developers: \n"
|
||||||
|
"- Wang Zhiyu ([@pluvium27](https://github.com/pluvium27)): Project initiator and lead developer \n"
|
||||||
|
"\n"
|
||||||
|
"Special thanks to the following individuals and groups; their algorithms and theories form the cornerstone of the current software algorithms: \n"
|
||||||
|
"\n"
|
||||||
|
"- [Piotr A. Woźniak](https://supermemo.guru/wiki/Piotr_Wozniak): SM-2 algorithm and SM-15 algorithm theory \n"
|
||||||
|
"- [Jarrett Ye](https://github.com/L-M-Sherlock): FSRS algorithm and spaced repetition theory references \n"
|
||||||
|
"- [Kazuaki Tanida](https://github.com/slaypni): CoffeeScript reverse implementation of SM-15 algorithm \n"
|
||||||
|
"- [Open Spaced Repetition](https://github.com/open-spaced-repetition): FSRS algorithm underlying implementation \n"
|
||||||
|
"\n"
|
||||||
|
"# Runtime Environment\n"
|
||||||
|
"\n"
|
||||||
|
"Python interpreter version: {python_version} \n"
|
||||||
|
"Python interpreter path: {executable} \n"
|
||||||
|
"Textual framework version: {textual_version} \n"
|
||||||
|
"Terminal emulator: {terminal_info} \n"
|
||||||
|
"Operating system version: {os_version} \n"
|
||||||
|
"Disk free space: {disk_usage} \n"
|
||||||
|
"\n"
|
||||||
|
"When reporting issues, please copy this information into the issue description and attach `heurams.log` as an attachment to help developers locate the error."
|
||||||
|
msgstr ""
|
||||||
|
"# О HeurAMS\n"
|
||||||
|
"\n"
|
||||||
|
"Версия: `{ver}-python` \n"
|
||||||
|
"UI: `Textual TUI` \n"
|
||||||
|
"Версия UI: `{ver}` \n"
|
||||||
|
"Кодовое имя: `{codename}` \n"
|
||||||
|
"\n"
|
||||||
|
"> Эвристический планировщик запоминания на основе когнитивных наук. \n"
|
||||||
|
"\n"
|
||||||
|
"Сайт: https://ams.pluv27.top \n"
|
||||||
|
"\n"
|
||||||
|
"AGPL-3.0 с исключением для локальных API. \n"
|
||||||
|
"\n"
|
||||||
|
"Разработчик: \n"
|
||||||
|
"- Wang Zhiyu ([@pluvium27](https://github.com/pluvium27)) \n"
|
||||||
|
"\n"
|
||||||
|
"Благодарности: \n"
|
||||||
|
"- [Piotr A. Woźniak](https://supermemo.guru/wiki/Piotr_Wozniak): SM-2, SM-15 \n"
|
||||||
|
"- [Jarrett Ye](https://github.com/L-M-Sherlock): FSRS \n"
|
||||||
|
"- [Kazuaki Tanida](https://github.com/slaypni): SM-15 CoffeeScript \n"
|
||||||
|
"- [Open Spaced Repetition](https://github.com/open-spaced-repetition): FSRS \n"
|
||||||
|
"\n"
|
||||||
|
"# Окружение\n"
|
||||||
|
"\n"
|
||||||
|
"Python: {python_version} \n"
|
||||||
|
"Путь: {executable} \n"
|
||||||
|
"Textual: {textual_version} \n"
|
||||||
|
"Терминал: {terminal_info} \n"
|
||||||
|
"ОС: {os_version} \n"
|
||||||
|
"Диск: {disk_usage} \n"
|
||||||
|
"\n"
|
||||||
|
"Прикрепите эту информацию к баг-репорту."
|
||||||
|
|
||||||
|
msgid "Back to Main"
|
||||||
|
msgstr "На главную"
|
||||||
|
|
||||||
|
msgid "Unknown"
|
||||||
|
msgstr "Неизв."
|
||||||
|
|
||||||
|
msgid "Requires a float"
|
||||||
|
msgstr "Нужно число с точкой"
|
||||||
|
|
||||||
|
msgid "Requires a string"
|
||||||
|
msgstr "Нужна строка"
|
||||||
|
|
||||||
|
msgid "Requires an integer"
|
||||||
|
msgstr "Нужно целое"
|
||||||
|
|
||||||
|
msgid "Unknown type"
|
||||||
|
msgstr "Неизв. тип"
|
||||||
|
|
||||||
|
msgid "No sub-items"
|
||||||
|
msgstr "Нет подпунктов"
|
||||||
|
|
||||||
|
msgid "Changes are saved immediately when you leave this page, but restart is recommended to ensure the new configuration is applied."
|
||||||
|
msgstr "Изменения сохраняются сразу. Рекомендуется перезапуск."
|
||||||
|
|
||||||
|
msgid "Previous"
|
||||||
|
msgstr "Назад"
|
||||||
|
|
||||||
|
msgid "Read Aloud"
|
||||||
|
msgstr "Озвучить"
|
||||||
|
|
||||||
|
msgid "Favorite"
|
||||||
|
msgstr "Избранное"
|
||||||
|
|
||||||
|
msgid "Learning"
|
||||||
|
msgstr "Обучение"
|
||||||
|
|
||||||
|
msgid "Correct"
|
||||||
|
msgstr "Верно"
|
||||||
|
|
||||||
|
msgid "Incorrect"
|
||||||
|
msgstr "Неверно"
|
||||||
|
|
||||||
|
msgid "Failed to generate puzzle: {e}"
|
||||||
|
msgstr "Ошибка генерации: {e}"
|
||||||
|
|
||||||
|
msgid "Favorited"
|
||||||
|
msgstr "В избранном"
|
||||||
|
|
||||||
|
msgid "Not favorited"
|
||||||
|
msgstr "Не в избранном"
|
||||||
|
|
||||||
|
msgid "Previous: {ident}"
|
||||||
|
msgstr "Пред.: {ident}"
|
||||||
|
|
||||||
|
msgid "Are you sure? Press uppercase Q to go back."
|
||||||
|
msgstr "Уверены? Нажмите Q для возврата."
|
||||||
|
|
||||||
|
msgid "Cannot favorite: no repo associated"
|
||||||
|
msgstr "Ошибка: репозиторий не связан"
|
||||||
|
|
||||||
|
msgid "Unfavorited: {ident}"
|
||||||
|
msgstr "Удалено: {ident}"
|
||||||
|
|
||||||
|
msgid "Favorited: {ident}"
|
||||||
|
msgstr "Добавлено: {ident}"
|
||||||
|
|
||||||
|
msgid "This function is not available during memorization. Please finish or go back first."
|
||||||
|
msgstr "Функция недоступна во время запоминания."
|
||||||
|
|
||||||
|
msgid "Time resume corrected: {old} -> {new}"
|
||||||
|
msgstr "Время скорректировано: {old} -> {new}"
|
||||||
|
|
||||||
|
msgid " [Debug Connected]"
|
||||||
|
msgstr " [Отладка]"
|
||||||
|
|
||||||
|
msgid "Favorites"
|
||||||
|
msgstr "Избранное"
|
||||||
|
|
||||||
|
msgid "No favorites"
|
||||||
|
msgstr "Нет избранного"
|
||||||
|
|
||||||
|
msgid "Press * in the memorization screen to add favorites."
|
||||||
|
msgstr "Нажмите * для добавления в избранное."
|
||||||
|
|
||||||
|
msgid "Total {n} favorite(s)"
|
||||||
|
msgstr "Избранного: {n}"
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
" [d]Added: {time}\n"
|
||||||
|
" From {title}[/d]"
|
||||||
|
msgstr ""
|
||||||
|
" [d]Добавлено: {time}\n"
|
||||||
|
" Из: {title}[/d]"
|
||||||
|
|
||||||
|
msgid "Remove"
|
||||||
|
msgstr "Удалить"
|
||||||
|
|
||||||
|
msgid "Operation failed: invalid button identifier"
|
||||||
|
msgstr "Ошибка: неверный ID кнопки"
|
||||||
|
|
||||||
|
msgid "Removed favorite: {ident}"
|
||||||
|
msgstr "Удалено: {ident}"
|
||||||
|
|
||||||
|
msgid "Failed to remove: {ident}"
|
||||||
|
msgstr "Ошибка удаления: {ident}"
|
||||||
|
|
||||||
|
msgid "Switch"
|
||||||
|
msgstr "Переключить"
|
||||||
|
|
||||||
|
msgid "Cache Manager"
|
||||||
|
msgstr "Кеш"
|
||||||
|
|
||||||
|
msgid "Settings Page"
|
||||||
|
msgstr "Настройки"
|
||||||
|
|
||||||
|
msgid "Sync Tool"
|
||||||
|
msgstr "Синхр."
|
||||||
|
|
||||||
|
msgid "Exit"
|
||||||
|
msgstr "Выход"
|
||||||
|
|
||||||
|
msgid "Project Homepage"
|
||||||
|
msgstr "Сайт проекта"
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"[b]Select a function to navigate to\n"
|
||||||
|
"or a memorization session instance[/b]\n"
|
||||||
|
"\n"
|
||||||
|
"Tips will be displayed here"
|
||||||
|
msgstr ""
|
||||||
|
"[b]Выберите функцию\n"
|
||||||
|
"или сеанс[/b]\n"
|
||||||
|
"\n"
|
||||||
|
"Подсказки здесь"
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Press Enter to switch\n"
|
||||||
|
"All sessions will be saved"
|
||||||
|
msgstr ""
|
||||||
|
"Enter для переключения\n"
|
||||||
|
"Сеансы будут сохранены"
|
||||||
|
|
||||||
|
msgid "Close (n)"
|
||||||
|
msgstr "Закрыть (n)"
|
||||||
|
|
||||||
|
msgid "Prepare Repository"
|
||||||
|
msgstr "Подготовка"
|
||||||
|
|
||||||
|
msgid "Cache"
|
||||||
|
msgstr "Кеш"
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"**Ready**: `{title}`\n"
|
||||||
|
""
|
||||||
|
msgstr ""
|
||||||
|
"**Готово**: `{title}`\n"
|
||||||
|
""
|
||||||
|
|
||||||
|
msgid "Repo path: {path}"
|
||||||
|
msgstr "Путь: {path}"
|
||||||
|
|
||||||
|
msgid "Progress: {touched}/{total} [{pct}%]"
|
||||||
|
msgstr "Прогресс: {touched}/{total} [{pct}%]"
|
||||||
|
|
||||||
|
msgid "Scheduling algorithm: {algo} {desc}"
|
||||||
|
msgstr "Алгоритм: {algo} {desc}"
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Study count: {total} = {review} [d][Review][/d] + {new} [d][New][/d]\n"
|
||||||
|
""
|
||||||
|
msgstr ""
|
||||||
|
"Всего: {total} = {review} [d][Повтор][/d] + {new} [d][Новое][/d]\n"
|
||||||
|
""
|
||||||
|
|
||||||
|
msgid "Start Memorizing"
|
||||||
|
msgstr "Начать"
|
||||||
|
|
||||||
|
msgid "Manage Cache"
|
||||||
|
msgstr "Кеш"
|
||||||
|
|
||||||
|
msgid "Sync protocol: {proto}"
|
||||||
|
msgstr "Протокол: {proto}"
|
||||||
|
|
||||||
|
msgid "Server Configuration:"
|
||||||
|
msgstr "Сервер:"
|
||||||
|
|
||||||
|
msgid "Remote server:"
|
||||||
|
msgstr "Сервер:"
|
||||||
|
|
||||||
|
msgid "Remote path:"
|
||||||
|
msgstr "Путь:"
|
||||||
|
|
||||||
|
msgid "Test Connection"
|
||||||
|
msgstr "Тест"
|
||||||
|
|
||||||
|
msgid "Start Sync"
|
||||||
|
msgstr "Синхр."
|
||||||
|
|
||||||
|
msgid "Pause"
|
||||||
|
msgstr "Пауза"
|
||||||
|
|
||||||
|
msgid "Cancel"
|
||||||
|
msgstr "Отмена"
|
||||||
|
|
||||||
|
msgid "Sync Progress"
|
||||||
|
msgstr "Прогресс"
|
||||||
|
|
||||||
|
msgid "Sync Log"
|
||||||
|
msgstr "Лог"
|
||||||
|
|
||||||
|
msgid "Not configured"
|
||||||
|
msgstr "Не настроено"
|
||||||
|
|
||||||
|
msgid "Sync service ready"
|
||||||
|
msgstr "Синхр. готова"
|
||||||
|
|
||||||
|
msgid "Sync service not configured or not enabled"
|
||||||
|
msgstr "Синхр. не настроена"
|
||||||
|
|
||||||
|
msgid "Failed to update UI: {error}"
|
||||||
|
msgstr "Ошибка UI: {error}"
|
||||||
|
|
||||||
|
msgid "Failed to update status: {error}"
|
||||||
|
msgstr "Ошибка статуса: {error}"
|
||||||
|
|
||||||
|
msgid "Sync service not initialised, please check configuration"
|
||||||
|
msgstr "Синхр. не инициализирована"
|
||||||
|
|
||||||
|
msgid "Sync service not initialised"
|
||||||
|
msgstr "Не инициализирована"
|
||||||
|
|
||||||
|
msgid "Testing WebDAV connection..."
|
||||||
|
msgstr "Тест WebDAV…"
|
||||||
|
|
||||||
|
msgid "Testing connection..."
|
||||||
|
msgstr "Тест соединения…"
|
||||||
|
|
||||||
|
msgid "Connection test successful"
|
||||||
|
msgstr "Тест OK"
|
||||||
|
|
||||||
|
msgid "Connection OK"
|
||||||
|
msgstr "Соединение OK"
|
||||||
|
|
||||||
|
msgid "Connection test failed"
|
||||||
|
msgstr "Тест не удался"
|
||||||
|
|
||||||
|
msgid "Connection failed"
|
||||||
|
msgstr "Ошибка соединения"
|
||||||
|
|
||||||
|
msgid "Connection test error: {error}"
|
||||||
|
msgstr "Ошибка теста: {error}"
|
||||||
|
|
||||||
|
msgid "Connection error"
|
||||||
|
msgstr "Ошибка соединения"
|
||||||
|
|
||||||
|
msgid "Sync service not initialised, cannot start sync"
|
||||||
|
msgstr "Синхр. не иниц., запуск невозможен"
|
||||||
|
|
||||||
|
msgid "Sync already in progress"
|
||||||
|
msgstr "Синхр. уже выполняется"
|
||||||
|
|
||||||
|
msgid "Starting data sync..."
|
||||||
|
msgstr "Запуск синхр…"
|
||||||
|
|
||||||
|
msgid "Syncing..."
|
||||||
|
msgstr "Синхр…"
|
||||||
|
|
||||||
|
msgid "Syncing nucleon directory: {dir}"
|
||||||
|
msgstr "Синхр. nucleon: {dir}"
|
||||||
|
|
||||||
|
msgid "Syncing electron directory: {dir}"
|
||||||
|
msgstr "Синхр. electron: {dir}"
|
||||||
|
|
||||||
|
msgid "Syncing orbital directory: {dir}"
|
||||||
|
msgstr "Синхр. orbital: {dir}"
|
||||||
|
|
||||||
|
msgid "nucleon sync complete: uploaded {up}, downloaded {down}"
|
||||||
|
msgstr "nucleon: загружено {up}, скачано {down}"
|
||||||
|
|
||||||
|
msgid "electron sync complete: uploaded {up}, downloaded {down}"
|
||||||
|
msgstr "electron: загружено {up}, скачано {down}"
|
||||||
|
|
||||||
|
msgid "orbital sync complete: uploaded {up}, downloaded {down}"
|
||||||
|
msgstr "orbital: загружено {up}, скачано {down}"
|
||||||
|
|
||||||
|
msgid "Sync complete"
|
||||||
|
msgstr "Синхр. завершена"
|
||||||
|
|
||||||
|
msgid "All directories synced"
|
||||||
|
msgstr "Всё синхронизировано"
|
||||||
|
|
||||||
|
msgid "Error during sync: {error}"
|
||||||
|
msgstr "Ошибка синхр.: {error}"
|
||||||
|
|
||||||
|
msgid "Sync failed"
|
||||||
|
msgstr "Синхр. не удалась"
|
||||||
|
|
||||||
|
msgid "Sync paused"
|
||||||
|
msgstr "Пауза"
|
||||||
|
|
||||||
|
msgid "Sync resumed"
|
||||||
|
msgstr "Продолжено"
|
||||||
|
|
||||||
|
msgid "Sync cancelled"
|
||||||
|
msgstr "Отменено"
|
||||||
|
|
||||||
|
msgid "Resume"
|
||||||
|
msgstr "Продолжить"
|
||||||
|
|
||||||
|
msgid "Failed to update button state: {error}"
|
||||||
|
msgstr "Ошибка кнопки: {error}"
|
||||||
|
|
||||||
|
msgid "Unknown error"
|
||||||
|
msgstr "Неизв. ошибка"
|
||||||
|
|
||||||
|
msgid "[b]Audio Pre-cache[/b]"
|
||||||
|
msgstr "[b]Аудиокеш[/b]"
|
||||||
|
|
||||||
|
msgid "Cache rate: {rate:.1f}% ({cached} / {total} units)"
|
||||||
|
msgstr "Кеш: {rate:.1f}% ({cached} / {total})"
|
||||||
|
|
||||||
|
msgid "Target units from: [b]{desc}[/b]"
|
||||||
|
msgstr "Цель: [b]{desc}[/b]"
|
||||||
|
|
||||||
|
msgid "Unit count: {n}"
|
||||||
|
msgstr "Единиц: {n}"
|
||||||
|
|
||||||
|
msgid "Target: all units"
|
||||||
|
msgstr "Цель: всё"
|
||||||
|
|
||||||
|
msgid "Start Pre-cache"
|
||||||
|
msgstr "Начать"
|
||||||
|
|
||||||
|
msgid "Cancel Pre-cache"
|
||||||
|
msgstr "Отмена"
|
||||||
|
|
||||||
|
msgid "Clear Cache"
|
||||||
|
msgstr "Очистить"
|
||||||
|
|
||||||
|
msgid "Cache path: {path}"
|
||||||
|
msgstr "Путь кеша: {path}"
|
||||||
|
|
||||||
|
msgid "Files: {n}"
|
||||||
|
msgstr "Файлов: {n}"
|
||||||
|
|
||||||
|
msgid "Total size: {size}"
|
||||||
|
msgstr "Размер: {size}"
|
||||||
|
|
||||||
|
msgid "Refresh"
|
||||||
|
msgstr "Обновить"
|
||||||
|
|
||||||
|
msgid "If you leave this screen, ongoing cache processes will stop automatically."
|
||||||
|
msgstr "При выходе кеширование остановится."
|
||||||
|
|
||||||
|
msgid "Cache supports \"resume from break\"."
|
||||||
|
msgstr "Поддерживается возобновление."
|
||||||
|
|
||||||
|
msgid "Ready"
|
||||||
|
msgstr "Готово"
|
||||||
|
|
||||||
|
msgid "Waiting to start..."
|
||||||
|
msgstr "Ожидание…"
|
||||||
|
|
||||||
|
msgid "Status: {s}"
|
||||||
|
msgstr "Статус: {s}"
|
||||||
|
|
||||||
|
msgid "Current item: {item}"
|
||||||
|
msgstr "Элемент: {item}"
|
||||||
|
|
||||||
|
msgid "Processing ({i}/{total})"
|
||||||
|
msgstr "Обработка ({i}/{total})"
|
||||||
|
|
||||||
|
msgid "Error"
|
||||||
|
msgstr "Ошибка"
|
||||||
|
|
||||||
|
msgid "Failed, skipping: {item}"
|
||||||
|
msgstr "Ошибка, пропуск: {item}"
|
||||||
|
|
||||||
|
msgid "Cancelled"
|
||||||
|
msgstr "Отменено"
|
||||||
|
|
||||||
|
msgid "Pre-cache cancelled by user"
|
||||||
|
msgstr "Отменено пользователем"
|
||||||
|
|
||||||
|
msgid "Cleared"
|
||||||
|
msgstr "Очищено"
|
||||||
|
|
||||||
|
msgid "Audio cache cleared"
|
||||||
|
msgstr "Кеш очищен"
|
||||||
|
|
||||||
|
msgid "Failed to clear cache: {error}"
|
||||||
|
msgstr "Ошибка очистки: {error}"
|
||||||
|
|
||||||
|
msgid "Cache info refreshed"
|
||||||
|
msgstr "Инфо обновлено"
|
||||||
|
|
||||||
|
msgid "This memorization session is finished"
|
||||||
|
msgstr "Сеанс завершен"
|
||||||
|
|
||||||
|
msgid "Algorithm data {}"
|
||||||
|
msgstr "Данные алгоритма{}"
|
||||||
|
|
||||||
|
msgid "saved"
|
||||||
|
msgstr "сохранено"
|
||||||
|
|
||||||
|
msgid "not saved"
|
||||||
|
msgstr "не сохранено"
|
||||||
|
|
||||||
|
msgid "Back to Menu"
|
||||||
|
msgstr "В меню"
|
||||||
|
|
||||||
|
msgid "Perfect recall"
|
||||||
|
msgstr "Идеально"
|
||||||
|
|
||||||
|
msgid "Correct after hesitation"
|
||||||
|
msgstr "Верно после паузы"
|
||||||
|
|
||||||
|
msgid "Correct with difficulty"
|
||||||
|
msgstr "Верно с трудом"
|
||||||
|
|
||||||
|
msgid "Wrong but familiar"
|
||||||
|
msgstr "Неверно, но знакомо"
|
||||||
|
|
||||||
|
msgid "Wrong and unfamiliar"
|
||||||
|
msgstr "Неверно и незнакомо"
|
||||||
|
|
||||||
|
msgid "Complete blank"
|
||||||
|
msgstr "Полный провал"
|
||||||
|
|
||||||
|
msgid "Evaluate how well you remember this content: "
|
||||||
|
msgstr "Оцените запоминание: "
|
||||||
|
|
||||||
|
msgid "### Note: {note}"
|
||||||
|
msgstr "### Заметка: {note}"
|
||||||
|
|
||||||
|
msgid "I know this"
|
||||||
|
msgstr "Знаю"
|
||||||
|
|
||||||
|
msgid "Current input: {input}"
|
||||||
|
msgstr "Ввод: {input}"
|
||||||
|
|
||||||
|
msgid "Backspace"
|
||||||
|
msgstr "Забой"
|
||||||
|
|
||||||
|
msgid "Sample Label"
|
||||||
|
msgstr "Метка"
|
||||||
|
|
||||||
|
msgid "Sample Button"
|
||||||
|
msgstr "Кнопка"
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
|
<!DOCTYPE martif PUBLIC 'ISO 12200:1999A//DTD MARTIF core (DXFcdV04)//EN' 'TBXcdv04.dtd'>
|
||||||
|
<martif type="TBX" xml:lang="en_US">
|
||||||
|
<text>
|
||||||
|
<body/>
|
||||||
|
</text>
|
||||||
|
</martif>
|
||||||
Binary file not shown.
@@ -0,0 +1,679 @@
|
|||||||
|
# Chinese (Simplified) Language Pack for HeurAMS.
|
||||||
|
# Copyright (C) 2026 Wang Zhiyu
|
||||||
|
# This file is distributed under the same license as the HeurAMS project.
|
||||||
|
# Wang Zhiyu <pluvium27@outlook.com>, 2026.
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: heurams 0.5.1\n"
|
||||||
|
"Report-Msgid-Bugs-To: https://github.com/pluvium27/HeurAMS/issues\n"
|
||||||
|
"POT-Creation-Date: 2026-05-21 00:00+0000\n"
|
||||||
|
"PO-Revision-Date: 2026-05-21 00:00+0000\n"
|
||||||
|
"Last-Translator: Wang Zhiyu <pluvium27@outlook.com>\n"
|
||||||
|
"Language-Team: Chinese (Simplified) <pluvium27@outlook.com>\n"
|
||||||
|
"Language: zh_CN\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||||
|
|
||||||
|
msgid "Welcome to the basic user interface!"
|
||||||
|
msgstr "欢迎使用基本用户界面!"
|
||||||
|
|
||||||
|
msgid "Loading config and context... "
|
||||||
|
msgstr "加载配置与上下文... "
|
||||||
|
|
||||||
|
msgid "Done! ({time}ms)"
|
||||||
|
msgstr "已完成! (耗时: {time}ms)"
|
||||||
|
|
||||||
|
msgid "Loading UI framework... "
|
||||||
|
msgstr "加载用户界面框架... "
|
||||||
|
|
||||||
|
msgid "Loading UI layout... "
|
||||||
|
msgstr "加载用户界面布局... "
|
||||||
|
|
||||||
|
msgid "Component directory: {path}"
|
||||||
|
msgstr "组件目录: {path}"
|
||||||
|
|
||||||
|
msgid "Working directory: {path}"
|
||||||
|
msgstr "工作目录: {path}"
|
||||||
|
|
||||||
|
msgid "Pre-work total: {time}ms"
|
||||||
|
msgstr "前置工作共计耗时: {time}ms"
|
||||||
|
|
||||||
|
msgid "Heuristic Auxiliary Memorizing Scheduler"
|
||||||
|
msgstr "启发式辅助记忆调度器"
|
||||||
|
|
||||||
|
msgid "Quit"
|
||||||
|
msgstr "退出"
|
||||||
|
|
||||||
|
msgid "Theme"
|
||||||
|
msgstr "主题"
|
||||||
|
|
||||||
|
msgid "Navigate"
|
||||||
|
msgstr "导航"
|
||||||
|
|
||||||
|
msgid "Settings"
|
||||||
|
msgstr "设置"
|
||||||
|
|
||||||
|
msgid "About"
|
||||||
|
msgstr "关于"
|
||||||
|
|
||||||
|
msgid "HeurAMS {ver} - Heuristic Auxiliary Memorizing Scheduler"
|
||||||
|
msgstr "HeurAMS {ver} - 启发式辅助记忆调度器"
|
||||||
|
|
||||||
|
msgid "Listening address"
|
||||||
|
msgstr "监听地址"
|
||||||
|
|
||||||
|
msgid "Listening port"
|
||||||
|
msgstr "监听端口"
|
||||||
|
|
||||||
|
msgid "Development mode hot reload"
|
||||||
|
msgstr "开发模式热重载"
|
||||||
|
|
||||||
|
msgid "Show this help message"
|
||||||
|
msgstr "显示此帮助信息"
|
||||||
|
|
||||||
|
msgid "unifront API service started: http://{host}:{port}"
|
||||||
|
msgstr "unifront API 服务启动: http://{host}:{port}"
|
||||||
|
|
||||||
|
msgid "Show the version and exit."
|
||||||
|
msgstr "显示版本并退出."
|
||||||
|
|
||||||
|
msgid "Explicitly specify locale (defaults to LANG env)."
|
||||||
|
msgstr "显式指定语言 (默认使用 LANG 环境变量)."
|
||||||
|
|
||||||
|
msgid "Launch the built-in user interface (TUI)"
|
||||||
|
msgstr "启动内置基本用户界面 (TUI)"
|
||||||
|
|
||||||
|
msgid "Launch the API service (unifront)"
|
||||||
|
msgstr "启动 API 服务 (unifront)"
|
||||||
|
|
||||||
|
msgid "Dashboard"
|
||||||
|
msgstr "仪表盘"
|
||||||
|
|
||||||
|
msgid "Back"
|
||||||
|
msgstr "返回"
|
||||||
|
|
||||||
|
msgid "Current daystamp: {ds}"
|
||||||
|
msgstr "当前日时间戳: {ds}"
|
||||||
|
|
||||||
|
msgid "Timezone offset: UTC+{offset}"
|
||||||
|
msgstr "应用时区修正: UTC+{offset}"
|
||||||
|
|
||||||
|
msgid "Default algorithm: {algo}"
|
||||||
|
msgstr "默认算法设置: {algo}"
|
||||||
|
|
||||||
|
msgid "Loaded {n} repo(s)"
|
||||||
|
msgstr "已加载 {n} 个单元集"
|
||||||
|
|
||||||
|
msgid "Total {n} unit(s)"
|
||||||
|
msgstr "共计 {n} 个单元"
|
||||||
|
|
||||||
|
msgid "Activated {n} unit(s)"
|
||||||
|
msgstr "已激活 {n} 个单元"
|
||||||
|
|
||||||
|
msgid "Version {ver}-{stage}"
|
||||||
|
msgstr "版本 {ver}-{stage}"
|
||||||
|
|
||||||
|
msgid "Processed {puzzles} puzzles in {time}s, accuracy {accuracy}, speed {speed} puzzle(s)/s"
|
||||||
|
msgstr "在 {time} 秒内处理了 {puzzles} 个谜题, 正确率 {accuracy}, 平均速度 {speed} 个/秒"
|
||||||
|
|
||||||
|
msgid "N/A"
|
||||||
|
msgstr "无法求解"
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"No repo directories found in {path}.\n"
|
||||||
|
"Please import a repo and restart, or create a new one."
|
||||||
|
msgstr ""
|
||||||
|
"在 {path} 中未找到任何单元集仓库目录.\n"
|
||||||
|
"请导入单元集后重启应用, 或者新建单元集."
|
||||||
|
|
||||||
|
msgid "Start Learning"
|
||||||
|
msgstr "开始学习"
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"{title} [{algo}]\n"
|
||||||
|
" [d]Progress: {touched}/{total} ({pct}%)[/d]\n"
|
||||||
|
" [d]{status}[/d]"
|
||||||
|
msgstr ""
|
||||||
|
"{title} [{algo}]\n"
|
||||||
|
" [d]进度: {touched}/{total} ({pct}%)[/d]\n"
|
||||||
|
" [d]{status}[/d]"
|
||||||
|
|
||||||
|
msgid "Due: {review}R + {new}U"
|
||||||
|
msgstr "需要学习: {review}R + {new}U"
|
||||||
|
|
||||||
|
msgid "Not started: 0R + {new}U"
|
||||||
|
msgstr "暂未开始: 0R + {new}U"
|
||||||
|
|
||||||
|
msgid "Up to date"
|
||||||
|
msgstr "无需操作"
|
||||||
|
|
||||||
|
msgid "[b]About & Version Info[/b]"
|
||||||
|
msgstr "[b]关于与版本信息[/b]"
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"# About HeurAMS\n"
|
||||||
|
"\n"
|
||||||
|
"Main library version: `{ver}-python` \n"
|
||||||
|
"UI frontend: `Textual TUI (Basic UI)` \n"
|
||||||
|
"UI version: `{ver}` \n"
|
||||||
|
"API codename: `{codename}` \n"
|
||||||
|
"\n"
|
||||||
|
"> A heuristic auxiliary memorizing scheduler based on heuristic algorithms and cognitive science theories, designed to help users memorize and plan learning more efficiently. \n"
|
||||||
|
"> 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. \n"
|
||||||
|
"\n"
|
||||||
|
"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. \n"
|
||||||
|
"\n"
|
||||||
|
"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. \n"
|
||||||
|
"\n"
|
||||||
|
"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. \n"
|
||||||
|
"\n"
|
||||||
|
"Developers: \n"
|
||||||
|
"- Wang Zhiyu ([@pluvium27](https://github.com/pluvium27)): Project initiator and lead developer \n"
|
||||||
|
"\n"
|
||||||
|
"Special thanks to the following individuals and groups; their algorithms and theories form the cornerstone of the current software algorithms: \n"
|
||||||
|
"\n"
|
||||||
|
"- [Piotr A. Woźniak](https://supermemo.guru/wiki/Piotr_Wozniak): SM-2 algorithm and SM-15 algorithm theory \n"
|
||||||
|
"- [Jarrett Ye](https://github.com/L-M-Sherlock): FSRS algorithm and spaced repetition theory references \n"
|
||||||
|
"- [Kazuaki Tanida](https://github.com/slaypni): CoffeeScript reverse implementation of SM-15 algorithm \n"
|
||||||
|
"- [Open Spaced Repetition](https://github.com/open-spaced-repetition): FSRS algorithm underlying implementation \n"
|
||||||
|
"\n"
|
||||||
|
"# Runtime Environment\n"
|
||||||
|
"\n"
|
||||||
|
"Python interpreter version: {python_version} \n"
|
||||||
|
"Python interpreter path: {executable} \n"
|
||||||
|
"Textual framework version: {textual_version} \n"
|
||||||
|
"Terminal emulator: {terminal_info} \n"
|
||||||
|
"Operating system version: {os_version} \n"
|
||||||
|
"Disk free space: {disk_usage} \n"
|
||||||
|
"\n"
|
||||||
|
"When reporting issues, please copy this information into the issue description and attach `heurams.log` as an attachment to help developers locate the error."
|
||||||
|
msgstr ""
|
||||||
|
"# 关于 HeurAMS\n"
|
||||||
|
"\n"
|
||||||
|
"主程序库版本: `{ver}-python` \n"
|
||||||
|
"用户界面分支: `Textual TUI (基本用户界面)` \n"
|
||||||
|
"用户界面版本: `{ver}` \n"
|
||||||
|
"API 版本代号: `{codename}` \n"
|
||||||
|
"\n"
|
||||||
|
"> 一个基于启发式算法与认知科学理论的辅助记忆调度器, 旨在帮助用户更高效地进行记忆工作与学习规划. \n"
|
||||||
|
"> 一个开放, 优雅, 易于扩展的间隔重复调度器实验平台, 旨在帮助研究者更高效地进行前沿记忆算法的研究. \n"
|
||||||
|
"\n"
|
||||||
|
"您可在项目主页 https://ams.pluv27.top 获取用户指南, 开发文档与软件更新, 并参与到软件的开发与改进工作. \n"
|
||||||
|
"\n"
|
||||||
|
"以 GNU Affero 通用公共许可证 (第3版) 开放源代码, 并有一条豁免本机 API 调用的附加条款, 用于其他前端到程序库的接口调用. \n"
|
||||||
|
"\n"
|
||||||
|
"您正使用程序库内置的终端用户界面, 它是第一个全功能前端实现与程序库测试套件, 位于程序库的 interface 子目录. \n"
|
||||||
|
"\n"
|
||||||
|
"开发人员列表: \n"
|
||||||
|
"- Wang Zhiyu ([@pluvium27](https://github.com/pluvium27)): 项目发起与主要开发者 \n"
|
||||||
|
"\n"
|
||||||
|
"感谢以下人士与团体, 他们的算法与理论构成了此软件现有算法的基石: \n"
|
||||||
|
"\n"
|
||||||
|
"- [Piotr A. Woźniak](https://supermemo.guru/wiki/Piotr_Wozniak): SM-2 算法与 SM-15 算法理论 \n"
|
||||||
|
"- [Jarrett Ye](https://github.com/L-M-Sherlock): FSRS 算法与间隔重复理论文献参考 \n"
|
||||||
|
"- [Kazuaki Tanida](https://github.com/slaypni): SM-15 算法的 CoffeeScript 逆向实现 \n"
|
||||||
|
"- [Open Spaced Repetition](https://github.com/open-spaced-repetition): FSRS 算法底层实现 \n"
|
||||||
|
"\n"
|
||||||
|
"# 运行环境信息\n"
|
||||||
|
"\n"
|
||||||
|
"Python 解释器版本: {python_version} \n"
|
||||||
|
"Python 解释器路径: {executable} \n"
|
||||||
|
"Textual 框架版本: {textual_version} \n"
|
||||||
|
"终端模拟器: {terminal_info} \n"
|
||||||
|
"操作系统版本: {os_version} \n"
|
||||||
|
"存储余量: {disk_usage} \n"
|
||||||
|
"\n"
|
||||||
|
"报告问题时, 请复制这些信息到问题描述, 并上传软件日志 `heurams.log` 作为附件, 以协助开发者定位错误."
|
||||||
|
|
||||||
|
msgid "Back to Main"
|
||||||
|
msgstr "返回主界面"
|
||||||
|
|
||||||
|
msgid "Unknown"
|
||||||
|
msgstr "未知"
|
||||||
|
|
||||||
|
msgid "Requires a float"
|
||||||
|
msgstr "要求一个浮点数"
|
||||||
|
|
||||||
|
msgid "Requires a string"
|
||||||
|
msgstr "要求一个字符串"
|
||||||
|
|
||||||
|
msgid "Requires an integer"
|
||||||
|
msgstr "要求一个整数"
|
||||||
|
|
||||||
|
msgid "Unknown type"
|
||||||
|
msgstr "未知类型"
|
||||||
|
|
||||||
|
msgid "No sub-items"
|
||||||
|
msgstr "无子项"
|
||||||
|
|
||||||
|
msgid "Changes are saved immediately when you leave this page, but restart is recommended to ensure the new configuration is applied."
|
||||||
|
msgstr "退出页面时, 所作的更改会立即保存, 但仍建议重启软件以确保新的配置得到应用."
|
||||||
|
|
||||||
|
msgid "Previous"
|
||||||
|
msgstr "查看上一个"
|
||||||
|
|
||||||
|
msgid "Read Aloud"
|
||||||
|
msgstr "朗读"
|
||||||
|
|
||||||
|
msgid "Favorite"
|
||||||
|
msgstr "收藏"
|
||||||
|
|
||||||
|
msgid "Learning"
|
||||||
|
msgstr "学习中"
|
||||||
|
|
||||||
|
msgid "Correct"
|
||||||
|
msgstr "正确应答"
|
||||||
|
|
||||||
|
msgid "Incorrect"
|
||||||
|
msgstr "错误应答"
|
||||||
|
|
||||||
|
msgid "Failed to generate puzzle: {e}"
|
||||||
|
msgstr "无法生成谜题: {e}"
|
||||||
|
|
||||||
|
msgid "Favorited"
|
||||||
|
msgstr "已收藏"
|
||||||
|
|
||||||
|
msgid "Not favorited"
|
||||||
|
msgstr "未收藏"
|
||||||
|
|
||||||
|
msgid "Previous: {ident}"
|
||||||
|
msgstr "上一个: {ident}"
|
||||||
|
|
||||||
|
msgid "Are you sure? Press uppercase Q to go back."
|
||||||
|
msgstr "确定吗? 按下大写 Q 以返回."
|
||||||
|
|
||||||
|
msgid "Cannot favorite: no repo associated"
|
||||||
|
msgstr "无法收藏: 未关联仓库"
|
||||||
|
|
||||||
|
msgid "Unfavorited: {ident}"
|
||||||
|
msgstr "已取消收藏: {ident}"
|
||||||
|
|
||||||
|
msgid "Favorited: {ident}"
|
||||||
|
msgstr "已收藏: {ident}"
|
||||||
|
|
||||||
|
msgid "This function is not available during memorization. Please finish or go back first."
|
||||||
|
msgstr "功能在记忆界面中不可用, 完成或返回后再试."
|
||||||
|
|
||||||
|
msgid "Time resume corrected: {old} -> {new}"
|
||||||
|
msgstr "时间恢复已修正: {old} -> {new}"
|
||||||
|
|
||||||
|
msgid " [Debug Connected]"
|
||||||
|
msgstr " [调试已连接]"
|
||||||
|
|
||||||
|
msgid "Favorites"
|
||||||
|
msgstr "收藏夹"
|
||||||
|
|
||||||
|
msgid "No favorites"
|
||||||
|
msgstr "暂无收藏"
|
||||||
|
|
||||||
|
msgid "Press * in the memorization screen to add favorites."
|
||||||
|
msgstr "使用 * 键在记忆界面中添加收藏."
|
||||||
|
|
||||||
|
msgid "Total {n} favorite(s)"
|
||||||
|
msgstr "共 {n} 个收藏项"
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
" [d]Added: {time}\n"
|
||||||
|
" From {title}[/d]"
|
||||||
|
msgstr ""
|
||||||
|
" [d]添加于: {time}\n"
|
||||||
|
" 来自 {title}[/d]"
|
||||||
|
|
||||||
|
msgid "Remove"
|
||||||
|
msgstr "移除"
|
||||||
|
|
||||||
|
msgid "Operation failed: invalid button identifier"
|
||||||
|
msgstr "操作失败: 无效的按钮标识"
|
||||||
|
|
||||||
|
msgid "Removed favorite: {ident}"
|
||||||
|
msgstr "已移除收藏: {ident}"
|
||||||
|
|
||||||
|
msgid "Failed to remove: {ident}"
|
||||||
|
msgstr "移除失败: {ident}"
|
||||||
|
|
||||||
|
msgid "Switch"
|
||||||
|
msgstr "切换"
|
||||||
|
|
||||||
|
msgid "Cache Manager"
|
||||||
|
msgstr "缓存管理器"
|
||||||
|
|
||||||
|
msgid "Settings Page"
|
||||||
|
msgstr "设置页面"
|
||||||
|
|
||||||
|
msgid "Sync Tool"
|
||||||
|
msgstr "同步工具"
|
||||||
|
|
||||||
|
msgid "Exit"
|
||||||
|
msgstr "退出程序"
|
||||||
|
|
||||||
|
msgid "Project Homepage"
|
||||||
|
msgstr "项目主页"
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"[b]Select a function to navigate to\n"
|
||||||
|
"or a memorization session instance[/b]\n"
|
||||||
|
"\n"
|
||||||
|
"Tips will be displayed here"
|
||||||
|
msgstr ""
|
||||||
|
"[b]请选择要跳转的功能\n"
|
||||||
|
"或记忆会话实例[/b]\n"
|
||||||
|
"\n"
|
||||||
|
"将在此处显示提示"
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Press Enter to switch\n"
|
||||||
|
"All sessions will be saved"
|
||||||
|
msgstr ""
|
||||||
|
"按下回车以完成切换\n"
|
||||||
|
"所有会话将被保存"
|
||||||
|
|
||||||
|
msgid "Close (n)"
|
||||||
|
msgstr "关闭 (n)"
|
||||||
|
|
||||||
|
msgid "Prepare Repository"
|
||||||
|
msgstr "准备记忆集"
|
||||||
|
|
||||||
|
msgid "Cache"
|
||||||
|
msgstr "缓存"
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"**Ready**: `{title}`\n"
|
||||||
|
""
|
||||||
|
msgstr ""
|
||||||
|
"**准备就绪**: `{title}`\n"
|
||||||
|
""
|
||||||
|
|
||||||
|
msgid "Repo path: {path}"
|
||||||
|
msgstr "单元集路径: {path}"
|
||||||
|
|
||||||
|
msgid "Progress: {touched}/{total} [{pct}%]"
|
||||||
|
msgstr "学习完成度: {touched}/{total} [{pct}%]"
|
||||||
|
|
||||||
|
msgid "Scheduling algorithm: {algo} {desc}"
|
||||||
|
msgstr "调度算法: {algo} {desc}"
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Study count: {total} = {review} [d][Review][/d] + {new} [d][New][/d]\n"
|
||||||
|
""
|
||||||
|
msgstr ""
|
||||||
|
"学习数量: {total} = {review} [d][复习][/d] + {new} [d][新识记][/d]\n"
|
||||||
|
""
|
||||||
|
|
||||||
|
msgid "Start Memorizing"
|
||||||
|
msgstr "开始记忆"
|
||||||
|
|
||||||
|
msgid "Manage Cache"
|
||||||
|
msgstr "管理缓存"
|
||||||
|
|
||||||
|
msgid "Sync protocol: {proto}"
|
||||||
|
msgstr "同步协议: {proto}"
|
||||||
|
|
||||||
|
msgid "Server Configuration:"
|
||||||
|
msgstr "服务器配置:"
|
||||||
|
|
||||||
|
msgid "Remote server:"
|
||||||
|
msgstr "远程服务器:"
|
||||||
|
|
||||||
|
msgid "Remote path:"
|
||||||
|
msgstr "远程路径:"
|
||||||
|
|
||||||
|
msgid "Test Connection"
|
||||||
|
msgstr "测试连接"
|
||||||
|
|
||||||
|
msgid "Start Sync"
|
||||||
|
msgstr "开始同步"
|
||||||
|
|
||||||
|
msgid "Pause"
|
||||||
|
msgstr "暂停"
|
||||||
|
|
||||||
|
msgid "Cancel"
|
||||||
|
msgstr "取消"
|
||||||
|
|
||||||
|
msgid "Sync Progress"
|
||||||
|
msgstr "同步进度"
|
||||||
|
|
||||||
|
msgid "Sync Log"
|
||||||
|
msgstr "同步日志"
|
||||||
|
|
||||||
|
msgid "Not configured"
|
||||||
|
msgstr "未配置"
|
||||||
|
|
||||||
|
msgid "Sync service ready"
|
||||||
|
msgstr "同步服务已就绪"
|
||||||
|
|
||||||
|
msgid "Sync service not configured or not enabled"
|
||||||
|
msgstr "同步服务未配置或未启用"
|
||||||
|
|
||||||
|
msgid "Failed to update UI: {error}"
|
||||||
|
msgstr "更新 UI 失败: {error}"
|
||||||
|
|
||||||
|
msgid "Failed to update status: {error}"
|
||||||
|
msgstr "更新状态失败: {error}"
|
||||||
|
|
||||||
|
msgid "Sync service not initialised, please check configuration"
|
||||||
|
msgstr "同步服务未初始化, 请检查配置"
|
||||||
|
|
||||||
|
msgid "Sync service not initialised"
|
||||||
|
msgstr "同步服务未初始化"
|
||||||
|
|
||||||
|
msgid "Testing WebDAV connection..."
|
||||||
|
msgstr "正在测试 WebDAV 连接..."
|
||||||
|
|
||||||
|
msgid "Testing connection..."
|
||||||
|
msgstr "正在测试连接..."
|
||||||
|
|
||||||
|
msgid "Connection test successful"
|
||||||
|
msgstr "连接测试成功"
|
||||||
|
|
||||||
|
msgid "Connection OK"
|
||||||
|
msgstr "连接正常"
|
||||||
|
|
||||||
|
msgid "Connection test failed"
|
||||||
|
msgstr "连接测试失败"
|
||||||
|
|
||||||
|
msgid "Connection failed"
|
||||||
|
msgstr "连接失败"
|
||||||
|
|
||||||
|
msgid "Connection test error: {error}"
|
||||||
|
msgstr "连接测试异常: {error}"
|
||||||
|
|
||||||
|
msgid "Connection error"
|
||||||
|
msgstr "连接异常"
|
||||||
|
|
||||||
|
msgid "Sync service not initialised, cannot start sync"
|
||||||
|
msgstr "同步服务未初始化, 无法开始同步"
|
||||||
|
|
||||||
|
msgid "Sync already in progress"
|
||||||
|
msgstr "同步已在进行中"
|
||||||
|
|
||||||
|
msgid "Starting data sync..."
|
||||||
|
msgstr "开始同步数据..."
|
||||||
|
|
||||||
|
msgid "Syncing..."
|
||||||
|
msgstr "正在同步..."
|
||||||
|
|
||||||
|
msgid "Syncing nucleon directory: {dir}"
|
||||||
|
msgstr "同步 nucleon 目录: {dir}"
|
||||||
|
|
||||||
|
msgid "Syncing electron directory: {dir}"
|
||||||
|
msgstr "同步 electron 目录: {dir}"
|
||||||
|
|
||||||
|
msgid "Syncing orbital directory: {dir}"
|
||||||
|
msgstr "同步 orbital 目录: {dir}"
|
||||||
|
|
||||||
|
msgid "nucleon sync complete: uploaded {up}, downloaded {down}"
|
||||||
|
msgstr "nucleon 同步完成: 上传 {up} 个, 下载 {down} 个"
|
||||||
|
|
||||||
|
msgid "electron sync complete: uploaded {up}, downloaded {down}"
|
||||||
|
msgstr "electron 同步完成: 上传 {up} 个, 下载 {down} 个"
|
||||||
|
|
||||||
|
msgid "orbital sync complete: uploaded {up}, downloaded {down}"
|
||||||
|
msgstr "orbital 同步完成: 上传 {up} 个, 下载 {down} 个"
|
||||||
|
|
||||||
|
msgid "Sync complete"
|
||||||
|
msgstr "同步完成"
|
||||||
|
|
||||||
|
msgid "All directories synced"
|
||||||
|
msgstr "所有目录同步完成"
|
||||||
|
|
||||||
|
msgid "Error during sync: {error}"
|
||||||
|
msgstr "同步过程中发生错误: {error}"
|
||||||
|
|
||||||
|
msgid "Sync failed"
|
||||||
|
msgstr "同步失败"
|
||||||
|
|
||||||
|
msgid "Sync paused"
|
||||||
|
msgstr "同步已暂停"
|
||||||
|
|
||||||
|
msgid "Sync resumed"
|
||||||
|
msgstr "同步已恢复"
|
||||||
|
|
||||||
|
msgid "Sync cancelled"
|
||||||
|
msgstr "同步已取消"
|
||||||
|
|
||||||
|
msgid "Resume"
|
||||||
|
msgstr "继续"
|
||||||
|
|
||||||
|
msgid "Failed to update button state: {error}"
|
||||||
|
msgstr "更新按钮状态失败: {error}"
|
||||||
|
|
||||||
|
msgid "Unknown error"
|
||||||
|
msgstr "未知错误"
|
||||||
|
|
||||||
|
msgid "[b]Audio Pre-cache[/b]"
|
||||||
|
msgstr "[b]音频预缓存[/b]"
|
||||||
|
|
||||||
|
msgid "Cache rate: {rate:.1f}% ({cached} / {total} units)"
|
||||||
|
msgstr "缓存率: {rate:.1f}% (已缓存 {cached} / {total} 个单元)"
|
||||||
|
|
||||||
|
msgid "Target units from: [b]{desc}[/b]"
|
||||||
|
msgstr "目标单元归属: [b]{desc}[/b]"
|
||||||
|
|
||||||
|
msgid "Unit count: {n}"
|
||||||
|
msgstr "单元数量: {n}"
|
||||||
|
|
||||||
|
msgid "Target: all units"
|
||||||
|
msgstr "目标: 所有单元"
|
||||||
|
|
||||||
|
msgid "Start Pre-cache"
|
||||||
|
msgstr "开始预缓存"
|
||||||
|
|
||||||
|
msgid "Cancel Pre-cache"
|
||||||
|
msgstr "取消预缓存"
|
||||||
|
|
||||||
|
msgid "Clear Cache"
|
||||||
|
msgstr "清空缓存"
|
||||||
|
|
||||||
|
msgid "Cache path: {path}"
|
||||||
|
msgstr "缓存路径: {path}"
|
||||||
|
|
||||||
|
msgid "Files: {n}"
|
||||||
|
msgstr "文件数: {n}"
|
||||||
|
|
||||||
|
msgid "Total size: {size}"
|
||||||
|
msgstr "总大小: {size}"
|
||||||
|
|
||||||
|
msgid "Refresh"
|
||||||
|
msgstr "刷新"
|
||||||
|
|
||||||
|
msgid "If you leave this screen, ongoing cache processes will stop automatically."
|
||||||
|
msgstr "若您离开此界面, 未完成的缓存进程会自动停止."
|
||||||
|
|
||||||
|
msgid "Cache supports \"resume from break\"."
|
||||||
|
msgstr "缓存程序支持 [断点续传]."
|
||||||
|
|
||||||
|
msgid "Ready"
|
||||||
|
msgstr "就绪"
|
||||||
|
|
||||||
|
msgid "Waiting to start..."
|
||||||
|
msgstr "等待开始..."
|
||||||
|
|
||||||
|
msgid "Status: {s}"
|
||||||
|
msgstr "状态: {s}"
|
||||||
|
|
||||||
|
msgid "Current item: {item}"
|
||||||
|
msgstr "当前项目: {item}"
|
||||||
|
|
||||||
|
msgid "Processing ({i}/{total})"
|
||||||
|
msgstr "正处理 ({i}/{total})"
|
||||||
|
|
||||||
|
msgid "Error"
|
||||||
|
msgstr "出错"
|
||||||
|
|
||||||
|
msgid "Failed, skipping: {item}"
|
||||||
|
msgstr "处理失败, 已跳过: {item}"
|
||||||
|
|
||||||
|
msgid "Cancelled"
|
||||||
|
msgstr "已取消"
|
||||||
|
|
||||||
|
msgid "Pre-cache cancelled by user"
|
||||||
|
msgstr "预缓存操作被用户取消"
|
||||||
|
|
||||||
|
msgid "Cleared"
|
||||||
|
msgstr "已清空"
|
||||||
|
|
||||||
|
msgid "Audio cache cleared"
|
||||||
|
msgstr "音频缓存已清空"
|
||||||
|
|
||||||
|
msgid "Failed to clear cache: {error}"
|
||||||
|
msgstr "清空缓存失败: {error}"
|
||||||
|
|
||||||
|
msgid "Cache info refreshed"
|
||||||
|
msgstr "缓存信息已刷新"
|
||||||
|
|
||||||
|
msgid "This memorization session is finished"
|
||||||
|
msgstr "本次记忆进程结束"
|
||||||
|
|
||||||
|
msgid "Algorithm data {}"
|
||||||
|
msgstr "算法数据{}"
|
||||||
|
|
||||||
|
msgid "saved"
|
||||||
|
msgstr "已保存"
|
||||||
|
|
||||||
|
msgid "not saved"
|
||||||
|
msgstr "未能保存"
|
||||||
|
|
||||||
|
msgid "Back to Menu"
|
||||||
|
msgstr "返回上一级"
|
||||||
|
|
||||||
|
msgid "Perfect recall"
|
||||||
|
msgstr "完美回想"
|
||||||
|
|
||||||
|
msgid "Correct after hesitation"
|
||||||
|
msgstr "犹豫后正确"
|
||||||
|
|
||||||
|
msgid "Correct with difficulty"
|
||||||
|
msgstr "困难地正确"
|
||||||
|
|
||||||
|
msgid "Wrong but familiar"
|
||||||
|
msgstr "错误但熟悉"
|
||||||
|
|
||||||
|
msgid "Wrong and unfamiliar"
|
||||||
|
msgstr "错误且不熟"
|
||||||
|
|
||||||
|
msgid "Complete blank"
|
||||||
|
msgstr "完全空白"
|
||||||
|
|
||||||
|
msgid "Evaluate how well you remember this content: "
|
||||||
|
msgstr "请评估你对这个内容的记忆程度: "
|
||||||
|
|
||||||
|
msgid "### Note: {note}"
|
||||||
|
msgstr "### 笔记: {note}"
|
||||||
|
|
||||||
|
msgid "I know this"
|
||||||
|
msgstr "我已知晓"
|
||||||
|
|
||||||
|
msgid "Current input: {input}"
|
||||||
|
msgstr "当前输入: {input}"
|
||||||
|
|
||||||
|
msgid "Backspace"
|
||||||
|
msgstr "退格"
|
||||||
|
|
||||||
|
msgid "Sample Label"
|
||||||
|
msgstr "示例标签"
|
||||||
|
|
||||||
|
msgid "Sample Button"
|
||||||
|
msgstr "示例按钮"
|
||||||
@@ -11,4 +11,4 @@ __all__ = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
providers = {"termux": termux_audio, "playsound": playsound_audio}
|
providers = {"termux": termux_audio, "playsound": playsound_audio}
|
||||||
logger.debug("音频 providers 已注册: %s", list(providers.keys()))
|
logger.debug("Audio providers registered: %s", list(providers.keys()))
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ logger = get_logger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
def play_by_path(path: pathlib.Path):
|
def play_by_path(path: pathlib.Path):
|
||||||
logger.debug("playsound_audio.play_by_path: 开始播放 %s", path)
|
logger.debug("playsound_audio.play_by_path: playing %s", path)
|
||||||
try:
|
try:
|
||||||
import playsound3
|
import playsound3
|
||||||
playsound3.playsound(str(path))
|
playsound3.playsound(str(path))
|
||||||
logger.debug("播放完成: %s", path)
|
logger.debug("Audio playing finished: %s", path)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("播放失败: %s, 错误: %s", path, e)
|
logger.error("Failed to play: %s, error: %s", path, e)
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ logger = get_logger(__name__)
|
|||||||
# from .protocol import PlayFunctionProtocol
|
# from .protocol import PlayFunctionProtocol
|
||||||
|
|
||||||
def play_by_path(path: pathlib.Path):
|
def play_by_path(path: pathlib.Path):
|
||||||
logger.debug("termux_audio.play_by_path: 开始播放 %s", path)
|
logger.debug("termux_audio.play_by_path: playing %s", path)
|
||||||
try:
|
try:
|
||||||
os.system(f"play-audio {path.resolve()}")
|
os.system(f"play-audio {path.resolve()}")
|
||||||
logger.debug("播放命令已执行: %s", path)
|
logger.debug("Play audio: %s", path)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("播放失败: %s, 错误: %s", path, e)
|
logger.error("Failed to play audio: %s, error: %s", path, e)
|
||||||
|
|||||||
@@ -16,4 +16,4 @@ providers = {
|
|||||||
"openai": OpenAILLM,
|
"openai": OpenAILLM,
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug("LLM providers 已注册: %s", list(providers.keys()))
|
logger.debug("LLM providers registered: %s", list(providers.keys()))
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
"""LLM 提供者基类"""
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from typing import Any, Dict, List
|
|
||||||
|
|
||||||
from heurams.services.logger import get_logger
|
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class BaseLLM:
|
|
||||||
"""LLM 提供者基类"""
|
|
||||||
|
|
||||||
name = "BaseLLM"
|
|
||||||
|
|
||||||
def __init__(self, config: Dict[str, Any]):
|
|
||||||
"""初始化 LLM 提供者
|
|
||||||
|
|
||||||
Args:
|
|
||||||
config: 提供者配置字典
|
|
||||||
"""
|
|
||||||
self.config = config
|
|
||||||
logger.debug("BaseLLM 初始化完成")
|
|
||||||
|
|
||||||
async def chat(self, messages: List[Dict[str, str]], **kwargs) -> str:
|
|
||||||
"""发送聊天消息并获取响应
|
|
||||||
|
|
||||||
Args:
|
|
||||||
messages: 消息列表, 每个消息为 {"role": "user"|"assistant"|"system", "content": "消息内容"}
|
|
||||||
**kwargs: 其他参数, 如 temperature, max_tokens 等
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
模型返回的文本响应
|
|
||||||
"""
|
|
||||||
logger.debug("BaseLLM.chat: messages=%d, kwargs=%s", len(messages), kwargs)
|
|
||||||
logger.warning("BaseLLM.chat 是基类方法, 未实现具体功能")
|
|
||||||
await asyncio.sleep(0) # 避免未使用异步的警告
|
|
||||||
return "BaseLLM 未实现具体功能"
|
|
||||||
|
|
||||||
async def chat_stream(self, messages: List[Dict[str, str]], **kwargs):
|
|
||||||
"""流式聊天(可选实现)
|
|
||||||
|
|
||||||
Args:
|
|
||||||
messages: 消息列表
|
|
||||||
**kwargs: 其他参数
|
|
||||||
|
|
||||||
Yields:
|
|
||||||
流式响应的文本块
|
|
||||||
"""
|
|
||||||
logger.debug(
|
|
||||||
"BaseLLM.chat_stream: messages=%d, kwargs=%s", len(messages), kwargs
|
|
||||||
)
|
|
||||||
logger.warning("BaseLLM.chat_stream 是基类方法, 未实现具体功能")
|
|
||||||
await asyncio.sleep(0)
|
|
||||||
yield "BaseLLM 未实现流式功能"
|
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
"""OpenAI 兼容 LLM 提供者"""
|
|
||||||
|
|
||||||
from typing import Any, AsyncGenerator, Dict, List
|
|
||||||
|
|
||||||
from heurams.services.logger import get_logger
|
|
||||||
|
|
||||||
from .base import BaseLLM
|
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class OpenAILLM(BaseLLM):
|
|
||||||
"""OpenAI 兼容 LLM 提供者"""
|
|
||||||
|
|
||||||
name = "OpenAI"
|
|
||||||
|
|
||||||
def __init__(self, config: Dict[str, Any]):
|
|
||||||
super().__init__(config)
|
|
||||||
self.api_key = config.get("key", "")
|
|
||||||
self.base_url = config.get("url", "https://api.openai.com/v1")
|
|
||||||
self._client = None
|
|
||||||
logger.debug("OpenAILLM 初始化完成: base_url=%s", self.base_url)
|
|
||||||
|
|
||||||
def _get_client(self):
|
|
||||||
"""获取 OpenAI 客户端(延迟导入)"""
|
|
||||||
if self._client is None:
|
|
||||||
try:
|
|
||||||
from openai import AsyncOpenAI
|
|
||||||
except ImportError:
|
|
||||||
logger.error("未安装 openai 库, 请运行: pip install openai")
|
|
||||||
raise ImportError("未安装 openai 库, 请运行: pip install openai")
|
|
||||||
|
|
||||||
self._client = AsyncOpenAI(
|
|
||||||
api_key=self.api_key if self.api_key else None,
|
|
||||||
base_url=self.base_url if self.base_url else None,
|
|
||||||
)
|
|
||||||
return self._client
|
|
||||||
|
|
||||||
async def chat(self, messages: List[Dict[str, str]], **kwargs) -> str:
|
|
||||||
"""发送聊天消息并获取响应"""
|
|
||||||
logger.debug("OpenAILLM.chat: messages=%d", len(messages))
|
|
||||||
|
|
||||||
client = self._get_client()
|
|
||||||
|
|
||||||
# 默认参数
|
|
||||||
default_kwargs = {
|
|
||||||
"model": kwargs.get("model", "gpt-3.5-turbo"),
|
|
||||||
"temperature": kwargs.get("temperature", 0.7),
|
|
||||||
"max_tokens": kwargs.get("max_tokens", 1000),
|
|
||||||
}
|
|
||||||
|
|
||||||
# 合并参数, 优先使用传入的 kwargs
|
|
||||||
request_kwargs = {**default_kwargs, **kwargs}
|
|
||||||
request_kwargs["messages"] = messages
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = await client.chat.completions.create(**request_kwargs)
|
|
||||||
content = response.choices[0].message.content
|
|
||||||
logger.debug(
|
|
||||||
"OpenAILLM.chat 成功: response length=%d",
|
|
||||||
len(content) if content else 0,
|
|
||||||
)
|
|
||||||
return content or ""
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("OpenAILLM.chat 失败: %s", e)
|
|
||||||
raise
|
|
||||||
|
|
||||||
async def chat_stream(
|
|
||||||
self, messages: List[Dict[str, str]], **kwargs
|
|
||||||
) -> AsyncGenerator[str, None]:
|
|
||||||
"""流式聊天"""
|
|
||||||
logger.debug("OpenAILLM.chat_stream: messages=%d", len(messages))
|
|
||||||
|
|
||||||
client = self._get_client()
|
|
||||||
|
|
||||||
# 默认参数
|
|
||||||
default_kwargs = {
|
|
||||||
"model": kwargs.get("model", "gpt-3.5-turbo"),
|
|
||||||
"temperature": kwargs.get("temperature", 0.7),
|
|
||||||
"max_tokens": kwargs.get("max_tokens", 1000),
|
|
||||||
"stream": True,
|
|
||||||
}
|
|
||||||
|
|
||||||
# 合并参数
|
|
||||||
request_kwargs = {**default_kwargs, **kwargs}
|
|
||||||
request_kwargs["messages"] = messages
|
|
||||||
|
|
||||||
try:
|
|
||||||
stream = await client.chat.completions.create(**request_kwargs)
|
|
||||||
async for chunk in stream:
|
|
||||||
if chunk.choices[0].delta.content:
|
|
||||||
yield chunk.choices[0].delta.content
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("OpenAILLM.chat_stream 失败: %s", e)
|
|
||||||
raise
|
|
||||||
@@ -15,4 +15,4 @@ providers = {
|
|||||||
"edgetts": EdgeTTS,
|
"edgetts": EdgeTTS,
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug("TTS providers 已注册: %s", list(providers.keys()))
|
logger.debug("TTS providers registered: %s", list(providers.keys()))
|
||||||
|
|||||||
@@ -12,5 +12,5 @@ class BaseTTS:
|
|||||||
def convert(cls, text: str, path: pathlib.Path | str = "") -> pathlib.Path:
|
def convert(cls, text: str, path: pathlib.Path | str = "") -> pathlib.Path:
|
||||||
"""path 是可选参数, 不填则自动返回生成文件路径"""
|
"""path 是可选参数, 不填则自动返回生成文件路径"""
|
||||||
logger.debug("BaseTTS.convert: text length=%d, path=%s", len(text), path)
|
logger.debug("BaseTTS.convert: text length=%d, path=%s", len(text), path)
|
||||||
logger.warning("BaseTTS.convert 是基类方法, 未实现具体功能")
|
logger.warning("BaseTTS.convert is not a functional implementation")
|
||||||
return path # type: ignore
|
return path # type: ignore
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ class EdgeTTS(BaseTTS):
|
|||||||
text,
|
text,
|
||||||
config_var.get()["providers"]["tts"]["edgetts"]["voice"],
|
config_var.get()["providers"]["tts"]["edgetts"]["voice"],
|
||||||
)
|
)
|
||||||
logger.debug("EdgeTTS 通信对象创建成功, 正在保存音频")
|
logger.debug("EdgeTTS object created, saving audio")
|
||||||
communicate.save_sync(str(path))
|
communicate.save_sync(str(path))
|
||||||
logger.debug("EdgeTTS 音频已保存到: %s", path)
|
logger.debug("EdgeTTS audio saved as %s", path)
|
||||||
return path # type: ignore
|
return path # type: ignore
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("EdgeTTS.convert 失败: %s", e)
|
logger.error("EdgeTTS.convert failed: %s", e)
|
||||||
raise
|
raise
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ logger = get_logger(__name__)
|
|||||||
play_by_path: Callable = prov[
|
play_by_path: Callable = prov[
|
||||||
config_var.get()["services"]["audio"]["provider"]
|
config_var.get()["services"]["audio"]["provider"]
|
||||||
].play_by_path
|
].play_by_path
|
||||||
logger.debug(
|
logger.info(
|
||||||
"音频服务初始化完成, 使用 Provider: %s",
|
"TTS Service inited, using provider %s",
|
||||||
config_var.get()["services"]["audio"]["provider"],
|
config_var.get()["services"]["audio"]["provider"],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ class ConfigDict(UserDict):
|
|||||||
if i.suffix == ".toml":
|
if i.suffix == ".toml":
|
||||||
self.data[i.stem] = i
|
self.data[i.stem] = i
|
||||||
else:
|
else:
|
||||||
logger.debug(f"配置目录中有无效的文件 {i.stem}") # what's up bro
|
logger.error(f"Illegal file detected in config: {i.stem}") # what's up bro
|
||||||
|
|
||||||
def persist(self):
|
def persist(self):
|
||||||
if self.is_dir:
|
if self.is_dir:
|
||||||
@@ -92,7 +92,7 @@ class ConfigDict(UserDict):
|
|||||||
j = self[i]
|
j = self[i]
|
||||||
if isinstance(j, ConfigDict):
|
if isinstance(j, ConfigDict):
|
||||||
j.persist()
|
j.persist()
|
||||||
logger.debug("完成配置持久化")
|
logger.info("Data persisted")
|
||||||
return
|
return
|
||||||
|
|
||||||
with open(self.path, "w+") as f:
|
with open(self.path, "w+") as f:
|
||||||
|
|||||||
@@ -19,14 +19,12 @@ def epath(
|
|||||||
path = path.lstrip(".")
|
path = path.lstrip(".")
|
||||||
target = dct
|
target = dct
|
||||||
keys = path.split(".")
|
keys = path.split(".")
|
||||||
logger.debug(f"处理 EPATH {path}, {new_value}")
|
logger.debug(f"Proceeding EPATH {path}, {new_value}")
|
||||||
for idx, i in enumerate(keys):
|
for idx, i in enumerate(keys):
|
||||||
is_last = idx == len(keys) - 1
|
is_last = idx == len(keys) - 1
|
||||||
|
|
||||||
# 处理字典键
|
# 处理字典键
|
||||||
logger.debug(
|
logger.debug(f"Proceeding in detail: {i}, {(isinstance(target, dict) or isinstance(target, ConfigDict))} {i in target}")
|
||||||
f"处理 {i}, {(isinstance(target, dict) or isinstance(target, ConfigDict))} {i in target}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if is_last and enable_modify:
|
if is_last and enable_modify:
|
||||||
# 最后一次循环执行修改
|
# 最后一次循环执行修改
|
||||||
|
|||||||
@@ -73,9 +73,9 @@ class FavoriteManager:
|
|||||||
with open(self._file_path, "r", encoding="utf-8") as f:
|
with open(self._file_path, "r", encoding="utf-8") as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
self._favorites = [FavoriteItem.from_dict(item) for item in data]
|
self._favorites = [FavoriteItem.from_dict(item) for item in data]
|
||||||
logger.debug("收藏列表加载成功, 共 %d 项", len(self._favorites))
|
logger.info("Finished loading favlist, %d items in total", len(self._favorites))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("加载收藏列表失败: %s", e)
|
logger.error("Filed to load favlist: %s", e)
|
||||||
self._favorites = []
|
self._favorites = []
|
||||||
else:
|
else:
|
||||||
self._favorites = []
|
self._favorites = []
|
||||||
@@ -86,9 +86,9 @@ class FavoriteManager:
|
|||||||
data = [item.to_dict() for item in self._favorites]
|
data = [item.to_dict() for item in self._favorites]
|
||||||
with open(self._file_path, "w", encoding="utf-8") as f:
|
with open(self._file_path, "w", encoding="utf-8") as f:
|
||||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||||
logger.debug("收藏列表保存成功, 共 %d 项", len(self._favorites))
|
logger.info("Finished saving favlist, %d items in total", len(self._favorites))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("保存收藏列表失败: %s", e)
|
logger.error("Failed to save favlist: %s", e)
|
||||||
|
|
||||||
def add(self, repo_path: str, ident: str, tags: List[str] | None = None) -> bool:
|
def add(self, repo_path: str, ident: str, tags: List[str] | None = None) -> bool:
|
||||||
"""添加收藏
|
"""添加收藏
|
||||||
@@ -103,7 +103,7 @@ class FavoriteManager:
|
|||||||
# 检查是否已存在
|
# 检查是否已存在
|
||||||
for item in self._favorites:
|
for item in self._favorites:
|
||||||
if item.repo_path == repo_path and item.ident == ident:
|
if item.repo_path == repo_path and item.ident == ident:
|
||||||
logger.debug("收藏已存在: %s/%s", repo_path, ident)
|
logger.info("Favitem already exists: %s/%s", repo_path, ident)
|
||||||
return False
|
return False
|
||||||
item = FavoriteItem(
|
item = FavoriteItem(
|
||||||
repo_path=repo_path,
|
repo_path=repo_path,
|
||||||
@@ -113,7 +113,7 @@ class FavoriteManager:
|
|||||||
)
|
)
|
||||||
self._favorites.append(item)
|
self._favorites.append(item)
|
||||||
self.save()
|
self.save()
|
||||||
logger.info("添加收藏: %s/%s", repo_path, ident)
|
logger.info("Add favitem: %s/%s", repo_path, ident)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def remove(self, repo_path: str, ident: str) -> bool:
|
def remove(self, repo_path: str, ident: str) -> bool:
|
||||||
@@ -126,9 +126,9 @@ class FavoriteManager:
|
|||||||
if item.repo_path == repo_path and item.ident == ident:
|
if item.repo_path == repo_path and item.ident == ident:
|
||||||
del self._favorites[idx]
|
del self._favorites[idx]
|
||||||
self.save()
|
self.save()
|
||||||
logger.info("移除收藏: %s/%s", repo_path, ident)
|
logger.info("Remove favitem: %s/%s", repo_path, ident)
|
||||||
return True
|
return True
|
||||||
logger.debug("收藏不存在: %s/%s", repo_path, ident)
|
logger.error("Non-existed favitem: %s/%s", repo_path, ident)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def has(self, repo_path: str, ident: str) -> bool:
|
def has(self, repo_path: str, ident: str) -> bool:
|
||||||
@@ -150,7 +150,7 @@ class FavoriteManager:
|
|||||||
"""清空收藏列表"""
|
"""清空收藏列表"""
|
||||||
self._favorites = []
|
self._favorites = []
|
||||||
self.save()
|
self.save()
|
||||||
logger.info("清空收藏列表")
|
logger.info("Clear favlist")
|
||||||
|
|
||||||
def count(self) -> int:
|
def count(self) -> int:
|
||||||
"""收藏总数"""
|
"""收藏总数"""
|
||||||
|
|||||||
@@ -6,15 +6,11 @@ logger = get_logger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
def get_md5(text):
|
def get_md5(text):
|
||||||
logger.debug(f"计算MD5哈希, 输入`{text}`")
|
logger.debug(f"MD5 hash input`{text}`")
|
||||||
result = hashlib.md5(text.encode("utf-8")).hexdigest()
|
result = hashlib.md5(text.encode("utf-8")).hexdigest()
|
||||||
logger.debug("哈希结果: %s...", result[:8])
|
logger.debug("Providing MD5 hash: %s...", result[:8])
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def hash(text):
|
def hash(text):
|
||||||
# logger.debug(f"计算MD5-时间复合哈希, 输入`{text}`")
|
|
||||||
# result = hashlib.md5(f"{text}{random.randint(0,1000)}".encode("utf-8")).hexdigest()
|
|
||||||
# logger.debug("哈希结果: %s...", result[:8])
|
|
||||||
# return result
|
|
||||||
return get_md5(text)
|
return get_md5(text)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""
|
"""日志服务模块
|
||||||
HeurAMS 日志服务模块
|
|
||||||
基于Python标准logging库, 提供统一的日志记录功能
|
基于 logging 库, 提供统一日志记录功能
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@@ -8,26 +8,24 @@ import logging.handlers
|
|||||||
import pathlib
|
import pathlib
|
||||||
from typing import Optional, Union
|
from typing import Optional, Union
|
||||||
|
|
||||||
# 默认配置
|
|
||||||
DEFAULT_LOG_LEVEL = logging.DEBUG
|
DEFAULT_LOG_LEVEL = logging.DEBUG
|
||||||
DEFAULT_LOG_FILE = pathlib.Path("heurams.log")
|
DEFAULT_LOG_FILE = pathlib.Path("heurams.log")
|
||||||
DEFAULT_LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
DEFAULT_LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - [%(filename)s:%(lineno)d:%(funcName)s] - %(message)s"
|
||||||
DEFAULT_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
|
DEFAULT_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
|
||||||
|
|
||||||
# 全局logger缓存
|
# 全局logger缓存
|
||||||
_loggers = {}
|
_loggers = {}
|
||||||
|
|
||||||
|
|
||||||
def setup_logging(
|
def setup_logging(
|
||||||
log_file: Union[str, pathlib.Path] = DEFAULT_LOG_FILE,
|
log_file: Union[str, pathlib.Path] = DEFAULT_LOG_FILE,
|
||||||
log_level: int = DEFAULT_LOG_LEVEL,
|
log_level: int = DEFAULT_LOG_LEVEL,
|
||||||
log_format: str = DEFAULT_LOG_FORMAT,
|
log_format: str = DEFAULT_LOG_FORMAT,
|
||||||
date_format: str = DEFAULT_DATE_FORMAT,
|
date_format: str = DEFAULT_DATE_FORMAT,
|
||||||
max_bytes: int = 10 * 1024 * 1024, # 10MB
|
max_bytes: int = 16 * 1024 * 1024, # 16MB
|
||||||
backup_count: int = 5,
|
backup_count: int = 5,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
配置全局日志系统
|
设置全局日志服务
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
log_file: 日志文件路径
|
log_file: 日志文件路径
|
||||||
@@ -44,7 +42,7 @@ def setup_logging(
|
|||||||
# 创建formatter
|
# 创建formatter
|
||||||
formatter = logging.Formatter(log_format, date_format)
|
formatter = logging.Formatter(log_format, date_format)
|
||||||
|
|
||||||
# 创建文件handler(使用RotatingFileHandler防止日志过大)
|
# 创建文件 handler (RotatingFileHandler)
|
||||||
file_handler = logging.handlers.RotatingFileHandler(
|
file_handler = logging.handlers.RotatingFileHandler(
|
||||||
filename=log_path,
|
filename=log_path,
|
||||||
maxBytes=max_bytes,
|
maxBytes=max_bytes,
|
||||||
@@ -54,7 +52,6 @@ def setup_logging(
|
|||||||
file_handler.setFormatter(formatter)
|
file_handler.setFormatter(formatter)
|
||||||
file_handler.setLevel(log_level)
|
file_handler.setLevel(log_level)
|
||||||
|
|
||||||
# 配置root logger - 设置为 WARNING 级别(只记录重要信息)
|
|
||||||
root_logger = logging.getLogger()
|
root_logger = logging.getLogger()
|
||||||
root_logger.setLevel(logging.WARNING) # 这里改为 WARNING
|
root_logger.setLevel(logging.WARNING) # 这里改为 WARNING
|
||||||
|
|
||||||
@@ -62,40 +59,28 @@ def setup_logging(
|
|||||||
for handler in root_logger.handlers[:]:
|
for handler in root_logger.handlers[:]:
|
||||||
root_logger.removeHandler(handler)
|
root_logger.removeHandler(handler)
|
||||||
|
|
||||||
# 创建自己的应用logger(单独设置DEBUG级别)
|
# 创建 heurams logger 并单独设置 DEBUG 级别
|
||||||
app_logger = logging.getLogger("heurams")
|
app_logger = logging.getLogger("heurams")
|
||||||
app_logger.setLevel(log_level) # 保持DEBUG级别
|
app_logger.setLevel(log_level) # 保持DEBUG级别
|
||||||
app_logger.addHandler(file_handler)
|
app_logger.addHandler(file_handler)
|
||||||
|
|
||||||
# 禁止传播到root logger, 避免双重记录
|
# 禁止传播到 root logger, 避免双重记录
|
||||||
app_logger.propagate = False
|
app_logger.propagate = False
|
||||||
|
|
||||||
# 设置第三方库的日志级别为WARNING, 避免调试信息干扰
|
|
||||||
third_party_loggers = [
|
|
||||||
"markdown_it",
|
|
||||||
"markdown_it.rules_block",
|
|
||||||
"markdown_it.rules_core",
|
|
||||||
"markdown_it.rules_inline",
|
|
||||||
"asyncio",
|
|
||||||
]
|
|
||||||
|
|
||||||
for logger_name in third_party_loggers:
|
|
||||||
logging.getLogger(logger_name).setLevel(logging.WARNING)
|
|
||||||
|
|
||||||
# 记录日志系统初始化
|
# 记录日志系统初始化
|
||||||
app_logger.info("日志系统已初始化, 日志文件: %s", log_path)
|
app_logger.debug("HeurAMS logger inited, path: %s", log_path.resolve())
|
||||||
|
|
||||||
|
|
||||||
def get_logger(name: Optional[str] = None) -> logging.Logger:
|
def get_logger(name: Optional[str] = None) -> logging.Logger:
|
||||||
"""
|
"""
|
||||||
获取指定名称的logger
|
获取指定名称的 logger
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name: logger名称, 通常使用模块名(__name__)
|
name: logger名称, 通常使用模块名(__name__)
|
||||||
如果为None, 返回root logger
|
如果为None, 返回 root logger
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
logging.Logger实例
|
logging.Logger 实例
|
||||||
"""
|
"""
|
||||||
if name is None:
|
if name is None:
|
||||||
return logging.getLogger()
|
return logging.getLogger()
|
||||||
@@ -106,49 +91,12 @@ def get_logger(name: Optional[str] = None) -> logging.Logger:
|
|||||||
else:
|
else:
|
||||||
logger_name = name
|
logger_name = name
|
||||||
|
|
||||||
# 缓存logger以提高性能
|
# 缓存 logger 以提高性能, 以模块为单位的单例
|
||||||
if logger_name not in _loggers:
|
if logger_name not in _loggers:
|
||||||
logger = logging.getLogger(logger_name)
|
logger = logging.getLogger(logger_name)
|
||||||
_loggers[logger_name] = logger
|
_loggers[logger_name] = logger
|
||||||
|
|
||||||
return _loggers[logger_name]
|
return _loggers[logger_name]
|
||||||
|
|
||||||
|
# 初始化日志系统
|
||||||
# 便捷函数
|
setup_logging()
|
||||||
def debug(msg: str, *args, **kwargs) -> None:
|
|
||||||
"""DEBUG级别日志"""
|
|
||||||
get_logger().debug(msg, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def info(msg: str, *args, **kwargs) -> None:
|
|
||||||
"""INFO级别日志"""
|
|
||||||
get_logger().info(msg, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def warning(msg: str, *args, **kwargs) -> None:
|
|
||||||
"""WARNING级别日志"""
|
|
||||||
get_logger().warning(msg, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def error(msg: str, *args, **kwargs) -> None:
|
|
||||||
"""ERROR级别日志"""
|
|
||||||
get_logger().error(msg, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def critical(msg: str, *args, **kwargs) -> None:
|
|
||||||
"""CRITICAL级别日志"""
|
|
||||||
get_logger().critical(msg, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def exception(msg: str, *args, **kwargs) -> None:
|
|
||||||
"""记录异常信息 (ERROR级别)"""
|
|
||||||
get_logger().exception(msg, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
# 初始化日志系统(硬编码配置)
|
|
||||||
setup_logging()
|
|
||||||
|
|
||||||
|
|
||||||
# 模块级别的logger实例
|
|
||||||
logger = get_logger(__name__)
|
|
||||||
logger.info("HeurAMS日志服务模块已加载")
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
# 时间服务
|
"""时间服务
|
||||||
|
"""
|
||||||
import datetime
|
import datetime
|
||||||
import time
|
import time
|
||||||
|
|
||||||
@@ -7,32 +8,40 @@ from heurams.services.logger import get_logger
|
|||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
daystamp_override = config_var.get()["services"]["timer"]["daystamp_override"]
|
||||||
|
timestamp_override = config_var.get()["services"]["timer"]["timestamp_override"]
|
||||||
|
last_daystamp = 0
|
||||||
|
last_timestamp = 0
|
||||||
|
|
||||||
def get_daystamp() -> int:
|
def get_daystamp() -> int:
|
||||||
"""获取当前日戳(以天为单位的整数时间戳)"""
|
"""获取当前日戳(以天为单位的整数时间戳)"""
|
||||||
time_override = config_var.get()["services"]["timer"]["daystamp_override"]
|
if daystamp_override != -1:
|
||||||
if time_override != -1:
|
logger.debug("Daystamp overrode: %d", daystamp_override)
|
||||||
logger.debug("使用覆盖的日戳: %d", time_override)
|
return int(daystamp_override)
|
||||||
return int(time_override)
|
|
||||||
|
|
||||||
result = int(
|
result = int(
|
||||||
(time.time() + config_var.get()["services"]["timer"]["timezone_offset"])
|
(time.time() + config_var.get()["services"]["timer"]["timezone_offset"])
|
||||||
// (24 * 3600)
|
// (24 * 3600)
|
||||||
)
|
)
|
||||||
logger.debug("计算日戳: %d", result)
|
global last_daystamp
|
||||||
|
if last_daystamp != result: # 用于避免日志泛洪
|
||||||
|
logger.debug("Providing new daystamp: %d", result)
|
||||||
|
last_daystamp = result
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def get_timestamp() -> float:
|
def get_timestamp() -> float:
|
||||||
"""获取 UNIX 时间戳"""
|
"""获取 UNIX 时间戳"""
|
||||||
# 搞这个函数的原因是要支持可复现操作
|
# 搞这个函数的原因是要支持可复现操作
|
||||||
time_override = config_var.get()["services"]["timer"]["timestamp_override"]
|
if timestamp_override != -1:
|
||||||
if time_override != -1:
|
logger.debug("Timestamp overrode: %f", timestamp_override)
|
||||||
logger.debug("使用覆盖的时间戳: %f", time_override)
|
return float(timestamp_override)
|
||||||
return float(time_override)
|
|
||||||
|
|
||||||
result = time.time()
|
result = time.time()
|
||||||
logger.debug("获取当前时间戳: %f", result)
|
global last_timestamp
|
||||||
|
if last_timestamp != result:
|
||||||
|
logger.debug("Providing new timestamp: %d", result)
|
||||||
|
last_timestamp = result
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@@ -49,9 +58,9 @@ def daystamp_to_datetime(daystamp: int) -> datetime.datetime:
|
|||||||
|
|
||||||
|
|
||||||
def datetime_to_daystamp(dt: datetime.datetime) -> int:
|
def datetime_to_daystamp(dt: datetime.datetime) -> int:
|
||||||
"""将 datetime 转换为日戳(从 1970-01-01 起的天数)
|
"""将 datetime 转换为日戳 (从 1970-01-01 起的天数)
|
||||||
|
|
||||||
接受带时区或 naive 的 datetime(naive 视为 UTC)。
|
接受带时区或 naive 的 datetime (naive 视为 UTC)
|
||||||
"""
|
"""
|
||||||
epoch = datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc)
|
epoch = datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc)
|
||||||
if dt.tzinfo is None:
|
if dt.tzinfo is None:
|
||||||
@@ -61,5 +70,5 @@ def datetime_to_daystamp(dt: datetime.datetime) -> int:
|
|||||||
|
|
||||||
|
|
||||||
def get_now_datetime() -> datetime.datetime:
|
def get_now_datetime() -> datetime.datetime:
|
||||||
"""获取当前时间的 UTC datetime(遵守时间覆盖)"""
|
"""获取当前时间的 UTC datetime (遵守时间覆盖)"""
|
||||||
return datetime.datetime.fromtimestamp(get_timestamp(), tz=datetime.timezone.utc)
|
return datetime.datetime.fromtimestamp(get_timestamp(), tz=datetime.timezone.utc)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
# 文本转语音服务
|
"""文本转语音服务
|
||||||
|
"""
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
|
||||||
from heurams.context import config_var
|
from heurams.context import config_var
|
||||||
@@ -8,7 +9,7 @@ from heurams.services.logger import get_logger
|
|||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
convertor: Callable = prov[config_var.get()["services"]["tts"]["provider"]].convert
|
convertor: Callable = prov[config_var.get()["services"]["tts"]["provider"]].convert
|
||||||
logger.debug(
|
logger.info(
|
||||||
"TTS 服务初始化完成, 使用 provider: %s",
|
"TTS Service inited, using provider: %s",
|
||||||
config_var.get()["services"]["tts"]["provider"],
|
config_var.get()["services"]["tts"]["provider"],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
# 版本控制集成服务
|
"""版本服务
|
||||||
|
"""
|
||||||
from heurams.services.logger import get_logger
|
from heurams.services.logger import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
@@ -6,6 +7,4 @@ logger = get_logger(__name__)
|
|||||||
ver = "0.5.1"
|
ver = "0.5.1"
|
||||||
stage = "stable"
|
stage = "stable"
|
||||||
codename = "fulcrum"
|
codename = "fulcrum"
|
||||||
codename_cn = "支点"
|
codename_cn = "支点"
|
||||||
|
|
||||||
logger.info("HeurAMS 版本: %s (%s), 阶段: %s", ver, codename, stage)
|
|
||||||
@@ -1,53 +1,53 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>HeurAMS 潜进</title>
|
<title>HeurAMS</title>
|
||||||
<link rel="stylesheet" href="/static/style.css">
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<!-- 顶栏 -->
|
<!-- Top bar -->
|
||||||
<nav id="navbar">
|
<nav id="navbar">
|
||||||
<button id="nav-toggle" onclick="toggleSidebar()">≡</button>
|
<button id="nav-toggle" onclick="toggleSidebar()">≡</button>
|
||||||
<span id="nav-title">仪表盘</span>
|
<span id="nav-title">Dashboard</span>
|
||||||
<span id="nav-version"></span>
|
<span id="nav-version"></span>
|
||||||
<button id="theme-btn" onclick="toggleTheme()" class="btn-icon">☼</button>
|
<button id="theme-btn" onclick="toggleTheme()" class="btn-icon">☼</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- 主体 -->
|
<!-- Main -->
|
||||||
<div id="main-wrap">
|
<div id="main-wrap">
|
||||||
|
|
||||||
<!-- 侧边栏 -->
|
<!-- Sidebar -->
|
||||||
<aside id="sidebar">
|
<aside id="sidebar">
|
||||||
<ul class="sidenav">
|
<ul class="sidenav">
|
||||||
<li class="active" id="nav-lobby"><a href="#" onclick="switchView('lobby')">仪表盘</a></li>
|
<li class="active" id="nav-lobby"><a href="#" onclick="switchView('lobby')">Dashboard</a></li>
|
||||||
<li id="nav-favs"><a href="#" onclick="switchView('favs')">收藏夹</a></li>
|
<li id="nav-favs"><a href="#" onclick="switchView('favs')">Favorites</a></li>
|
||||||
<li id="nav-cache"><a href="#" onclick="switchView('cache')">缓存管理</a></li>
|
<li id="nav-cache"><a href="#" onclick="switchView('cache')">Cache</a></li>
|
||||||
<li id="nav-settings"><a href="#" onclick="switchView('settings')">设置</a></li>
|
<li id="nav-settings"><a href="#" onclick="switchView('settings')">Settings</a></li>
|
||||||
<li id="nav-about"><a href="#" onclick="switchView('about')">关于</a></li>
|
<li id="nav-about"><a href="#" onclick="switchView('about')">About</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<!-- 内容区 -->
|
<!-- Content area -->
|
||||||
<main id="main">
|
<main id="main">
|
||||||
|
|
||||||
<!-- 错误 -->
|
<!-- Error -->
|
||||||
<div id="error-box"></div>
|
<div id="error-box"></div>
|
||||||
|
|
||||||
<!-- ===== 仪表盘 ===== -->
|
<!-- ===== Dashboard ===== -->
|
||||||
<div id="view-lobby" class="view">
|
<div id="view-lobby" class="view">
|
||||||
<div class="stat-row" id="lobby-stats"></div>
|
<div class="stat-row" id="lobby-stats"></div>
|
||||||
<div id="lobby-analysis" class="dim sm tc" style="margin-bottom:12px;"></div>
|
<div id="lobby-analysis" class="dim sm tc" style="margin-bottom:12px;"></div>
|
||||||
<p id="loading-lobby" class="dim tc">加载仓库...</p>
|
<p id="loading-lobby" class="dim tc">Loading repositories...</p>
|
||||||
<p id="empty-lobby" class="dim tc hidden">没有找到仓库</p>
|
<p id="empty-lobby" class="dim tc hidden">No repositories found</p>
|
||||||
<div id="repo-list"></div>
|
<div id="repo-list"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ===== 仓库详情 ===== -->
|
<!-- ===== Repo details ===== -->
|
||||||
<div id="view-prep" class="view hidden">
|
<div id="view-prep" class="view hidden">
|
||||||
<p id="loading-prep" class="dim tc">加载仓库数据...</p>
|
<p id="loading-prep" class="dim tc">Loading repo data...</p>
|
||||||
<div id="prep-content" class="hidden">
|
<div id="prep-content" class="hidden">
|
||||||
<h2 id="prep-title"></h2>
|
<h2 id="prep-title"></h2>
|
||||||
<p class="dim" id="prep-meta"></p>
|
<p class="dim" id="prep-meta"></p>
|
||||||
@@ -58,66 +58,66 @@
|
|||||||
<p class="dim sm" id="prep-pct"></p>
|
<p class="dim sm" id="prep-pct"></p>
|
||||||
|
|
||||||
<div class="h-group">
|
<div class="h-group">
|
||||||
<label>每次复习量:</label>
|
<label>Study count per session:</label>
|
||||||
<input type="number" id="prep-num" value="10" min="1" max="200">
|
<input type="number" id="prep-num" value="10" min="1" max="200">
|
||||||
<button class="btn pri" onclick="startReviewFromPrep()">开始记忆</button>
|
<button class="btn pri" onclick="startReviewFromPrep()">Start</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="prep-list"></div>
|
<div id="prep-list"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ===== 复习 ===== -->
|
<!-- ===== Review ===== -->
|
||||||
<div id="view-review" class="view hidden">
|
<div id="view-review" class="view hidden">
|
||||||
<div class="h-group sb">
|
<div class="h-group sb">
|
||||||
<div class="h-group">
|
<div class="h-group">
|
||||||
<strong id="review-repo"></strong>
|
<strong id="review-repo"></strong>
|
||||||
<span id="review-phase" class="tag"></span>
|
<span id="review-phase" class="tag"></span>
|
||||||
<button id="fav-btn" class="btn-link" onclick="toggleFav()">☆</button>
|
<button id="fav-btn" class="btn-link" onclick="toggleFav()">☆</button>
|
||||||
<button id="tts-btn" class="btn-link" onclick="playTTS()" title="朗读">▶</button>
|
<button id="tts-btn" class="btn-link" onclick="playTTS()" title="Read Aloud">▶</button>
|
||||||
</div>
|
</div>
|
||||||
<span id="review-status" class="tag">断开</span>
|
<span id="review-status" class="tag">Disconnected</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="bar-wrap sm"><div class="bar-fill" id="review-bar"></div></div>
|
<div class="bar-wrap sm"><div class="bar-fill" id="review-bar"></div></div>
|
||||||
<p class="dim sm tc" id="review-pos"></p>
|
<p class="dim sm tc" id="review-pos"></p>
|
||||||
|
|
||||||
<div id="screen-start" class="empty-state">
|
<div id="screen-start" class="empty-state">
|
||||||
<h3>准备就绪</h3>
|
<h3>Ready</h3>
|
||||||
<p class="dim">开始复习</p>
|
<p class="dim">Start review</p>
|
||||||
<button class="btn pri" onclick="startReview()">开始复习</button>
|
<button class="btn pri" onclick="startReview()">Start Review</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="screen-puzzle" class="hidden">
|
<div id="screen-puzzle" class="hidden">
|
||||||
<div id="puzzle-card"></div>
|
<div id="puzzle-card"></div>
|
||||||
<div class="tc">
|
<div class="tc">
|
||||||
<p class="dim sm">自评记忆程度</p>
|
<p class="dim sm">Self-assessment</p>
|
||||||
<div id="rating-group"></div>
|
<div id="rating-group"></div>
|
||||||
<div class="h-group jc">
|
<div class="h-group jc">
|
||||||
<button class="btn sm suc" onclick="quickPass()">正确 (5)</button>
|
<button class="btn sm suc" onclick="quickPass()">Correct (5)</button>
|
||||||
<button class="btn sm err" onclick="quickFail()">错误 (2)</button>
|
<button class="btn sm err" onclick="quickFail()">Incorrect (2)</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="screen-finished" class="hidden empty-state">
|
<div id="screen-finished" class="hidden empty-state">
|
||||||
<p class="ok-icon">✓</p>
|
<p class="ok-icon">✓</p>
|
||||||
<h3>本次记忆进程结束</h3>
|
<h3>Session complete</h3>
|
||||||
<p class="dim" id="finish-info"></p>
|
<p class="dim" id="finish-info"></p>
|
||||||
<p class="dim sm" id="finish-saved"></p>
|
<p class="dim sm" id="finish-saved"></p>
|
||||||
<button class="btn pri" onclick="goBack()" style="margin-top:16px;">返回</button>
|
<button class="btn pri" onclick="goBack()" style="margin-top:16px;">Back</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ===== 收藏夹 ===== -->
|
<!-- ===== Favorites ===== -->
|
||||||
<div id="view-favs" class="view hidden">
|
<div id="view-favs" class="view hidden">
|
||||||
<p id="loading-favs" class="dim tc">加载收藏...</p>
|
<p id="loading-favs" class="dim tc">Loading favorites...</p>
|
||||||
<p id="empty-favs" class="dim tc hidden">暂无收藏</p>
|
<p id="empty-favs" class="dim tc hidden">No favorites</p>
|
||||||
<div id="favs-list"></div>
|
<div id="favs-list"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ===== 缓存管理 ===== -->
|
<!-- ===== Cache ===== -->
|
||||||
<div id="view-cache" class="view hidden">
|
<div id="view-cache" class="view hidden">
|
||||||
<p id="loading-cache" class="dim tc">加载缓存信息...</p>
|
<p id="loading-cache" class="dim tc">Loading cache info...</p>
|
||||||
<div id="cache-content" class="hidden">
|
<div id="cache-content" class="hidden">
|
||||||
<div class="cache-stats">
|
<div class="cache-stats">
|
||||||
<div class="stat-row" id="cache-stats-cards"></div>
|
<div class="stat-row" id="cache-stats-cards"></div>
|
||||||
@@ -126,36 +126,36 @@
|
|||||||
<p class="dim sm" id="cache-path"></p>
|
<p class="dim sm" id="cache-path"></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-group mt">
|
<div class="h-group mt">
|
||||||
<button class="btn pri" id="cache-precache-btn" onclick="startPrecache()">生成缓存</button>
|
<button class="btn pri" id="cache-precache-btn" onclick="startPrecache()">Generate Cache</button>
|
||||||
<button class="btn" id="cache-refresh-btn" onclick="loadCache()">刷新</button>
|
<button class="btn" id="cache-refresh-btn" onclick="loadCache()">Refresh</button>
|
||||||
<button class="btn err" id="cache-clear-btn" onclick="clearCache()">清空缓存</button>
|
<button class="btn err" id="cache-clear-btn" onclick="clearCache()">Clear Cache</button>
|
||||||
</div>
|
</div>
|
||||||
<p id="cache-msg" class="dim sm mt"></p>
|
<p id="cache-msg" class="dim sm mt"></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ===== 设置 ===== -->
|
<!-- ===== Settings ===== -->
|
||||||
<div id="view-settings" class="view hidden">
|
<div id="view-settings" class="view hidden">
|
||||||
<p id="loading-settings" class="dim tc">加载设置...</p>
|
<p id="loading-settings" class="dim tc">Loading settings...</p>
|
||||||
<div id="settings-content" class="hidden"></div>
|
<div id="settings-content" class="hidden"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ===== 关于 ===== -->
|
<!-- ===== About ===== -->
|
||||||
<div id="view-about" class="view hidden">
|
<div id="view-about" class="view hidden">
|
||||||
<div class="about-section">
|
<div class="about-section">
|
||||||
<h2>HeurAMS 潜进</h2>
|
<h2>HeurAMS</h2>
|
||||||
<p class="dim" id="about-version"></p>
|
<p class="dim" id="about-version"></p>
|
||||||
<p class="dim sm" id="about-codename"></p>
|
<p class="dim sm" id="about-codename"></p>
|
||||||
<p class="mt">一个基于启发式算法与认知科学理论的辅助记忆调度器。</p>
|
<p class="mt">A heuristic auxiliary memorizing scheduler based on cognitive science theories.</p>
|
||||||
<p class="dim sm mt">以 GNU AGPL-3.0 许可证开放源代码,并含本机 API 调用豁免条款。</p>
|
<p class="dim sm mt">Licensed under GNU AGPL-3.0 with local API call exemption.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="about-section">
|
<div class="about-section">
|
||||||
<h3>开发人员</h3>
|
<h3>Developers</h3>
|
||||||
<p class="dim sm">Wang Zhiyu (<a href="https://github.com/pluvium27" target="_blank">@pluvium27</a>)</p>
|
<p class="dim sm">Wang Zhiyu (<a href="https://github.com/pluvium27" target="_blank">@pluvium27</a>)</p>
|
||||||
<p class="dim sm mt">项目发起与主要开发者</p>
|
<p class="dim sm mt">Project initiator and lead developer</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="about-section">
|
<div class="about-section">
|
||||||
<h3>运行环境</h3>
|
<h3>Runtime Environment</h3>
|
||||||
<table class="info-table" id="about-env"></table>
|
<table class="info-table" id="about-env"></table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user