feat: 开发 unifront 前端会话模块

This commit is contained in:
2026-04-21 16:52:04 +08:00
parent fc70aa07f6
commit 093034828b
26 changed files with 191 additions and 73 deletions

View File

@@ -25,6 +25,7 @@ from .screens.navigator import NavigatorScreen
from .screens.precache import PrecachingScreen
from .screens.setting import SettingScreen
from .screens.synctool import SyncScreen
from . import shim
_end = perf_counter()
print(f"已完成! (耗时: {round(1000 * (_end - _start))}ms)")
@@ -56,6 +57,9 @@ class HeurAMSApp(App):
"setting": SettingScreen,
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def on_mount(self) -> None:
self.push_screen("dashboard")

View File

@@ -8,7 +8,7 @@
.repo-list-item {
layout: grid;
grid-size: 2;
grid-size: 1;
height: 3;
}

View File

@@ -5,6 +5,8 @@ from textual.containers import ScrollableContainer
from textual.screen import Screen
from textual.widgets import Button, Footer, Header, Label, Markdown, Static
from textual import events, on
import heurams.services.version as version
from heurams.context import *
import platform
@@ -19,8 +21,19 @@ class AboutScreen(Screen):
("q", "go_back", "返回"),
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@on(events.ScreenResume)
def post_active(self, event):
from heurams.interface import shim
shim.set_term_title(f"{self.app.TITLE} - {self.SUB_TITLE}")
def compose(self) -> ComposeResult:
yield Header(show_clock=True)
if config_var.get()['interface']['global']['show_header']:
yield Header(show_clock=config_var.get()['interface']['global']['clock_on_header'])
with ScrollableContainer(id="about_container"):
yield Label("[b]关于与版本信息[/b]")

View File

@@ -10,6 +10,8 @@ from textual.containers import ScrollableContainer, Container, Horizontal, Verti
from textual.screen import Screen
from textual.widgets import Button, Footer, Header, Label, ListItem, ListView, Static
from textual.layouts import horizontal
from textual import events, on
from textual.reactive import reactive
import heurams.kernel.particles as pt
import heurams.services.timer as timer
@@ -37,6 +39,8 @@ class DashboardScreen(Screen):
CSS_PATH = rootdir / "interface" / "css" / "screens" / "dashboard.tcss"
repolink = reactive({})
def __init__(
self,
name: str | None = None,
@@ -44,12 +48,12 @@ class DashboardScreen(Screen):
classes: str | None = None,
) -> None:
super().__init__(name, id, classes)
self.repolink = {}
self._load_data()
def compose(self) -> ComposeResult:
"""组合界面组件"""
self._load_data()
yield Header(show_clock=True)
if config_var.get()['interface']['global']['show_header']:
yield Header(show_clock=config_var.get()['interface']['global']['clock_on_header'])
with ScrollableContainer():
yield Horizontal( # 顶部的状态
Vertical(
@@ -58,7 +62,7 @@ class DashboardScreen(Screen):
f"应用时区修正: UTC+{str(config_var.get()['services']['timer']['timezone_offset'] / 3600).rstrip('.0')}"
),
Label(
f"默认算法设置: {config_var.get()['interface']['global']['algorithm']}"
f"默认算法设置: {config_var.get()['interface']['global']['algorithm']}",
),
classes="left",
),
@@ -81,6 +85,13 @@ class DashboardScreen(Screen):
yield Label(f"版本 {version.ver} {version.stage.capitalize()}") # 版本信息
yield Footer()
@on(events.ScreenResume)
def post_active(self, event):
from heurams.interface import shim
shim.set_term_title(f"{self.app.TITLE} - {self.SUB_TITLE}")
# https://github.com/Textualize/textual/discussions/4268
# self.refresh(recompose=True) 此函数有问题且官方不管 而且性能低
def _load_data(self):
repo_dirs = Repo.probe_valid_repos_in_dir(
Path(config_var.get()["global"]["paths"]["repo"])
@@ -154,7 +165,7 @@ class DashboardScreen(Screen):
for r in self.repos:
self.repolink[str(id(r))] = r # 用于规避 ctype id 对象还原
list_item = ListItem(
Label(r.prompt),
*[Label(line) for line in r.prompt.splitlines()],
Button(
f"开始学习",
flat=True,

View File

@@ -4,6 +4,8 @@ import base64
from pathlib import Path
from typing import List, Optional
from textual import events, on
from textual.app import ComposeResult
from textual.containers import ScrollableContainer
from textual.screen import Screen
@@ -18,6 +20,7 @@ from textual.widgets import (
Static,
)
from textual import events, on
from heurams.context import config_var
from heurams.kernel.repolib import Repo
from heurams.services.favorite_service import FavoriteItem, favorite_manager
@@ -53,7 +56,9 @@ class FavoriteManagerScreen(Screen):
def compose(self) -> ComposeResult:
"""组合界面组件"""
yield Header(show_clock=True)
if config_var.get()['interface']['global']['show_header']:
yield Header(show_clock=config_var.get()['interface']['global']['clock_on_header'])
with ScrollableContainer(id="favorites-container"):
if not self.favorites:
yield Label("暂无收藏", classes="empty-label")
@@ -63,6 +68,12 @@ class FavoriteManagerScreen(Screen):
yield ListView(id="favorites-list")
yield Footer()
@on(events.ScreenResume)
def post_active(self, event):
from heurams.interface import shim
shim.set_term_title(f"{self.app.TITLE} - {self.SUB_TITLE}")
def on_mount(self) -> None:
"""挂载后填充列表"""
if self.favorites:

View File

@@ -10,6 +10,8 @@ from textual.reactive import reactive
from textual.screen import Screen
from textual.widgets import Button, Footer, Header, Label, Static
from textual import events, on
import heurams.kernel.particles as pt
import heurams.kernel.puzzles as pz
from heurams.context import config_var, rootdir
@@ -17,10 +19,13 @@ from heurams.kernel.reactor import *
from heurams.services.favorite_service import favorite_manager
from heurams.services.logger import get_logger
import pickle
from .. import shim
logger = get_logger(__name__)
class MemScreen(Screen):
BINDINGS = [
("q", "go_back", "返回"),
@@ -33,7 +38,8 @@ class MemScreen(Screen):
("z", "block_prompt"),
]
CSS_PATH = rootdir / 'interface' / 'css' / 'screens' / 'memoqueue.tcss'
SUB_TITLE = "学习中"
CSS_PATH = rootdir / "interface" / "css" / "screens" / "memoqueue.tcss"
if config_var.get()["interface"]["global"]["quick_pass"]:
BINDINGS.append(("k", "quick_pass", "正确应答"))
@@ -55,8 +61,15 @@ class MemScreen(Screen):
self.update_state()
self.expander: Expander
@on(events.ScreenResume)
def post_active(self, event):
from heurams.interface import shim
shim.set_term_title(f"{self.app.TITLE} - {self.SUB_TITLE}")
def compose(self) -> ComposeResult:
yield Header(show_clock=True)
if config_var.get()['interface']['global']['show_header']:
yield Header(show_clock=config_var.get()['interface']['global']['clock_on_header'])
with ScrollableContainer():
yield Label(self._get_progress_text(), id="head_stat")
yield ScrollableContainer(id="puzzle_container")
@@ -83,7 +96,7 @@ class MemScreen(Screen):
return Static(f"无法生成谜题 {e}")
def _get_progress_text(self):
s = f"阶段: {self.procession.phase.name}\n"
s = f"阶段: {self.procession.route.name}\n"
# 收藏状态
if self.repo is not None:
fav_status = "已收藏" if self._is_current_atom_favorited() else "未收藏"
@@ -98,7 +111,7 @@ class MemScreen(Screen):
def mount_puzzle(self):
"""挂载当前谜题组件"""
if self.procession.phase == RouterState.FINISHED:
if self.procession.route == RouterState.FINISHED:
self.mount_finished_widget()
return
container = self.query_one("#puzzle_container")
@@ -144,7 +157,7 @@ class MemScreen(Screen):
if new_rating == -1: # 安全值
return
self.update_state()
if self.procession.phase == RouterState.FINISHED:
if self.procession.route == RouterState.FINISHED:
rating = -1
return
self.expander.report(new_rating)
@@ -231,4 +244,4 @@ class MemScreen(Screen):
self.update_display()
def action_block_prompt(self):
self.app.notify("功能在记忆界面中不可用, 完成或返回后再试", severity="error")
self.app.notify("功能在记忆界面中不可用, 完成或返回后再试", severity="error")

View File

@@ -5,6 +5,8 @@ from textual.containers import Grid, ScrollableContainer
from textual.screen import ModalScreen
from textual.widgets import Button, Footer, Header, Label, ListItem, ListView, Static
from textual import events, on
from heurams.context import *
from heurams.services.logger import get_logger

View File

@@ -8,6 +8,8 @@ from textual.screen import Screen
from textual.widgets import Button, Footer, Header, Label, ProgressBar, Static
from textual.worker import get_current_worker
from textual import events, on
import heurams.kernel.particles as pt
import heurams.services.hasher as hasher
from heurams.context import *
@@ -80,6 +82,12 @@ class PrecachingScreen(Screen):
continue
return total
@on(events.ScreenResume)
def post_active(self, event):
from heurams.interface import shim
shim.set_term_title(f"{self.app.TITLE} - {self.SUB_TITLE}")
def _update_cache_stats(self) -> None:
"""更新缓存统计信息"""
total_size = 0
@@ -103,7 +111,9 @@ class PrecachingScreen(Screen):
self.cache_stats["cache_rate"] = cache_rate
def compose(self) -> ComposeResult:
yield Header(show_clock=True)
if config_var.get()['interface']['global']['show_header']:
yield Header(show_clock=config_var.get()['interface']['global']['clock_on_header'])
with ScrollableContainer(id="precache_container"):
yield Label("[b]音频预缓存[/b]", classes="title-label")
with Container():

View File

@@ -17,6 +17,8 @@ from textual.widgets import (
)
from textual.lazy import Reveal, Lazy
from textual import events, on
import heurams.kernel.particles as pt
import heurams.services.hasher as hasher
from heurams.context import *
@@ -34,7 +36,7 @@ class PreparationScreen(Screen):
BINDINGS = [
("q", "go_back", "返回"),
("p", "precache", "缓存音频"),
("p", "precache", "缓存"),
("d", "toggle_dark", ""),
("0,1,2,3", "app.push_screen('about')", ""),
]
@@ -46,8 +48,16 @@ class PreparationScreen(Screen):
self.repo = repo
self.load_data()
@on(events.ScreenResume)
def post_active(self, event):
from heurams.interface import shim
shim.set_term_title(f"{self.app.TITLE} - {self.SUB_TITLE}")
def compose(self) -> ComposeResult:
yield Header(show_clock=True)
if config_var.get()['interface']['global']['show_header']:
yield Header(show_clock=config_var.get()['interface']['global']['clock_on_header'])
with ScrollableContainer(id="main_container"):
yield Markdown(
f"**准备就绪**: `{self.repo.manifest['title']}`\n", id="title"
@@ -72,7 +82,7 @@ class PreparationScreen(Screen):
classes="btn",
),
Button(
"预缓存音频",
"管理缓存",
id="precache_button",
variant="success",
classes="btn",

View File

@@ -23,6 +23,8 @@ from textual.widgets import (
)
from textual.layouts import horizontal
from textual import events, on
import heurams.kernel.particles as pt
import heurams.services.timer as timer
import heurams.services.version as version
@@ -54,9 +56,16 @@ class SettingScreen(Screen):
) -> None:
super().__init__(name, id, classes)
@on(events.ScreenResume)
def post_active(self, event):
from heurams.interface import shim
shim.set_term_title(f"{self.app.TITLE} - {self.SUB_TITLE}")
def compose(self) -> ComposeResult:
"""组合界面组件"""
yield Header(show_clock=True)
if config_var.get()['interface']['global']['show_header']:
yield Header(show_clock=config_var.get()['interface']['global']['clock_on_header'])
with ScrollableContainer():
yield Label("[b]设置页面[/b]")
for i in config_var.get():
@@ -65,7 +74,7 @@ class SettingScreen(Screen):
a = self._get_subcfg(f"{i}")
if a:
yield Collapsible(
*a, title=i + f'\n{config_var.get().get(f"_{i}_desc", "")}'
*a, title=i + f'\n[d]{config_var.get().get(f"_{i}_desc", "")}[/d]'
)
yield Label(
"退出页面时, 所作的更改会立即保存, 但仍建议重启软件以确保新的配置得到应用",
@@ -85,7 +94,7 @@ class SettingScreen(Screen):
if a:
lst.append(
Collapsible(
*a, title=i + f'\n{parent.get(f"_{i}_desc", "")}'
*a, title=i + f'\n[d]{parent.get(f"_{i}_desc", "")}[/d]'
)
)
return lst
@@ -101,17 +110,17 @@ class SettingScreen(Screen):
if a:
lst.append(
Collapsible(
*a, title=i + f'\n{parent.get(f"_{i}_desc", "")}'
*a, title=i + f'\n[d]{parent.get(f"_{i}_desc", "")}[/d]'
)
)
elif f"_{i}_candidate" in parent: # 选择框模式
if isinstance(parent[f"_{i}_candidate"], dict):
lst.append(
Horizontal(
Label(i + f'\n{parent.get(f"_{i}_desc", "")}'),
Label(i + f'\n[d]{parent.get(f"_{i}_desc", "")}[/d]'),
Select(
(
(f"{j} ({k})", j)
(f"{j}\n[d]{k}[/d]", j)
for j, k in parent[f"_{i}_candidate"].items()
),
prompt=f'{parent.get(f"{i}", "")}',
@@ -122,7 +131,7 @@ class SettingScreen(Screen):
elif isinstance(parent[f"_{i}_candidate"], list):
lst.append(
Horizontal(
Label(i + f'\n{parent.get(f"_{i}_desc", "")}'),
Label(i + f'\n[d]{parent.get(f"_{i}_desc", "")}[/d]'),
Select(
((j, j) for j in parent[f"_{i}_candidate"]),
prompt=f'{parent.get(f"{i}", "")}',
@@ -134,7 +143,7 @@ class SettingScreen(Screen):
if isinstance(parent[i], float):
lst.append(
Horizontal(
Label(i + f'\n{parent.get(f"_{i}_desc", "")}'),
Label(i + f'\n[d]{parent.get(f"_{i}_desc", "")}[/d]'),
Input(
value=str(parent[i]),
placeholder="要求一个浮点数",
@@ -146,7 +155,7 @@ class SettingScreen(Screen):
elif isinstance(parent[i], str):
lst.append(
Horizontal(
Label(i + f'\n{parent.get(f"_{i}_desc", "")}'),
Label(i + f'\n[d]{parent.get(f"_{i}_desc", "")}[/d]'),
Input(
value=parent[i],
placeholder="要求一个字符串",
@@ -158,7 +167,7 @@ class SettingScreen(Screen):
elif isinstance(parent[i], bool):
lst.append(
Horizontal(
Label(i + f'\n{parent.get(f"_{i}_desc", "")}'),
Label(i + f'\n[d]{parent.get(f"_{i}_desc", "")}[/d]'),
Switch(
value=parent[i], id=domize(f"{parent_epath}.{i}")
),
@@ -167,7 +176,7 @@ class SettingScreen(Screen):
elif isinstance(parent[i], int):
lst.append(
Horizontal(
Label(i + f'\n{parent.get(f"_{i}_desc", "")}'),
Label(i + f'\n[d]{parent.get(f"_{i}_desc", "")}[/d]'),
Input(
value=str(parent[i]),
placeholder="要求一个整数",

View File

@@ -9,6 +9,8 @@ from textual.screen import Screen
from textual.widgets import Button, Footer, Header, Label, ProgressBar, Static
from textual.worker import get_current_worker
from textual import events, on
import heurams.kernel.particles as pt
import heurams.services.hasher as hasher
from heurams.context import *
@@ -26,8 +28,16 @@ class SyncScreen(Screen):
self.log_messages = []
self.max_log_lines = 50
@on(events.ScreenResume)
def post_active(self, event):
from heurams.interface import shim
shim.set_term_title(f"{self.app.TITLE} - {self.SUB_TITLE}")
def compose(self) -> ComposeResult:
yield Header(show_clock=True)
if config_var.get()['interface']['global']['show_header']:
yield Header(show_clock=config_var.get()['interface']['global']['clock_on_header'])
with ScrollableContainer(id="sync_container"):
# 标题和连接状态
yield Static("同步工具", classes="title")

View File

@@ -2,6 +2,8 @@
import heurams.interface.widgets as pzw
import heurams.kernel.puzzles as pz
import platform, os, sys
from heurams.context import config_var
puzzle2widget = {
pz.RecognitionPuzzle: pzw.Recognition,
@@ -9,3 +11,13 @@ puzzle2widget = {
pz.MCQPuzzle: pzw.MCQPuzzle,
pz.BasePuzzle: pzw.BasePuzzleWidget,
}
def set_term_title(title):
if not config_var.get()['interface']['global']['change_window_title']:
return
system = platform.system()
if system == "Windows":
os.system(f"title {title}")
else: # Linux, Mac, etc.
os.write(2, f"\033]2;{title}\007".encode("utf-8"))