上传文件至 /

This commit is contained in:
Wang Zhiyu 2025-07-08 00:48:24 +08:00
commit 5319719661
5 changed files with 501 additions and 0 deletions

121
main.py Normal file
View File

@ -0,0 +1,121 @@
import os
from pathlib import Path
import memoria
from textual.app import App, ComposeResult
from textual.widgets import Header, Footer, ListView, ListItem, Label, Static, Button
from textual.containers import Container
from textual.screen import Screen
ver = 0.01
# --- MemApp 学习屏幕 ---
class MemAppLearningScreen(Screen):
BINDINGS = [
("q", "go_back", "返回"),
("escape", "quit_app", "退出")
]
def __init__(self, file_to_learn: str, history_file: str, name: str | None = None, id: str | None = None, classes: str | None = None) -> None:
super().__init__(name=name, id=id, classes=classes)
self.file_to_learn = file_to_learn
self.history_file = history_file
def compose(self) -> ComposeResult:
yield Header()
with Container(id="learning_screen_container"):
yield Label(f"正在学习文件: [b]{Path(self.file_to_learn).name}[/b]", classes="learning-info")
yield Label(f"历史文件: [b]{Path(self.history_file).name}[/b]", classes="learning-info")
yield Label("\n[i]点击 '开始记忆' 进入学习模式.[/i]", classes="placeholder-message")
yield Static("\n按 [b]Q[/b] 返回;按 [b]ESC[/b] 退出.", classes="instructions")
yield Button("开始记忆", id="start_memorizing_button", variant="primary", classes="start-button")
yield Static(f"\n以下是全文:\n", classes="read-info")
with open(self.file_to_learn, 'r', encoding='UTF-8') as f:
for i in f.readlines():
yield Static(' ' + i, classes="read-info")
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:
if event.button.id == "start_memorizing_button":
self.app.push_screen(
memoria.MemScreen(
file_to_learn=self.file_to_learn,
history_file=self.history_file
)
)
# --- 文件选择屏幕 ---
class FileSelectionScreen(Screen):
global ver
def compose(self) -> ComposeResult:
yield Header()
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:
Path("./history").mkdir(parents=True, exist_ok=True)
Path("./library").mkdir(parents=True, exist_ok=True)
file_list_widget = self.query_one("#file-list", ListView)
library_path = Path("./library")
txt_files = sorted([f.name for f in library_path.iterdir() if f.suffix == ".txt"])
if txt_files:
for filename in txt_files:
file_list_widget.append(ListItem(Label(filename)))
else:
file_list_widget.append(ListItem(Static("在 ./library/ 中未找到任何 .txt 文件。请放置文件后重启应用。")))
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 "未找到任何 .txt 文件" in str(selected_label.renderable):
self.notify("请先在 `./library/` 目录中放置 .txt 文件。", severity="warning")
return
selected_filename = str(selected_label.renderable)
file_to_learn_path = Path("./library") / selected_filename
history_file_name = f"{Path(selected_filename).stem}_history.json"
history_file_path = Path("./history") / history_file_name
self.notify(f"已选择: {selected_filename}", timeout=2)
self.app.push_screen(MemAppLearningScreen(
file_to_learn=str(file_to_learn_path),
history_file=str(history_file_path)
))
def action_quit_app(self) -> None:
self.app.exit()
# --- 主 Textual 应用类 ---
class MemAppLauncher(App):
CSS_PATH = "styles.css"
TITLE = '青鸟 辅助记忆程序'
BINDINGS = [("escape", "quit", "退出"), ("d", "toggle_dark", "改变色调")]
SCREENS = {
"file_selection_screen": FileSelectionScreen,
}
def on_mount(self) -> None:
self.action_toggle_dark()
self.push_screen("file_selection_screen")
if __name__ == "__main__":
css_path = Path("styles_dashboard.css")
app = MemAppLauncher()
app.run()

296
memoria.py Normal file
View File

@ -0,0 +1,296 @@
import random
import json
import datetime
from pathlib import Path
import cn2an
from textual.app import App, ComposeResult # App 导入只是为了类型提示MemScreen 继承自 Screen
from textual.containers import Container
from textual.widgets import Button, Header, Footer, Static, Label
from textual.color import Color
from textual.screen import Screen # 核心:导入 Screen
# --- 辅助函数和类 (这些通常会放在 MemScreen 类定义的上方或单独的模块中) ---
def load_sentences(filename: str = 'test.txt') -> list[str]:
"""
Loads and parses a text file into a list of sentences.
Each sentence is identified by '.', '?', or '!'.
If the file doesn't exist, it creates a default one.
Args:
filename (str): The path to the text file.
Returns:
list[str]: A list of sentences extracted from the file.
"""
try:
with open(filename, 'r', encoding='UTF-8') as f:
s = f.read()
sentences = []
current_sentence = ""
for char in s:
if char in '.?!':
current_sentence += char
stripped_sentence = current_sentence.strip()
if stripped_sentence:
sentences.append(stripped_sentence)
current_sentence = ""
elif char == '\n':
pass
else:
if char == " " and not current_sentence:
pass
else:
current_sentence += char
stripped_sentence = current_sentence.strip()
if stripped_sentence:
sentences.append(stripped_sentence)
return [item for item in sentences if item]
except FileNotFoundError:
with open(filename, 'w', encoding='UTF-8') as f:
f.write("这是第一个句子。这是第二个吗?以及第三个!")
return load_sentences(filename)
def get_daystamp() -> int:
"""
Returns the number of days since the epoch.
Returns:
int: The current daystamp.
"""
return round(datetime.datetime.now().timestamp() // 86400)
class History:
"""
Handles reading and writing the user's progress to a JSON file.
It tracks whether a sentence was remembered (1) or not (0) on a given day.
"""
def __init__(self, path: str = "history.json"):
"""
Initializes the History manager.
Args:
path (str): The path to the history JSON file.
"""
self.path = Path(path)
self.history_data = self._read_from_file()
def add(self, key: str, stat: int):
"""
Adds a new entry to the history.
Args:
key (str): The sentence that was reviewed.
stat (int): The review result (1 for remembered, 0 for not).
"""
if key not in self.history_data:
self.history_data[key] = []
self.history_data[key].append((get_daystamp(), stat))
self._save_to_file()
def _read_from_file(self) -> dict:
"""
Reads the history data from the JSON file.
Returns:
dict: The loaded history data, or an empty dictionary if the file
does not exist or is invalid.
"""
if not self.path.exists():
return {}
try:
with self.path.open('r', encoding="UTF-8") as file:
return json.loads(file.read())
except (json.JSONDecodeError, FileNotFoundError):
return {}
def _save_to_file(self):
"""Saves the current history data to the JSON file."""
with self.path.open('w', encoding="UTF-8") as file:
json.dump(self.history_data, file, indent=4, ensure_ascii=False)
# --- MemScreen 类定义 ---
class MemScreen(Screen):
"""
一个 Textual 屏幕用于帮助用户通过重复记忆句子
句子会分轮次呈现未记住的句子会在后续轮次中重新出现
"""
# CSS_PATH 不再需要在此处定义,应由主 App 全局管理或在主 App 中指定
BINDINGS = [
("d", "toggle_dark", "改变色调"),
("q", "pop_screen", "返回主菜单") # 绑定到 pop_screen
]
def __init__(
self,
file_to_learn: str = 'test.txt',
history_file: str = 'history.json',
name: str | None = None,
id: str | None = None,
classes: str | None = None
):
"""
初始化 MemScreen
Args:
file_to_learn (str): 包含要记忆句子的文本文件路径
history_file (str): 存储记忆历史的 JSON 文件路径
name (str | None): 屏幕的名称
id (str | None): 屏幕的 ID
classes (str | None): 屏幕的 CSS
"""
super().__init__(name=name, id=id, classes=classes)
self.file_to_learn = file_to_learn
self.all_sentences = load_sentences(self.file_to_learn)
self.history = History(history_file)
random.shuffle(self.all_sentences)
self.round_number = 1
self.current_round_sentences = list(self.all_sentences)
self.failed_in_this_round = []
self.current_index = 0
# 如果初始没有句子,则标记为已完成
self.is_finished = not self.all_sentences
def compose(self) -> ComposeResult:
"""
为屏幕的 UI 创建子组件
"""
# Header 和 Footer 仍然可以放在屏幕内部,如果每个屏幕都需要独立的
yield Header(show_clock=True)
with Container(id="main_container"):
yield Label(self._get_round_text(), id="round_label")
yield Label("记住了吗?", id="question")
yield Static(self._get_current_sentence(), id="sentence")
yield Static("", id="feedback") # 用于显示“请重复记忆”
yield Label(self._get_progress_text(), id="progress")
with Container(id="button_container"):
yield Button("记住了", variant="success", id="yes")
yield Button("没记住", variant="error", id="no")
yield Footer()
def on_mount(self) -> None:
"""
屏幕首次挂载时调用
检查句子文件是否为空如果为空则显示消息
"""
if self.is_finished:
self._show_finished_screen("文件为空,没有句子可供学习。")
else:
# 确保 UI 在屏幕挂载时正确显示第一个句子
self._update_ui()
# --- 内部辅助方法 (私有方法,约定以 `_` 开头) ---
def _get_round_text(self) -> str:
"""返回当前轮次编号的格式化文本。"""
return f"{cn2an.transform(str(self.round_number), 'an2cn')}轮复习"
def _get_progress_text(self) -> str:
"""返回当前轮次进度的格式化文本。"""
total = len(self.current_round_sentences)
# 确保在总数为0时不会出现除以零或显示“1/0”的情况
current = min(self.current_index + 1, total) if total > 0 else 0
return f"进程: {current} / {total}"
def _get_current_sentence(self) -> str:
"""返回当前要显示的句子,或者完成消息。"""
if not self.is_finished:
if self.current_round_sentences:
return self.current_round_sentences[self.current_index]
return "没有句子可供复习。" # 理论上不应该在 is_finished 为 False 时触发
return "全部完成! ✨"
def _start_next_round(self) -> None:
"""
管理记忆过程的下一轮
如果没有句子未记住则会话完成
否则新一轮将从之前未记住的句子开始
"""
if not self.failed_in_this_round:
self.is_finished = True
self._show_finished_screen("🎉 已完成此文件的记忆 🎉")
else:
self.round_number += 1
# 使用 set 来移除重复项,如果一个句子在同一轮中多次被标记为“没记住”
self.current_round_sentences = list(set(self.failed_in_this_round))
random.shuffle(self.current_round_sentences)
self.failed_in_this_round = [] # 清空本轮失败列表,为下一轮做准备
self.current_index = 0
self._update_ui()
def _update_ui(self) -> None:
"""
用当前数据更新屏幕上所有动态组件
"""
self.query_one("#round_label", Label).update(self._get_round_text())
self.query_one("#sentence", Static).update(self._get_current_sentence())
self.query_one("#progress", Label).update(self._get_progress_text())
self.query_one("#feedback", Static).update("") # 清除任何之前的反馈消息
def _show_finished_screen(self, message: str) -> None:
"""
显示最终完成消息并禁用交互按钮
Args:
message (str): 在完成屏幕上显示的消息
"""
self.query_one("#question").update(message)
self.query_one("#sentence").update("已经完成记忆任务")
self.query_one("#round_label").display = False # 隐藏轮次信息
self.query_one("#progress").display = False # 隐藏进度信息
self.query_one("#yes", Button).disabled = True # 禁用按钮
self.query_one("#no", Button).disabled = True
# --- 事件处理方法 (Textual 框架调用) ---
def on_button_pressed(self, event: Button.Pressed) -> None:
"""
记住了 (Yes) 没记住 (No) 按钮按下的事件处理器
管理句子进展和历史记录
"""
if self.is_finished:
# 如果已经完成,则不再处理按钮事件
return
current_sentence = self._get_current_sentence()
feedback_label = self.query_one("#feedback", Static)
if event.button.id == "no":
self.history.add(current_sentence, 0) # 记录为未记住
if current_sentence not in self.failed_in_this_round:
self.failed_in_this_round.append(current_sentence) # 添加到本轮失败列表
feedback_label.update("已经加入至复习, 请重复记忆")
return # IMPORTANT: 不会前进到下一个句子,用户需要再次确认
# 如果是“记住了”按钮被按下
self.history.add(current_sentence, 1) # 记录为记住
feedback_label.update("") # 清除反馈消息
self.current_index += 1 # 前进到下一个句子
if self.current_index >= len(self.current_round_sentences):
# 当前轮次所有句子都已处理,检查是否需要开始下一轮
self._start_next_round()
else:
# 继续当前轮次的下一个句子
self._update_ui()
# --- 动作方法 (通过 BINDINGS 绑定) ---
def action_toggle_dark(self) -> None:
"""一个切换深色模式的动作。"""
# 访问主应用 (self.app) 的 dark 属性来切换主题
self.app.action_toggle_dark()
def action_pop_screen(self) -> None:
"""一个弹出当前屏幕并返回到上一个屏幕的动作。"""
# Textual 的 Screen 管理方法,用于从屏幕堆栈中移除当前屏幕
self.app.pop_screen()

43
requirements.txt Normal file
View File

@ -0,0 +1,43 @@
aiohappyeyeballs==2.6.1
aiohttp==3.12.13
aiohttp-jinja2==1.6
aiosignal==1.4.0
annotated-types==0.7.0
anyio==4.9.0
attrs==25.3.0
certifi==2025.6.15
click==8.2.1
cn2an==0.5.23
frozenlist==1.7.0
h11==0.16.0
httpcore==1.0.9
httpx==0.28.1
idna==3.10
importlib_metadata==8.7.0
Jinja2==3.1.6
linkify-it-py==2.0.3
markdown-it-py==3.0.0
MarkupSafe==3.0.2
mdit-py-plugins==0.4.2
mdurl==0.1.2
msgpack==1.1.1
multidict==6.6.3
platformdirs==4.3.8
proces==0.1.7
propcache==0.3.2
pydantic==2.11.7
pydantic_core==2.33.2
Pygments==2.19.2
rich==14.0.0
sniffio==1.3.1
textual==3.6.0
textual-dev==1.7.0
textual-serve==1.1.2
textual-web==0.4.2
tomli==2.2.1
typing-inspection==0.4.1
typing_extensions==4.14.1
uc-micro-py==1.0.3
xdg==6.0.0
yarl==1.20.1
zipp==3.23.0

3
serve.py Normal file
View File

@ -0,0 +1,3 @@
from textual_serve.server import Server
server = Server("python3 main.py", title="辅助记忆程序", host="0.0.0.0")
server.serve()

38
styles.css Normal file
View File

@ -0,0 +1,38 @@
Screen {
align: center middle;
}
#main_container {
align: center middle;
width: 80%;
height: auto;
border: thick $primary-lighten-2;
padding: 2;
}
#sentence {
content-align: center middle;
width: 100%;
height: 5;
margin-bottom: 2;
}
#progress {
width: 100%;
content-align: center middle;
margin-bottom: 1;
color: $text-muted;
}
#button_container {
align-horizontal: center;
width: 100%;
height: auto;
}
Button {
width: 50%;
margin: 0 1;
}