diff --git a/Cargo.toml b/Cargo.toml index 3f684ed..d8289c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,9 @@ dirs = "5" chrono = { version = "0.4", features = ["serde"] } # 时间处理 uuid = { version = "1.0", features = ["v4"] } # UUID 生成 +# 文件对话框 +rfd = "0.14" + [features] default = ["desktop"] desktop = ["dioxus/desktop"] diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 6efc65b..5a1f900 100644 --- a/src/ui/mod.rs +++ b/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::); // 主题 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, mut opened_document: Signal>) { + 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 启动 diff --git a/src/ui/mod.rs.bak b/src/ui/mod.rs.bak new file mode 100644 index 0000000..d8355cb --- /dev/null +++ b/src/ui/mod.rs.bak @@ -0,0 +1,639 @@ +//! UI 模块 +//! +//! ReadFlow Dioxus GUI 界面 + +#![allow(non_snake_case)] + +use dioxus::prelude::*; +use crate::config::{ThemeMode, load}; +use crate::library::{Library, LibraryItem}; +use crate::core::document::DocumentEngine; +use std::path::PathBuf; + +/// 选中的文件类型 +#[derive(Debug, Clone, Copy, PartialEq, Default)] +enum FilterType { + #[default] + All, + Recent, + Pdf, + Epub, + Mobi, + Text, +} + +/// 主应用组件 +#[component] +fn App() -> Element { + // 加载配置 + let config = load(); + + // 书库路径 + let library_path = dirs::home_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join("ReadFlow"); + + // 初始化书库(创建时自动扫描) + let mut library = use_signal(|| Library::new(library_path)); + + // 选中状态 + let selected = use_signal(|| None::); + + // 过滤器 + let mut filter = use_signal(|| FilterType::All); + + // 搜索关键词 + let mut query = use_signal(|| String::new()); + + // 设置面板显示 + let mut show_settings = use_signal(|| false); + + // 阅读器视图:显示打开的文档 + let opened_document = use_signal(|| None::); + + // 主题 + let is_dark = config.theme.mode == ThemeMode::Dark; + + let theme_class = if is_dark { "dark" } else { "light" }; + + rsx! { + div { + class: "app-container {theme_class}", + + // 侧边栏 + div { class: "sidebar", + div { class: "sidebar-header", + h1 { "ReadFlow" } + div { class: "header-actions", + button { + class: "open-file-btn", + onclick: move |_| { + // 打开文件选择对话框 + spawn(async move { + open_local_file(library, opened_document); + }); + }, + "📂 打开文件" + } + button { + class: "settings-btn", + onclick: move |_| show_settings.set(!show_settings()), + "⚙️" + } + } + } + + // 搜索框 + div { class: "search-box", + input { + r#type: "text", + placeholder: "搜索文件...", + value: "{query}", + oninput: move |e| query.set(e.value().to_string()), + } + } + + // 过滤器 + div { class: "filter-buttons", + button { + class: if filter() == FilterType::All { "active" }, + onclick: move |_| filter.set(FilterType::All), + "全部" + } + button { + class: if filter() == FilterType::Recent { "active" }, + onclick: move |_| filter.set(FilterType::Recent), + "最近" + } + button { + class: if filter() == FilterType::Pdf { "active" }, + onclick: move |_| filter.set(FilterType::Pdf), + "PDF" + } + button { + class: if filter() == FilterType::Epub { "active" }, + onclick: move |_| filter.set(FilterType::Epub), + "EPUB" + } + button { + class: if filter() == FilterType::Mobi { "active" }, + onclick: move |_| filter.set(FilterType::Mobi), + "MOBI" + } + } + + // 文件列表 + div { class: "file-list", + // 获取并过滤文件列表(在渲染前准备好数据) + FileList { + items: get_filtered_items(library, filter(), query()), + selected: selected + } + } + } + + // 主内容区 + div { class: "main-content", + if let Some(path) = selected() { + div { class: "reader", + h2 { "正在阅读: {path}" } + p { "阅读器功能开发中..." } + } + } else { + div { class: "welcome", + h2 { "欢迎使用 ReadFlow" } + p { "从左侧选择一个文件开始阅读" } + } + } + } + + // 设置面板 + if show_settings() { + div { class: "modal-overlay", + div { class: "settings-panel", + h2 { "设置" } + button { class: "close-btn", onclick: move |_| show_settings.set(false), "×" } + div { class: "setting-item", + label { "主题" } + select { + value: "{config.theme.mode}", + option { value: "light", "浅色" } + option { value: "dark", "深色" } + } + } + } + } + } + } + } +} + +/// 文件列表组件 +#[component] +fn FileList(items: Vec, selected: Signal>) -> Element { + rsx! { + if items.is_empty() { + p { class: "empty", "暂无文件" } + } else { + for item in items { + FileItem { item: item, selected: selected } + } + } + } +} + +/// 单个文件项组件 +#[component] +fn FileItem(item: crate::library::LibraryItem, selected: Signal>) -> Element { + let is_selected = selected().as_ref().map(|s| s == &item.path).unwrap_or(false); + let item_path = item.path.clone(); + + rsx! { + div { + class: if is_selected { "file-item selected" } else { "file-item" }, + onclick: move |_| { + selected.set(Some(item_path.clone())); + }, + div { class: "file-icon", "{get_file_icon(&item.format)}" } + div { class: "file-info", + div { class: "file-title", "{item.title}" } + div { class: "file-meta", + span { class: "file-format", "{item.format}" } + span { "{item.format_file_size()}" } + } + } + } + } +} + +/// 获取过滤后的文件列表 +fn get_filtered_items(library: Signal, filter: FilterType, query: String) -> Vec { + let library_guard = library.read(); + let all_items: Vec = library_guard.get_all_items().into_iter().cloned().collect(); + + // 搜索过滤 + let filtered: Vec<_> = if query.is_empty() { + all_items + } else { + let q = query.to_lowercase(); + all_items.into_iter() + .filter(|item| item.title.to_lowercase().contains(&q) || item.path.to_lowercase().contains(&q)) + .collect() + }; + + // 类型过滤 + match filter { + FilterType::All => filtered, + FilterType::Recent => { + let recent: Vec<&LibraryItem> = library_guard.get_recent(20); + let recent_paths: Vec<&str> = recent.iter().map(|i| i.path.as_str()).collect(); + filtered.into_iter().filter(|item| recent_paths.contains(&item.path.as_str())).collect() + } + FilterType::Pdf => filtered.into_iter().filter(|item| item.format == "PDF").collect(), + FilterType::Epub => filtered.into_iter().filter(|item| item.format == "EPUB").collect(), + FilterType::Mobi => filtered.into_iter().filter(|item| item.format == "MOBI" || item.format == "AZW3").collect(), + FilterType::Text => filtered.into_iter().filter(|item| item.format == "TXT" || item.format == "Markdown").collect(), + } +} + +/// 获取文件图标 +fn get_file_icon(format: &str) -> &'static str { + match format { + "PDF" => "📕", + "EPUB" => "📙", + "MOBI" | "AZW3" => "📘", + "TXT" | "Markdown" => "📄", + _ => "📁", + } +} + +/// 获取应用 CSS +fn get_app_css() -> &'static str { + r#" + * { + box-sizing: border-box; + margin: 0; + padding: 0; + } + + body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + } + + .app-container { + display: flex; + height: 100vh; + overflow: hidden; + } + + .app-container.light { + background: #f5f5f5; + color: #333; + } + + .app-container.dark { + background: #1a1a2e; + color: #eee; + } + + .sidebar { + width: 280px; + background: #16213e; + color: #fff; + display: flex; + flex-direction: column; + border-right: 1px solid #0f3460; + } + + .app-container.light .sidebar { + background: #fff; + color: #333; + border-right: #ddd; + } + + .sidebar-header { + padding: 20px; + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid #0f3460; + } + + .settings-btn { + background: none; + border: none; + font-size: 20px; + cursor: pointer; + padding: 5px; + } + + .search-box { + padding: 10px 20px; + } + + .search-box input { + width: 100%; + padding: 8px 12px; + border: none; + border-radius: 6px; + background: #0f3460; + color: #fff; + } + + .app-container.light .search-box input { + background: #f0f0f0; + color: #333; + } + + .filter-buttons { + display: flex; + flex-wrap: wrap; + gap: 5px; + padding: 0 10px 10px; + } + + .filter-buttons button { + padding: 5px 10px; + border: none; + border-radius: 4px; + background: #0f3460; + color: #fff; + cursor: pointer; + font-size: 12px; + } + + .app-container.light .filter-buttons button { + background: #e0e0e0; + color: #333; + } + + .filter-buttons button.active { + background: #e94560; + } + + .file-list { + flex: 1; + overflow-y: auto; + padding: 10px; + } + + .file-item { + display: flex; + align-items: center; + padding: 10px; + margin-bottom: 5px; + border-radius: 6px; + cursor: pointer; + transition: background 0.2s; + } + + .file-item:hover { + background: #0f3460; + } + + .app-container.light .file-item:hover { + background: #f0f0f0; + } + + .file-item.selected { + background: #e94560; + } + + .file-icon { + font-size: 24px; + margin-right: 10px; + } + + .file-info { + flex: 1; + overflow: hidden; + } + + .file-title { + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .file-meta { + font-size: 12px; + opacity: 0.7; + display: flex; + gap: 10px; + } + + .main-content { + flex: 1; + padding: 40px; + display: flex; + align-items: center; + justify-content: center; + } + + .welcome, .reader { + text-align: center; + } + + .welcome h2, .reader h2 { + margin-bottom: 20px; + } + + .empty { + text-align: center; + opacity: 0.5; + padding: 20px; + } + + .modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0,0,0,0.5); + display: flex; + align-items: center; + justify-content: center; + } + + .settings-panel { + background: #16213e; + color: #fff; + padding: 30px; + border-radius: 12px; + min-width: 300px; + position: relative; + } + + .app-container.light .settings-panel { + background: #fff; + color: #333; + } + + .settings-panel h2 { + margin-bottom: 20px; + } + + .close-btn { + position: absolute; + top: 10px; + right: 10px; + background: none; + border: none; + color: #fff; + font-size: 24px; + cursor: pointer; + } + + .setting-item { + margin-bottom: 15px; + } + + .setting-item label { + display: block; + margin-bottom: 5px; + } + + .setting-item select { + width: 100%; + padding: 8px; + border-radius: 4px; + border: 1px solid #0f3460; + background: #0f3460; + color: #fff; + } + + .app-container.light .setting-item select { + background: #f0f0f0; + 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, mut opened_document: Signal>) { + 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(); + + // 添加到书库(如果尚未存在) + library.write().add_item(&path_str); + + // 打开文档 + opened_document.set(Some(path_str.clone())); + } +} + +/// 文档查看器组件 +#[component] +fn DocumentViewer(path: String) -> Element { + // 尝试加载文档 + let engine = DocumentEngine::new(); + + match engine.open(&path) { + Ok(doc) => { + rsx! { + div { class: "document-viewer", + div { class: "doc-header", + h3 { "{doc.title}" } + p { class: "doc-meta", + "格式:{:?} | 页数:{} | 大小:{}", + doc.format, + doc.metadata.page_count, + format_size(doc.metadata.file_size) + } + } + div { class: "doc-content", + // 这里后续可以渲染文档内容 + p { "文档已加载,阅读器渲染功能开发中..." } + p { "文件路径:{path}" } + } + } + } + } + Err(e) => { + rsx! { + div { class: "error-viewer", + h3 { "❌ 打开文件失败" } + p { "{e}" } + } + } + } + } +} + +/// 格式化文件大小 +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 启动 + dioxus::launch(App); +} \ No newline at end of file