From 2df605c86486ae00dbfb9ee631e40faf5a7f445e Mon Sep 17 00:00:00 2001 From: xiaji Date: Fri, 15 May 2026 11:27:22 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=86=E5=8C=96=E4=B9=A6=E7=AD=BE=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=EF=BC=9A=E6=AD=A3=E6=96=87=E6=8C=87=E7=A4=BA=E5=99=A8?= =?UTF-8?q?=E3=80=81=E4=BE=A7=E8=BE=B9=E6=A0=8F=E4=B9=A6=E7=AD=BE=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E3=80=81=E8=B7=B3=E8=BD=AC=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.rs | 48 +++++++++++++++++++++++++++++++++++- src/reader.rs | 68 +++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 108 insertions(+), 8 deletions(-) diff --git a/src/app.rs b/src/app.rs index c665421..4c607c8 100644 --- a/src/app.rs +++ b/src/app.rs @@ -275,9 +275,12 @@ BgType::Custom(ref path) if !path.is_empty() => { let theme_copy = self.settings.theme; let profile_names: Vec = self.settings.profiles.iter().map(|p| p.name.clone()).collect(); + let bookmarks = self.settings.bookmarks.get(&file_path).cloned().unwrap_or_default(); + let mut jump_to_bookmark: Option = None; + egui::CentralPanel::default().show(ctx, |ui| { let book = self.state.book.as_mut().unwrap(); - let action = crate::reader::reading_view( + let (action, jump) = crate::reader::reading_view( ui, book, &mut self.state.current_section, @@ -288,7 +291,9 @@ BgType::Custom(ref path) if !path.is_empty() => { bg_type.clone(), &file_path, &profile_names, + &bookmarks, ); + jump_to_bookmark = jump; if action.go_back { self.save_reading_position(); @@ -347,8 +352,49 @@ BgType::Custom(ref path) if !path.is_empty() => { if action.page_next { self.state.next_page(); } + + if action.toggle_bookmark { + let file_path = file_path.clone(); + let section = self.state.current_section; + let page = self.state.current_page; + let book = self.state.book.as_ref().unwrap(); + let section_title = book.sections.get(section) + .map(|s| s.title.clone()) + .unwrap_or_else(|| format!("第{}章", section + 1)); + + let bookmarks = self.settings.bookmarks.entry(file_path) + .or_insert_with(Vec::new); + + let existing_idx = bookmarks.iter() + .position(|b: &theme::Bookmark| b.section == section && b.page == page); + + if let Some(idx) = existing_idx { + bookmarks.remove(idx); + } else { + use std::time::{SystemTime, UNIX_EPOCH}; + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs() as i64) + .unwrap_or(0); + bookmarks.push(theme::Bookmark { + section, + page, + label: format!("{} - 第{}页", section_title, page + 1), + timestamp, + }); + } + } }); + if let Some(idx) = jump_to_bookmark { + if let Some(bookmarks) = self.settings.bookmarks.get(&file_path) { + if let Some(bm) = bookmarks.get(idx) { + self.state.current_section = bm.section; + self.state.current_page = bm.page; + } + } + } + // Sync style changes back to active profile (outside closure) if let Some(p) = self.settings.profiles.iter_mut() .find(|p| p.name == self.settings.active_profile) diff --git a/src/reader.rs b/src/reader.rs index 87c500b..a9c4176 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -42,7 +42,8 @@ pub fn reading_view( bg_type: BgType, _file_path: &str, profile_names: &[String], -) -> ReaderAction { + bookmarks: &[crate::theme::Bookmark], +) -> (ReaderAction, Option) { let mut action = ReaderAction { go_back: false, toggle_theme: false, @@ -52,21 +53,35 @@ pub fn reading_view( page_next: false, page_prev: false, }; + let mut jump_to_bookmark: Option = None; let panel_size = ui.available_size(); recalculate_pages(book, style.font_size, style.line_height(), panel_size.x, panel_size.y); let colors = theme::reader_colors(theme); + let has_bookmark = bookmarks.iter().any(|b| b.section == *current_section && b.page == *current_page); - // --- Sidebar (TOC) --- + // --- Sidebar (TOC + Bookmarks) --- + let mut sidebar_tab: usize = 0; if *sidebar_open { egui::SidePanel::left("toc_sidebar") .resizable(true) - .default_width(200.0) + .default_width(240.0) .show_inside(ui, |ui| { - ui.heading("目录"); + ui.horizontal(|ui| { + let toc_response = ui.selectable_label(sidebar_tab == 0, "📋 目录"); + let bm_response = ui.selectable_label(sidebar_tab == 1, + format!("🔖 书签 ({})", bookmarks.len()) + ); + if toc_response.clicked() { sidebar_tab = 0; } + if bm_response.clicked() { sidebar_tab = 1; } + }); ui.separator(); - render_toc(ui, &book.toc, current_section, current_page); + if sidebar_tab == 0 { + render_toc(ui, &book.toc, current_section, current_page); + } else { + render_bookmarks(ui, bookmarks, &mut jump_to_bookmark); + } }); } @@ -88,7 +103,9 @@ pub fn reading_view( if ui.button(theme_icon).on_hover_text(theme_hint).clicked() { action.toggle_theme = true; } - if ui.button("🔖").on_hover_text("添加/移除书签").clicked() { + let bookmark_icon = if has_bookmark { "🔴" } else { "🔖" }; + let bookmark_hint = if has_bookmark { "移除书签" } else { "添加书签" }; + if ui.button(bookmark_icon).on_hover_text(bookmark_hint).clicked() { action.toggle_bookmark = true; } if ui.button("A⁻").on_hover_text("缩小字体").clicked() { @@ -224,6 +241,18 @@ egui::ComboBox::from_id_salt("bg_type_selector") }, ); }); + + if has_bookmark { + let painter = ui.painter(); + let bookmark_pos = egui::pos2(rect.max.x - 30.0, rect.min.y + 10.0); + painter.text( + bookmark_pos, + egui::Align2::RIGHT_TOP, + "🔴", + egui::FontId::proportional(18.0), + egui::Color32::RED, + ); + } } } @@ -248,7 +277,7 @@ egui::ComboBox::from_id_salt("bg_type_selector") } }); - action + (action, jump_to_bookmark) } fn render_toc( @@ -282,6 +311,31 @@ fn render_toc( } } +fn render_bookmarks( + ui: &mut egui::Ui, + bookmarks: &[crate::theme::Bookmark], + jump_to_bookmark: &mut Option, +) { + if bookmarks.is_empty() { + ui.horizontal(|ui| { + ui.label("暂无书签"); + }); + ui.label("点击工具栏 🔖 按钮添加书签"); + return; + } + + for (idx, bm) in bookmarks.iter().enumerate() { + let label_text = egui::RichText::new(&bm.label).size(13.0); + if ui.add( + egui::Button::new(label_text) + .frame(false) + .wrap() + ).on_hover_text("点击跳转到该书签").clicked() { + *jump_to_bookmark = Some(idx); + } + } +} + pub fn calculate_pages(text: &str, chars_per_page: usize) -> Vec { let mut pages = Vec::new(); pages.push(0);