feat: 增加打开本地文件功能
## ✨ 新功能 - 添加「📂 打开文件」按钮 - 使用 rfd 文件选择对话框 - 支持 PDF, EPUB, MOBI, TXT, Markdown, 代码文件 ## 🛠 技术实现 - 添加 rfd = "0.14" 依赖 - 实现 open_local_file() 异步函数 - 添加 DocumentViewer 组件显示文档信息 - 自动将打开的文件添加到书库 ## 🎨 UI 改进 - 侧边栏添加打开文件按钮 - 文档查看器显示格式、页数、大小 - 错误处理与友好提示 --- 📅 开发日期:2026-03-10
This commit is contained in:
176
src/ui/mod.rs
176
src/ui/mod.rs
@@ -7,6 +7,7 @@
|
||||
use dioxus::prelude::*;
|
||||
use crate::config::{ThemeMode, load};
|
||||
use crate::library::{Library, LibraryItem};
|
||||
use crate::core::document::DocumentEngine;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// 选中的文件类型
|
||||
@@ -46,6 +47,9 @@ fn App() -> Element {
|
||||
|
||||
// 设置面板显示
|
||||
let mut show_settings = use_signal(|| false);
|
||||
|
||||
// 阅读器视图:显示打开的文档
|
||||
let opened_document = use_signal(|| None::<String>);
|
||||
|
||||
// 主题
|
||||
let is_dark = config.theme.mode == ThemeMode::Dark;
|
||||
@@ -60,10 +64,22 @@ fn App() -> Element {
|
||||
div { class: "sidebar",
|
||||
div { class: "sidebar-header",
|
||||
h1 { "ReadFlow" }
|
||||
button {
|
||||
class: "settings-btn",
|
||||
onclick: move |_| show_settings.set(!show_settings()),
|
||||
"⚙️"
|
||||
div { class: "header-actions",
|
||||
button {
|
||||
class: "open-file-btn",
|
||||
onclick: move |_| {
|
||||
// 打开文件选择对话框
|
||||
spawn(async move {
|
||||
open_local_file(library, opened_document).await;
|
||||
});
|
||||
},
|
||||
"📂 打开文件"
|
||||
}
|
||||
button {
|
||||
class: "settings-btn",
|
||||
onclick: move |_| show_settings.set(!show_settings()),
|
||||
"⚙️"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,7 +134,12 @@ fn App() -> Element {
|
||||
|
||||
// 主内容区
|
||||
div { class: "main-content",
|
||||
if let Some(path) = selected() {
|
||||
if let Some(doc_path) = opened_document() {
|
||||
div { class: "reader",
|
||||
h2 { "正在阅读:{doc_path}" }
|
||||
DocumentViewer { path: doc_path }
|
||||
}
|
||||
} else if let Some(path) = selected() {
|
||||
div { class: "reader",
|
||||
h2 { "正在阅读: {path}" }
|
||||
p { "阅读器功能开发中..." }
|
||||
@@ -126,7 +147,8 @@ fn App() -> Element {
|
||||
} else {
|
||||
div { class: "welcome",
|
||||
h2 { "欢迎使用 ReadFlow" }
|
||||
p { "从左侧选择一个文件开始阅读" }
|
||||
p { "从左侧选择一个文件,或点击「📂 打开文件」按钮" }
|
||||
p { "支持格式:PDF, EPUB, MOBI, TXT, Markdown, 代码文件" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -473,9 +495,151 @@ fn get_app_css() -> &'static str {
|
||||
color: #333;
|
||||
border-color: #ddd;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.open-file-btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: #e94560;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.open-file-btn:hover {
|
||||
background: #ff6b6b;
|
||||
}
|
||||
|
||||
.document-viewer {
|
||||
width: 100%;
|
||||
max-width: 900px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.doc-header {
|
||||
margin-bottom: 30px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 2px solid #e94560;
|
||||
}
|
||||
|
||||
.doc-header h3 {
|
||||
font-size: 24px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.doc-meta {
|
||||
font-size: 14px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.doc-content {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 8px;
|
||||
padding: 30px;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.error-viewer {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
background: rgba(233, 69, 96, 0.1);
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e94560;
|
||||
}
|
||||
|
||||
.error-viewer h3 {
|
||||
color: #e94560;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
"#
|
||||
}
|
||||
|
||||
/// 打开本地文件选择对话框并加载文件
|
||||
async fn open_local_file(mut library: Signal<Library>, mut opened_document: Signal<Option<String>>) {
|
||||
use rfd::FileDialog;
|
||||
|
||||
// 使用 rfd (Rust File Dialog) 打开文件选择器
|
||||
let file = FileDialog::new()
|
||||
.add_filter("文档", &["pdf", "epub", "mobi", "azw3", "txt", "md"])
|
||||
.add_filter("代码文件", &["rs", "py", "js", "ts", "go", "java", "c", "cpp", "h", "css", "html", "json", "xml", "yaml", "yml", "toml", "sql", "sh", "bash", "zsh"])
|
||||
.set_title("选择要打开的文件")
|
||||
.pick_file();
|
||||
|
||||
if let Some(path) = file {
|
||||
let path_str = path.to_string_lossy().to_string();
|
||||
|
||||
// 添加到书库(如果尚未存在)
|
||||
let path_obj = std::path::Path::new(&path_str);
|
||||
let _ = library.write().add_file(path_obj);
|
||||
|
||||
// 打开文档
|
||||
opened_document.set(Some(path_str.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
/// 文档查看器组件
|
||||
#[component]
|
||||
fn DocumentViewer(path: String) -> Element {
|
||||
// 尝试加载文档
|
||||
let engine = DocumentEngine::new();
|
||||
|
||||
match engine.open(&path) {
|
||||
Ok(doc) => {
|
||||
let format_str = format!("{:?}", doc.format);
|
||||
let page_count = doc.metadata.page_count;
|
||||
let size_str = format_size(doc.metadata.file_size);
|
||||
|
||||
rsx! {
|
||||
div { class: "document-viewer",
|
||||
div { class: "doc-header",
|
||||
h3 { "{doc.title}" }
|
||||
p { class: "doc-meta",
|
||||
"格式:{format_str} | 页数:{page_count} | 大小:{size_str}"
|
||||
}
|
||||
}
|
||||
div { class: "doc-content",
|
||||
p { "文档已加载,阅读器渲染功能开发中..." }
|
||||
p { "文件路径:{path}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let error_msg = e.to_string();
|
||||
rsx! {
|
||||
div { class: "error-viewer",
|
||||
h3 { "❌ 打开文件失败" }
|
||||
p { "{error_msg}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 格式化文件大小
|
||||
fn format_size(size: u64) -> String {
|
||||
const KB: u64 = 1024;
|
||||
const MB: u64 = KB * 1024;
|
||||
const GB: u64 = MB * 1024;
|
||||
|
||||
if size >= GB {
|
||||
format!("{:.2} GB", size as f64 / GB as f64)
|
||||
} else if size >= MB {
|
||||
format!("{:.2} MB", size as f64 / MB as f64)
|
||||
} else if size >= KB {
|
||||
format!("{:.2} KB", size as f64 / KB as f64)
|
||||
} else {
|
||||
format!("{} B", size)
|
||||
}
|
||||
}
|
||||
|
||||
/// 启动 GUI
|
||||
pub fn run() {
|
||||
// 使用 Dioxus 启动
|
||||
|
||||
Reference in New Issue
Block a user