Files
HeurAMS/src/heurams/interface/screens/favmgr.py
2026-01-08 00:05:00 +08:00

205 lines
7.5 KiB
Python

"""收藏夹管理器界面"""
import base64
from pathlib import Path
from typing import List, Optional
from textual.app import ComposeResult
from textual.containers import ScrollableContainer
from textual.screen import Screen
from textual.widgets import (
Button,
Footer,
Header,
Label,
ListItem,
ListView,
Markdown,
Static,
)
from heurams.context import config_var
from heurams.kernel.repolib import Repo
from heurams.services.favorite_service import FavoriteItem, favorite_manager
from heurams.services.logger import get_logger
logger = get_logger(__name__)
class FavoriteManagerScreen(Screen):
"""收藏夹管理器屏幕"""
SUB_TITLE = "收藏夹"
BINDINGS = [
("q", "go_back", "返回"),
("d", "toggle_dark", ""),
]
def __init__(
self,
name: str | None = None,
id: str | None = None,
classes: str | None = None,
) -> None:
super().__init__(name, id, classes)
self.favorites: List[FavoriteItem] = []
self._load_favorites()
def _load_favorites(self) -> None:
"""加载收藏列表"""
self.favorites = favorite_manager.get_all()
logger.debug("加载 %d 个收藏项", len(self.favorites))
def compose(self) -> ComposeResult:
"""组合界面组件"""
yield Header(show_clock=True)
with ScrollableContainer(id="favorites-container"):
if not self.favorites:
yield Label("暂无收藏", classes="empty-label")
yield Static("使用 * 键在记忆界面中添加收藏.")
else:
yield Label(f"{len(self.favorites)} 个收藏项", classes="count-label")
yield ListView(id="favorites-list")
yield Footer()
def on_mount(self) -> None:
"""挂载后填充列表"""
if self.favorites:
list_view = self.query_one("#favorites-list")
for fav in self.favorites:
list_view.append(self._create_favorite_item(fav)) # type: ignore
def _encode_favorite_key(self, repo_path: str, ident: str) -> str:
"""编码仓库路径和标识符为安全的按钮 ID 部分"""
# 使用 \x00 分隔两部分,然后进行 base64 编码
combined = f"{repo_path}\x00{ident}"
encoded = base64.urlsafe_b64encode(combined.encode()).decode()
# 去掉填充的等号
return encoded.rstrip("=")
def _decode_favorite_key(self, key: str) -> tuple[str, str]:
"""解码按钮 ID 部分为仓库路径和标识符"""
# 补全等号以使长度是4的倍数
padded = key + "=" * ((4 - len(key) % 4) % 4)
decoded = base64.urlsafe_b64decode(padded.encode()).decode()
repo_path, ident = decoded.split("\x00", 1)
return repo_path, ident
def _create_favorite_item(self, fav: FavoriteItem) -> ListItem:
"""创建收藏项列表项"""
# 尝试获取仓库信息
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
content_preview = repo_info.get("content_preview", "") if repo_info else ""
added_time = self._format_time(fav.added)
# 构建显示文本
display_text = f"[b]{title}[/b] ({fav.ident})\n"
if content_preview:
display_text += f"{content_preview}\n"
display_text += f"添加于: {added_time}"
if fav.tags:
display_text += f" 标签: {', '.join(fav.tags)}"
# 创建安全的按钮 ID
button_key = self._encode_favorite_key(fav.repo_path, fav.ident)
# 创建列表项,包含移除按钮
container = ScrollableContainer(
Markdown(display_text, classes="favorite-content"),
Button("移除", id=f"remove-{button_key}", variant="error"),
classes="favorite-item",
)
return ListItem(container)
def _get_repo_info(self, repo_path: str, fav: FavoriteItem) -> Optional[dict]:
"""获取仓库信息(标题、原子内容预览)"""
try:
data_repo = Path(config_var.get()["paths"]["data"]) / "repo"
repo_dir = data_repo / repo_path
if not repo_dir.exists():
logger.warning("仓库目录不存在: %s", repo_dir)
return None
repo = Repo.create_from_repodir(repo_dir)
# 获取原子内容预览
content_preview = ""
payload = repo.payload
# 查找对应 ident 的 payload 条目
for ident_key, content in payload:
if ident_key == fav.ident:
# 截断过长的内容
if isinstance(content, dict) and "content" in content:
text = content["content"]
else:
text = str(content)
if len(text) > 100:
content_preview = text[:100] + "..."
else:
content_preview = text
break
return {
"title": repo.manifest["title"],
"content_preview": content_preview,
}
except Exception as e:
logger.error("获取仓库信息失败: %s", e)
return None
def _format_time(self, timestamp: int) -> str:
"""格式化时间戳"""
from datetime import datetime
dt = datetime.fromtimestamp(timestamp)
return dt.strftime("%Y-%m-%d %H:%M")
def on_button_pressed(self, event: Button.Pressed) -> None:
"""处理按钮点击事件"""
button_id = event.button.id
if button_id and button_id.startswith("remove-"):
# 提取编码后的键
key = button_id[7:] # 去掉 "remove-" 前缀
try:
repo_path, ident = self._decode_favorite_key(key)
self._remove_favorite(repo_path, ident)
except Exception as e:
logger.error("解析按钮 ID 失败: %s", e)
self.app.notify("操作失败: 无效的按钮标识", severity="error")
def _remove_favorite(self, repo_path: str, ident: str) -> None:
"""移除收藏项"""
if favorite_manager.remove(repo_path, ident):
self.app.notify(f"已移除收藏: {ident}", severity="information")
# 重新加载列表
self._load_favorites()
# 刷新界面
self._refresh_list()
else:
self.app.notify(f"移除失败: {ident}", severity="error")
def _refresh_list(self) -> None:
"""刷新列表显示"""
container = self.query_one("#favorites-container")
# 清空容器
for child in container.children:
child.remove()
# 重新组合
if not self.favorites:
container.mount(Label("暂无收藏", classes="empty-label"))
container.mount(Static("使用 * 键在记忆界面中添加收藏。"))
else:
container.mount(
Label(f"{len(self.favorites)} 个收藏项", classes="count-label")
)
list_view = ListView(id="favorites-list")
container.mount(list_view)
for fav in self.favorites:
list_view.append(self._create_favorite_item(fav))
def action_go_back(self) -> None:
"""返回上一屏幕"""
self.app.pop_screen()
def action_toggle_dark(self) -> None:
"""切换暗黑模式"""
self.app.dark = not self.app.dark # type: ignore