from textual.app import App, ComposeResult from textual.widgets import Header, Footer, ListView, ListItem, Label, Static, Button from textual.containers import Container, Horizontal from textual.screen import Screen import particles as pt from reactor import Reactor import pathlib import threading import edge_tts as tts from playsound import playsound from textual.logging import TextualHandler ver = '0.2.1' debug = TextualHandler(stderr=True) class MemScreen(Screen): BINDINGS = [ ("d", "toggle_dark", "改变色调"), ("q", "pop_screen", "返回主菜单"), ("v", "play_voice", "朗读"), ("0", "press('q0')", None), ("1", "press('q1')", None), ("2", "press('q2')", None), ("3", "press('q3')", None), ("4", "press('q4')", None), ("5", "press('q5')", None), ("[", "press('q5')", None), ("]", "press('q4')", None), (";", "press('q3')", None), ("'", "press('q2')", None), (".", "press('q1')", None), ("/", "press('q0')", None), ] btn = dict() def __init__( self, nucleon_file: pt.AtomicFile, electron_file: pt.AtomicFile, tasked_num: int ): super().__init__(name=None, id=None, classes=None) self.reactor = Reactor(nucleon_file, electron_file, tasked_num) self.stage = 1 self.stage += self.reactor.set_round_templated(self.stage) def compose(self) -> ComposeResult: yield Header(show_clock=True) with Container(id="main_container"): yield Label(self.reactor.round_title, id="round_title") yield Label("记住了吗?", id="question") yield Static(self.reactor.current_atom[1].content, id="sentence") yield Static("", id="feedback") # 用于显示反馈 yield Label(self._get_progress_text(), id="progress") with Container(id="button_container"): self.btn['5'] = Button("完美回想", variant="success", id="q5", classes="choice") self.btn['4'] = Button("犹豫后正确", variant="success", id="q4", classes="choice") self.btn['3'] = Button("困难地正确", variant="warning", id="q3", classes="choice") self.btn['2'] = Button("错误但熟悉", variant="warning", id="q2", classes="choice") self.btn['1'] = Button("错误且不熟", variant="error", id="q1", classes="choice") self.btn['0'] = Button("完全空白", variant="error", id="q0", classes="choice") yield Horizontal(self.btn['5'], self.btn['4']) yield Horizontal(self.btn['3'], self.btn['2']) yield Horizontal(self.btn['1'], self.btn['0']) yield Footer() def _get_progress_text(self): return f"{len(self.reactor.procession) - self.reactor.index}/{len(self.reactor.procession)}" def on_mount(self): # 首次挂载时调用 self._update_ui() def _update_ui(self): self.query_one("#round_title", Label).update(self.reactor.round_title) self.query_one("#sentence", Static).update(self.reactor.current_atom[1].content) self.query_one("#progress", Label).update(self._get_progress_text()) self.query_one("#feedback", Static).update("") # 清除任何之前的反馈消息 def _show_finished_screen(self, message): self.query_one("#question", Label).update(message) self.query_one("#sentence", Static).update("已经完成记忆任务") self.query_one("#round_title").display = False self.query_one("#progress").display = False for i in range(6): self.query_one(f"#q{i}", Button).display = False def on_button_pressed(self, event): feedback_label = self.query_one("#feedback", Static) if type(event) == str: btnid = event else: btnid = event.button.id btnid = str(btnid) quality = int(btnid.replace('q', '')) assessment = self.reactor.report(self.reactor.current_atom, quality) if assessment == 1: # 需要复习 feedback_label.update(f"评分为 {quality}, 已经加入至复习, 请重复记忆") else: ret = self.reactor.forward(1) if ret == -1: if self.reactor.round_set == 0: if self.stage == 3: # NOTE # self.reactor.save() self._show_finished_screen("今日目标已完成") else: self.stage += 1 self.reactor.set_round_templated(self.stage) return #feedback_label.update("") # 清除反馈消息 self._update_ui() def action_press(self, btnid): self.on_button_pressed(btnid) def action_play_voice(self): def play(): cache = pathlib.Path(f"./cache/voice/{self.reactor.current_atom[1].content}.wav") if not cache.exists(): communicate = tts.Communicate(self.reactor.current_atom[1].content, "zh-CN-YunjianNeural") communicate.save_sync(f"./cache/voice/{self.reactor.current_atom[1].content}.wav") playsound(str(cache)) threading.Thread(target=play).start() def action_toggle_dark(self): self.app.action_toggle_dark() def action_pop_screen(self): """返回到上一个屏幕""" self.app.pop_screen() class PreparationScreen(Screen): BINDINGS = [ ("q", "go_back", "返回"), ("escape", "quit_app", "退出") ] def __init__(self, nucleon_file: pt.AtomicFile, electron_file: pt.AtomicFile) -> None: super().__init__(name=None, id=None, classes=None) self.nucleon_file = nucleon_file self.electron_file = electron_file def compose(self) -> ComposeResult: yield Header(show_clock=True) with Container(id="learning_screen_container"): yield Label(f"记忆项目: [b]{self.nucleon_file.name}[/b]\n") yield Label(f"核子文件对象: ./nucleon/[b]{self.nucleon_file.name}[/b].toml") yield Label(f"电子文件对象: ./electron/[b]{self.electron_file.name}[/b].toml") yield Label(f"核子数量:{self.nucleon_file.get_len()}") yield Button("开始记忆", id="start_memorizing_button", variant="primary", classes="start-button") yield Static(f"\n全文如下:\n") yield Static(self.nucleon_file.get_full_content(), classes="full") yield Footer() def action_go_back(self): self.app.pop_screen() def action_quit_app(self): self.app.exit() def on_button_pressed(self, event: Button.Pressed) -> None: pass if event.button.id == "start_memorizing_button": #init_file(Path(self.atom_file).name) newscr = MemScreen(self.nucleon_file, self.electron_file, 8) self.app.push_screen( newscr ) #if event.button.id == "edit_metadata_button": # init_file(Path(self.atom_file).name) # os.system("reset;nano ./data/" + str(Path(self.atom_file).name.replace(".txt", "_atoms.json"))) class FileSelectorScreen(Screen): global ver def compose(self) -> ComposeResult: yield Header(show_clock=True) yield Container( Label(f'欢迎使用 "潜进" 辅助记忆软件, 版本 {ver}', classes="title-label"), Label("选择要学习的文件:", classes="title-label"), ListView(id="file-list", classes="file-list-view") ) yield Footer() def on_mount(self) -> None: file_list_widget = self.query_one("#file-list", ListView) nucleon_path = pathlib.Path("./nucleon") nucleon_files = sorted([f.name for f in nucleon_path.iterdir() if f.suffix == ".toml"]) if nucleon_files: for filename in nucleon_files: file_list_widget.append(ListItem(Label(filename))) else: file_list_widget.append(ListItem(Static("在 ./nucleon/ 中未找到任何核子文件. 请放置文件后重启应用."))) file_list_widget.disabled = True def on_list_view_selected(self, event: ListView.Selected) -> None: if not isinstance(event.item, ListItem): self.notify("无法选择此项。", severity="error") return selected_label = event.item.query_one(Label) if "未找到任何 .toml 文件" in str(selected_label.renderable): self.notify("请先在 `./atoms/` 目录中放置 .toml 文件。", severity="warning") return selected_filename = str(selected_label.renderable) nucleon_file = pt.AtomicFile(pathlib.Path("./nucleon") / selected_filename, "nucleon") electron_file_path = pathlib.Path("./electron") / selected_filename if electron_file_path.exists(): pass else: electron_file_path.touch() electron_file = pt.AtomicFile(pathlib.Path("./electron") / selected_filename, "electron") # self.notify(f"已选择: {selected_filename}", timeout=2) self.app.push_screen(PreparationScreen(nucleon_file, electron_file)) def action_quit_app(self) -> None: self.app.exit() class AppLauncher(App): CSS_PATH = "styles.tcss" TITLE = '潜进 - 辅助记忆程序' BINDINGS = [("escape", "quit", "退出"), ("d", "toggle_dark", "改变色调")] SCREENS = { "file_selection_screen": FileSelectorScreen, } def on_mount(self) -> None: self.action_toggle_dark() self.push_screen("file_selection_screen") if __name__ == "__main__": css_path = pathlib.Path("styles_dashboard.tcss") app = AppLauncher() app.run()