//! 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).await; }); }, "📂 打开文件" } 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(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 { "阅读器功能开发中..." } } } else { div { class: "welcome", h2 { "欢迎使用 ReadFlow" } p { "从左侧选择一个文件,或点击「📂 打开文件」按钮" } p { "支持格式:PDF, EPUB, MOBI, TXT, Markdown, 代码文件" } } } } // 设置面板 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: #f7fafc; color: #1a202c; } .app-container.dark { background: #0f172a; color: #f1f5f9; } .sidebar { width: 280px; background: #1e293b; color: #ffffff; display: flex; flex-direction: column; border-right: 1px solid #475569; } .app-container.light .sidebar { background: #ffffff; color: #1a202c; border-right: #e2e8f0; } .sidebar-header { padding: 20px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #475569; } .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: #475569; color: #ffffff; } .app-container.light .search-box input { background: #f0f0f0; color: #1a202c; } .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: #475569; color: #ffffff; cursor: pointer; font-size: 12px; } .app-container.light .filter-buttons button { background: #e0e0e0; color: #1a202c; } .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: #475569; } .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: #1e293b; color: #ffffff; padding: 30px; border-radius: 12px; min-width: 300px; position: relative; } .app-container.light .settings-panel { background: #ffffff; color: #1a202c; } .settings-panel h2 { margin-bottom: 20px; } .close-btn { position: absolute; top: 10px; right: 10px; background: none; border: none; color: #ffffff; 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 #475569; background: #475569; color: #ffffff; } .app-container.light .setting-item select { background: #f0f0f0; color: #1a202c; border-color: #e2e8f0; } .header-actions { display: flex; gap: 10px; align-items: center; } .open-file-btn { padding: 8px 16px; border: none; border-radius: 6px; background: #e94560; color: #ffffff; 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 启动 dioxus::launch(App); }