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 pathlib import threading import edge_tts as tts from playsound import playsound from textual.logging import TextualHandler import particles as pt from reactor import Reactor import auxiliary as aux ver = '0.2.4' config = aux.ConfigFile("config.toml") 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 ): 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) #print(self.reactor.procession) self.reactor.forward() 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 == 4: # NOTE # if config.get("save"): self.reactor.save() self._show_finished_screen("今日目标已完成") else: self.reactor.set_round_templated(self.stage) self.reactor.forward(1) self._update_ui() self.stage += 1 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_dir = pathlib.Path(f"./cache/voice/") cache_dir.mkdir(parents = True, exist_ok = True) cache = cache_dir / f"{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, config.get("tasked_number", 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__": app = AppLauncher() app.run()