mirror of
https://github.com/lllin000/PaperForge.git
synced 2026-07-22 06:50:53 +00:00
715 lines
24 KiB
Python
715 lines
24 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
PaperForge Lite Setup Wizard (Textual Step-by-Step)
|
|
====================================================
|
|
基于 Textual ContentSwitcher + Tree + ProgressBar 的步骤向导。
|
|
|
|
Usage:
|
|
python setup_wizard.py --vault /path/to/vault
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import platform
|
|
import subprocess
|
|
import sys
|
|
import webbrowser
|
|
import winreg
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
from textual.app import App, ComposeResult
|
|
from textual.containers import Container, Grid, Horizontal, Vertical
|
|
from textual.reactive import reactive
|
|
from textual.screen import Screen
|
|
from textual.widgets import (
|
|
Button,
|
|
ContentSwitcher,
|
|
Footer,
|
|
Header,
|
|
Label,
|
|
Markdown,
|
|
ProgressBar,
|
|
Static,
|
|
Tree,
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# Detection Logic (unchanged from previous version)
|
|
# =============================================================================
|
|
|
|
class CheckResult:
|
|
def __init__(self, name: str):
|
|
self.name = name
|
|
self.passed = False
|
|
self.detail = ""
|
|
self.action_required = False
|
|
|
|
|
|
class EnvChecker:
|
|
"""环境检测器"""
|
|
|
|
def __init__(self, vault: Path):
|
|
self.vault = vault
|
|
self.results: dict[str, CheckResult] = {
|
|
"python": CheckResult("Python 版本"),
|
|
"vault": CheckResult("Vault 结构"),
|
|
"zotero": CheckResult("Zotero 安装"),
|
|
"bbt": CheckResult("Better BibTeX"),
|
|
"json": CheckResult("JSON 导出"),
|
|
}
|
|
|
|
def check_python(self) -> CheckResult:
|
|
r = self.results["python"]
|
|
v = sys.version_info
|
|
if v >= (3, 8):
|
|
r.passed = True
|
|
r.detail = f"Python {v.major}.{v.minor}.{v.micro}"
|
|
else:
|
|
r.passed = False
|
|
r.detail = f"Python {v.major}.{v.minor}.{v.micro} (需要 >= 3.8)"
|
|
r.action_required = True
|
|
return r
|
|
|
|
def check_vault(self) -> CheckResult:
|
|
r = self.results["vault"]
|
|
required = [
|
|
"03_Resources/LiteratureControl/library-records",
|
|
"99_System/LiteraturePipeline/exports",
|
|
"99_System/LiteraturePipeline/ocr",
|
|
"99_System/Template",
|
|
]
|
|
missing = [rel for rel in required if not (self.vault / rel).exists()]
|
|
if not missing:
|
|
r.passed = True
|
|
r.detail = "所有必要目录已就绪"
|
|
else:
|
|
r.passed = False
|
|
r.detail = f"缺少: {', '.join(missing)}"
|
|
r.action_required = True
|
|
return r
|
|
|
|
def _find_zotero(self) -> Optional[Path]:
|
|
system = platform.system()
|
|
if system == "Windows":
|
|
try:
|
|
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Zotero") as key:
|
|
install_dir, _ = winreg.QueryValueEx(key, "InstallDir")
|
|
path = Path(install_dir) / "zotero.exe"
|
|
if path.exists():
|
|
return path
|
|
except (FileNotFoundError, OSError):
|
|
pass
|
|
for p in [
|
|
Path(os.environ.get("PROGRAMFILES", r"C:\Program Files")) / "Zotero" / "zotero.exe",
|
|
Path(os.environ.get("LOCALAPPDATA", r"C:\Users\%USERNAME%\AppData\Local")) / "Zotero" / "zotero.exe",
|
|
]:
|
|
if p.exists():
|
|
return p
|
|
elif system == "Darwin":
|
|
p = Path("/Applications/Zotero.app/Contents/MacOS/zotero")
|
|
if p.exists():
|
|
return p
|
|
else:
|
|
for p in [Path.home() / ".local" / "share" / "zotero" / "zotero"]:
|
|
if p.exists():
|
|
return p
|
|
try:
|
|
result = subprocess.run(["which", "zotero"], capture_output=True, text=True)
|
|
if result.returncode == 0:
|
|
return Path(result.stdout.strip())
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
def check_zotero(self) -> CheckResult:
|
|
r = self.results["zotero"]
|
|
path = self._find_zotero()
|
|
if path:
|
|
r.passed = True
|
|
r.detail = str(path)
|
|
else:
|
|
r.passed = False
|
|
r.detail = "未找到 Zotero"
|
|
r.action_required = True
|
|
return r
|
|
|
|
def check_bbt(self) -> CheckResult:
|
|
r = self.results["bbt"]
|
|
system = platform.system()
|
|
bbt_found = False
|
|
bbt_path = None
|
|
|
|
if system == "Windows":
|
|
appdata = os.environ.get("APPDATA", "")
|
|
if appdata:
|
|
profiles = Path(appdata) / "Zotero" / "Zotero" / "Profiles"
|
|
if profiles.exists():
|
|
for profile in profiles.iterdir():
|
|
if profile.is_dir():
|
|
ext_dir = profile / "extensions"
|
|
if ext_dir.exists():
|
|
for ext in ext_dir.iterdir():
|
|
if "better-bibtex" in ext.name.lower() or "betterbibtex" in ext.name.lower():
|
|
bbt_found = True
|
|
bbt_path = ext
|
|
break
|
|
elif system == "Darwin":
|
|
profiles = Path.home() / "Library" / "Application Support" / "Zotero" / "Profiles"
|
|
if profiles.exists():
|
|
for profile in profiles.iterdir():
|
|
ext_dir = profile / "extensions"
|
|
if ext_dir.exists():
|
|
for ext in ext_dir.iterdir():
|
|
if "better-bibtex" in ext.name.lower():
|
|
bbt_found = True
|
|
bbt_path = ext
|
|
break
|
|
else:
|
|
profiles = Path.home() / ".zotero" / "zotero" / "Profiles"
|
|
if profiles.exists():
|
|
for profile in profiles.iterdir():
|
|
ext_dir = profile / "extensions"
|
|
if ext_dir.exists():
|
|
for ext in ext_dir.iterdir():
|
|
if "better-bibtex" in ext.name.lower():
|
|
bbt_found = True
|
|
bbt_path = ext
|
|
break
|
|
|
|
if bbt_found:
|
|
r.passed = True
|
|
r.detail = bbt_path.name if bbt_path else "Better BibTeX"
|
|
else:
|
|
r.passed = False
|
|
r.detail = "未找到 Better BibTeX 插件"
|
|
r.action_required = True
|
|
return r
|
|
|
|
def check_json(self) -> CheckResult:
|
|
r = self.results["json"]
|
|
exports_dir = self.vault / "99_System" / "LiteraturePipeline" / "exports"
|
|
if not exports_dir.exists():
|
|
r.passed = False
|
|
r.detail = f"导出目录不存在: {exports_dir}"
|
|
r.action_required = True
|
|
return r
|
|
|
|
json_files = list(exports_dir.glob("*.json"))
|
|
if not json_files:
|
|
r.passed = False
|
|
r.detail = "未找到 JSON 导出文件"
|
|
r.action_required = True
|
|
return r
|
|
|
|
valid = []
|
|
for jf in json_files:
|
|
try:
|
|
data = json.loads(jf.read_text(encoding="utf-8"))
|
|
if isinstance(data, list) and len(data) > 0:
|
|
valid.append(jf.name)
|
|
except Exception:
|
|
pass
|
|
|
|
if valid:
|
|
r.passed = True
|
|
r.detail = f"找到 {len(valid)} 个有效 JSON"
|
|
else:
|
|
r.passed = False
|
|
r.detail = "JSON 文件格式无效"
|
|
r.action_required = True
|
|
return r
|
|
|
|
|
|
# =============================================================================
|
|
# Step Screens
|
|
# =============================================================================
|
|
|
|
STEP_TITLES = [
|
|
"欢迎使用 PaperForge",
|
|
"检查 Python 版本",
|
|
"检查 Vault 结构",
|
|
"安装 Zotero",
|
|
"安装 Better BibTeX",
|
|
"配置 JSON 导出",
|
|
"安装完成",
|
|
]
|
|
|
|
STEP_IDS = [f"step-{i}" for i in range(len(STEP_TITLES))]
|
|
|
|
|
|
class StepScreen(Static):
|
|
"""单个步骤页面基类"""
|
|
|
|
def __init__(self, step_id: str, checker: EnvChecker, **kwargs):
|
|
super().__init__(**kwargs)
|
|
self.step_id = step_id
|
|
self.checker = checker
|
|
self.step_idx = int(step_id.split("-")[1])
|
|
|
|
def compose(self) -> ComposeResult:
|
|
yield Static(f"## {STEP_TITLES[self.step_idx]}", classes="step-title")
|
|
yield Static("", id=f"{self.step_id}-status", classes="status-bar")
|
|
|
|
def set_status(self, text: str, success: bool | None = None) -> None:
|
|
status = self.query_one(f"#{self.step_id}-status", Static)
|
|
if success is True:
|
|
status.update(f"[green]✓ {text}[/]")
|
|
elif success is False:
|
|
status.update(f"[red]✗ {text}[/]")
|
|
else:
|
|
status.update(text)
|
|
|
|
|
|
class WelcomeStep(StepScreen):
|
|
"""Step 0: 欢迎页"""
|
|
|
|
def compose(self) -> ComposeResult:
|
|
yield from super().compose()
|
|
yield Markdown("""
|
|
**PaperForge Lite** 是一个连接 Zotero 与 Obsidian 的文献工作流工具。
|
|
|
|
安装向导将引导你完成以下配置:
|
|
|
|
1. 确认 Python 版本 (>= 3.8)
|
|
2. 检查 Vault 目录结构
|
|
3. 确认 Zotero 已安装
|
|
4. 确认 Better BibTeX 插件已安装
|
|
5. 确认 JSON 自动导出已配置
|
|
|
|
点击 **开始安装** 继续。
|
|
""")
|
|
yield Button("▶ 开始安装", id="btn-start", variant="primary")
|
|
|
|
|
|
class PythonStep(StepScreen):
|
|
"""Step 1: Python 版本"""
|
|
|
|
def compose(self) -> ComposeResult:
|
|
yield from super().compose()
|
|
yield Markdown("""
|
|
PaperForge 需要 **Python 3.8 或更高版本**。
|
|
|
|
当前环境将自动检测,无需手动操作。
|
|
""")
|
|
yield Horizontal(
|
|
Button("🔍 自动检测", id="btn-check-python", variant="primary"),
|
|
Button("⬇ 下载 Python", id="btn-dl-python", variant="default"),
|
|
id="btn-row",
|
|
)
|
|
|
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
if event.button.id == "btn-check-python":
|
|
result = self.checker.check_python()
|
|
self.set_status(result.detail, result.passed)
|
|
if result.passed:
|
|
self.app.post_message(StepPassed(self.step_idx))
|
|
elif event.button.id == "btn-dl-python":
|
|
webbrowser.open("https://www.python.org/downloads/")
|
|
|
|
|
|
class VaultStep(StepScreen):
|
|
"""Step 2: Vault 结构"""
|
|
|
|
def compose(self) -> ComposeResult:
|
|
yield from super().compose()
|
|
yield Markdown("""
|
|
PaperForge 需要特定的目录结构来存放文献数据。
|
|
|
|
必要目录:
|
|
- `03_Resources/LiteratureControl/library-records/` — 文献状态跟踪
|
|
- `99_System/LiteraturePipeline/exports/` — Zotero JSON 导出
|
|
- `99_System/LiteraturePipeline/ocr/` — OCR 结果
|
|
- `99_System/Template/` — 模板文件
|
|
""")
|
|
yield Horizontal(
|
|
Button("🔍 自动检测", id="btn-check-vault", variant="primary"),
|
|
Button("📁 一键创建目录", id="btn-create-vault", variant="default"),
|
|
id="btn-row",
|
|
)
|
|
|
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
if event.button.id == "btn-check-vault":
|
|
result = self.checker.check_vault()
|
|
self.set_status(result.detail, result.passed)
|
|
if result.passed:
|
|
self.app.post_message(StepPassed(self.step_idx))
|
|
elif event.button.id == "btn-create-vault":
|
|
dirs = [
|
|
"03_Resources/LiteratureControl/library-records",
|
|
"99_System/LiteraturePipeline/exports",
|
|
"99_System/LiteraturePipeline/ocr",
|
|
"99_System/Template",
|
|
]
|
|
for d in dirs:
|
|
(self.checker.vault / d).mkdir(parents=True, exist_ok=True)
|
|
self.set_status("目录已创建,请重新检测", None)
|
|
|
|
|
|
class ZoteroStep(StepScreen):
|
|
"""Step 3: Zotero"""
|
|
|
|
def compose(self) -> ComposeResult:
|
|
yield from super().compose()
|
|
yield Markdown("""
|
|
**Zotero** 是必需的文献管理软件。
|
|
|
|
如果你尚未安装,请点击 **下载 Zotero** 前往官网下载。
|
|
|
|
安装完成后,点击 **检测** 确认。
|
|
""")
|
|
yield Horizontal(
|
|
Button("🔍 自动检测", id="btn-check-zotero", variant="primary"),
|
|
Button("⬇ 下载 Zotero", id="btn-dl-zotero", variant="default"),
|
|
Button("📷 查看安装截图", id="btn-img-zotero", variant="default"),
|
|
id="btn-row",
|
|
)
|
|
|
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
if event.button.id == "btn-check-zotero":
|
|
result = self.checker.check_zotero()
|
|
self.set_status(result.detail, result.passed)
|
|
if result.passed:
|
|
self.app.post_message(StepPassed(self.step_idx))
|
|
elif event.button.id == "btn-dl-zotero":
|
|
webbrowser.open("https://www.zotero.org/download/")
|
|
elif event.button.id == "btn-img-zotero":
|
|
self.app.open_screenshot("zotero-install.png")
|
|
|
|
|
|
class BBTStep(StepScreen):
|
|
"""Step 4: Better BibTeX"""
|
|
|
|
def compose(self) -> ComposeResult:
|
|
yield from super().compose()
|
|
yield Markdown("""
|
|
**Better BibTeX (BBT)** 是 Zotero 的插件,用于生成 citation key 和自动导出 JSON。
|
|
|
|
**安装步骤:**
|
|
1. 下载 BBT 插件
|
|
2. Zotero → 工具 → 插件
|
|
3. 齿轮图标 → Install Plugin From File...
|
|
4. 选择下载的 `.xpi` 文件 → 重启 Zotero
|
|
|
|
安装完成后点击 **检测**。
|
|
""")
|
|
yield Horizontal(
|
|
Button("🔍 自动检测", id="btn-check-bbt", variant="primary"),
|
|
Button("⬇ 下载 BBT", id="btn-dl-bbt", variant="default"),
|
|
Button("📷 查看安装截图", id="btn-img-bbt", variant="default"),
|
|
id="btn-row",
|
|
)
|
|
|
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
if event.button.id == "btn-check-bbt":
|
|
result = self.checker.check_bbt()
|
|
self.set_status(result.detail, result.passed)
|
|
if result.passed:
|
|
self.app.post_message(StepPassed(self.step_idx))
|
|
elif event.button.id == "btn-dl-bbt":
|
|
webbrowser.open("https://retorque.re/zotero-better-bibtex/")
|
|
elif event.button.id == "btn-img-bbt":
|
|
self.app.open_screenshot("bbt-install.png")
|
|
|
|
|
|
class JsonStep(StepScreen):
|
|
"""Step 5: JSON Export"""
|
|
|
|
def compose(self) -> ComposeResult:
|
|
yield from super().compose()
|
|
exports_dir = self.checker.vault / "99_System" / "LiteraturePipeline" / "exports"
|
|
yield Markdown(f"""
|
|
**Better BibTeX 自动导出**是 PaperForge 的数据来源。
|
|
|
|
**配置步骤:**
|
|
1. Zotero → 文件 → 导出库...
|
|
2. 格式选择 **Better BibLaTeX**
|
|
3. 保存到:`{exports_dir}/library.json`
|
|
4. 勾选 **Keep updated**(自动保持更新)
|
|
|
|
每个 JSON 文件对应一个 Obsidian Base 视图。
|
|
""")
|
|
yield Horizontal(
|
|
Button("🔍 自动检测", id="btn-check-json", variant="primary"),
|
|
Button("📷 查看配置截图", id="btn-img-json", variant="default"),
|
|
id="btn-row",
|
|
)
|
|
|
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
if event.button.id == "btn-check-json":
|
|
result = self.checker.check_json()
|
|
self.set_status(result.detail, result.passed)
|
|
if result.passed:
|
|
self.app.post_message(StepPassed(self.step_idx))
|
|
elif event.button.id == "btn-img-json":
|
|
self.app.open_screenshot("json-export.png")
|
|
|
|
|
|
class DoneStep(StepScreen):
|
|
"""Step 6: 完成页"""
|
|
|
|
def compose(self) -> ComposeResult:
|
|
yield from super().compose()
|
|
yield Markdown("""
|
|
## 🎉 安装完成!
|
|
|
|
所有前置条件已满足,你现在可以开始使用 PaperForge Lite。
|
|
|
|
### 首次使用步骤:
|
|
|
|
**1. 同步 Zotero 文献**
|
|
```bash
|
|
python pipeline/worker/scripts/literature_pipeline.py --vault . selection-sync
|
|
```
|
|
|
|
**2. 生成正式笔记**
|
|
```bash
|
|
python pipeline/worker/scripts/literature_pipeline.py --vault . index-refresh
|
|
```
|
|
|
|
**3. 标记精读文献**
|
|
在 Obsidian 中打开 library-records 文件,设置:
|
|
- `do_ocr: true`
|
|
- `analyze: true`
|
|
|
|
**4. 运行 OCR**
|
|
```bash
|
|
python pipeline/worker/scripts/literature_pipeline.py --vault . ocr
|
|
```
|
|
|
|
**5. 执行精读**
|
|
```
|
|
/LD-deep <zotero_key>
|
|
```
|
|
|
|
### 常用命令速查
|
|
|
|
| 命令 | 作用 |
|
|
|------|------|
|
|
| `selection-sync` | 检测新文献 |
|
|
| `index-refresh` | 生成正式笔记 |
|
|
| `ocr` | PDF OCR |
|
|
| `deep-reading` | 查看精读队列 |
|
|
| `update` | 检查更新 |
|
|
|
|
### 详细文档
|
|
|
|
- 安装后指南:`AGENTS.md`
|
|
- 图表阅读指南:`99_System/Template/科研读图指南.md`
|
|
- GitHub: https://github.com/LLLin000/PaperForge
|
|
""")
|
|
yield Horizontal(
|
|
Button("📖 打开详细指南", id="btn-open-guide", variant="primary"),
|
|
Button("🔄 重新检测", id="btn-restart", variant="default"),
|
|
id="btn-row",
|
|
)
|
|
|
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
if event.button.id == "btn-open-guide":
|
|
guide_path = self.checker.vault / "docs" / "setup-guide.md"
|
|
if guide_path.exists():
|
|
if sys.platform == "win32":
|
|
os.startfile(str(guide_path))
|
|
elif sys.platform == "darwin":
|
|
subprocess.run(["open", str(guide_path)])
|
|
else:
|
|
subprocess.run(["xdg-open", str(guide_path)])
|
|
else:
|
|
webbrowser.open("https://github.com/LLLin000/PaperForge/blob/master/docs/setup-guide.md")
|
|
elif event.button.id == "btn-restart":
|
|
self.app.post_message(RestartWizard())
|
|
|
|
|
|
# =============================================================================
|
|
# Custom Messages
|
|
# =============================================================================
|
|
|
|
class StepPassed:
|
|
"""步骤通过消息"""
|
|
def __init__(self, step_idx: int):
|
|
self.step_idx = step_idx
|
|
|
|
|
|
class RestartWizard:
|
|
"""重新开始消息"""
|
|
pass
|
|
|
|
|
|
# =============================================================================
|
|
# Main App
|
|
# =============================================================================
|
|
|
|
class SetupWizardApp(App):
|
|
"""PaperForge 安装向导主应用"""
|
|
|
|
CSS = """
|
|
Screen { align: center middle; }
|
|
|
|
.wizard-container { width: 95%; height: 95%; border: solid green; }
|
|
|
|
.sidebar { width: 25%; height: 100%; border: solid gray; padding: 1; }
|
|
.sidebar-title { text-align: center; text-style: bold; color: cyan; padding: 1; }
|
|
.step-tree { height: 1fr; }
|
|
|
|
.main-area { width: 75%; height: 100%; padding: 1; }
|
|
.progress-area { height: auto; padding: 0 1; }
|
|
.content-area { height: 1fr; border: solid blue; padding: 1; }
|
|
|
|
.step-title { text-style: bold; color: yellow; }
|
|
.status-bar { height: auto; padding: 1; }
|
|
|
|
.step-content { padding: 1; }
|
|
.step-content Markdown { padding: 0 1; }
|
|
|
|
#btn-row { height: auto; padding: 1; }
|
|
#btn-row Button { margin: 0 1; }
|
|
|
|
.done { color: green; }
|
|
.current { color: yellow; text-style: bold; }
|
|
.pending { color: gray; }
|
|
"""
|
|
|
|
BINDINGS = [
|
|
("q", "quit", "退出"),
|
|
("n", "next_step", "下一步"),
|
|
("p", "prev_step", "上一步"),
|
|
]
|
|
|
|
current_step = reactive(0)
|
|
step_states = reactive([False] * len(STEP_TITLES))
|
|
|
|
def __init__(self, vault: Path):
|
|
super().__init__()
|
|
self.vault = vault
|
|
self.checker = EnvChecker(vault)
|
|
self.step_screens: dict[str, StepScreen] = {}
|
|
|
|
def compose(self) -> ComposeResult:
|
|
yield Header(show_clock=False)
|
|
|
|
with Container(classes="wizard-container"):
|
|
with Horizontal():
|
|
# 左侧:步骤导航树
|
|
with Vertical(classes="sidebar"):
|
|
yield Static("安装步骤", classes="sidebar-title")
|
|
tree = Tree("PaperForge Lite", id="step-tree", classes="step-tree")
|
|
for i, title in enumerate(STEP_TITLES):
|
|
tree.root.add_leaf(f"{i}. {title}")
|
|
yield tree
|
|
|
|
# 右侧:主内容区
|
|
with Vertical(classes="main-area"):
|
|
# 进度条
|
|
with Container(classes="progress-area"):
|
|
yield ProgressBar(total=len(STEP_TITLES), show_eta=False, id="progress")
|
|
yield Static("Step 0 / 6", id="progress-text", classes="progress-text")
|
|
|
|
# 内容切换器
|
|
with ContentSwitcher(id="content-switcher", classes="content-area"):
|
|
screens = [
|
|
WelcomeStep("step-0", self.checker),
|
|
PythonStep("step-1", self.checker),
|
|
VaultStep("step-2", self.checker),
|
|
ZoteroStep("step-3", self.checker),
|
|
BBTStep("step-4", self.checker),
|
|
JsonStep("step-5", self.checker),
|
|
DoneStep("step-6", self.checker),
|
|
]
|
|
for screen in screens:
|
|
self.step_screens[screen.step_id] = screen
|
|
yield screen
|
|
|
|
yield Footer()
|
|
|
|
def on_mount(self) -> None:
|
|
self._update_step_display()
|
|
|
|
def watch_current_step(self, step: int) -> None:
|
|
self._update_step_display()
|
|
|
|
def watch_step_states(self, states: list[bool]) -> None:
|
|
self._update_step_display()
|
|
|
|
def _update_step_display(self) -> None:
|
|
# 更新 ContentSwitcher
|
|
switcher = self.query_one("#content-switcher", ContentSwitcher)
|
|
switcher.current = f"step-{self.current_step}"
|
|
|
|
# 更新进度条
|
|
progress = self.query_one("#progress", ProgressBar)
|
|
progress.advance(self.current_step - progress.progress)
|
|
|
|
progress_text = self.query_one("#progress-text", Static)
|
|
progress_text.update(f"Step {self.current_step} / {len(STEP_TITLES) - 1}: {STEP_TITLES[self.current_step]}")
|
|
|
|
# 更新 Tree 高亮
|
|
tree = self.query_one("#step-tree", Tree)
|
|
for i, node in enumerate(tree.root.children):
|
|
if i < self.current_step:
|
|
node.label = f"[green]✓ {i}. {STEP_TITLES[i]}[/]"
|
|
elif i == self.current_step:
|
|
node.label = f"[yellow]▶ {i}. {STEP_TITLES[i]}[/]"
|
|
else:
|
|
node.label = f"[gray]○ {i}. {STEP_TITLES[i]}[/]"
|
|
|
|
def action_next_step(self) -> None:
|
|
if self.current_step < len(STEP_TITLES) - 1:
|
|
self.current_step += 1
|
|
|
|
def action_prev_step(self) -> None:
|
|
if self.current_step > 0:
|
|
self.current_step -= 1
|
|
|
|
def on_step_passed(self, message: StepPassed) -> None:
|
|
"""步骤通过,标记完成并自动前进"""
|
|
self.step_states[message.step_idx] = True
|
|
if message.step_idx < len(STEP_TITLES) - 1:
|
|
self.current_step = message.step_idx + 1
|
|
|
|
def on_restart_wizard(self) -> None:
|
|
"""重新开始"""
|
|
self.current_step = 0
|
|
self.step_states = [False] * len(STEP_TITLES)
|
|
for screen in self.step_screens.values():
|
|
if hasattr(screen, 'set_status'):
|
|
screen.set_status("")
|
|
|
|
def open_screenshot(self, filename: str) -> None:
|
|
"""打开截图"""
|
|
img_path = self.vault / "docs" / "images" / filename
|
|
if img_path.exists():
|
|
if sys.platform == "win32":
|
|
os.startfile(str(img_path))
|
|
elif sys.platform == "darwin":
|
|
subprocess.run(["open", str(img_path)])
|
|
else:
|
|
subprocess.run(["xdg-open", str(img_path)])
|
|
else:
|
|
# 截图不存在,打开在线文档
|
|
webbrowser.open(f"https://github.com/LLLin000/PaperForge/blob/master/docs/images/{filename}")
|
|
|
|
|
|
# =============================================================================
|
|
# Entry
|
|
# =============================================================================
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="PaperForge Lite 安装向导")
|
|
parser.add_argument("--vault", type=Path, default=Path("."), help="Vault 路径")
|
|
args = parser.parse_args()
|
|
|
|
vault = args.vault.resolve()
|
|
if not (vault / "paperforge.json").exists():
|
|
print(f"[ERR] 未找到 paperforge.json: {vault}")
|
|
print("请先在 Vault 根目录运行 setup.py 完成初始安装")
|
|
return 1
|
|
|
|
app = SetupWizardApp(vault)
|
|
app.run()
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|