From 594090db019f1633730d9f4b892704069c8459da Mon Sep 17 00:00:00 2001 From: Bastien Dejean Date: Fri, 13 Nov 2020 15:46:19 +0100 Subject: [PATCH] Add annotations and bookmarks pages --- css/annotations.css | 13 ++++ css/bookmarks.css | 13 ++++ src/app.rs | 25 ++++---- src/document/mod.rs | 63 ++++++++++++++----- src/emulator.rs | 7 ++- src/view/mod.rs | 6 +- src/view/reader/mod.rs | 135 ++++++++++++++++++----------------------- 7 files changed, 155 insertions(+), 107 deletions(-) create mode 100644 css/annotations.css create mode 100644 css/bookmarks.css diff --git a/css/annotations.css b/css/annotations.css new file mode 100644 index 0000000..bac4e24 --- /dev/null +++ b/css/annotations.css @@ -0,0 +1,13 @@ +ul { + margin: 0; + padding: 0; +} + +li { + list-style-type: none; + margin-top: 1.12em; +} + +a { + color: black; +} diff --git a/css/bookmarks.css b/css/bookmarks.css new file mode 100644 index 0000000..bac4e24 --- /dev/null +++ b/css/bookmarks.css @@ -0,0 +1,13 @@ +ul { + margin: 0; + padding: 0; +} + +li { + list-style-type: none; + margin-top: 1.12em; +} + +a { + color: black; +} diff --git a/src/app.rs b/src/app.rs index c7d05f3..2a3603a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -845,17 +845,6 @@ pub fn run() -> Result<(), Error> { handle_event(view.as_mut(), &Event::Invalid(info2), &tx, &mut bus, &mut rq, &mut context); } }, - Event::OpenToc(ref toc, chap_index) => { - let r = Reader::from_toc(context.fb.rect(), toc, chap_index, &tx, &mut context); - let mut next_view = Box::new(r) as Box; - transfer_notifications(view.as_mut(), next_view.as_mut(), &mut rq, &mut context); - history.push(HistoryItem { - view, - rotation: context.display.rotation, - monochrome: context.fb.monochrome(), - }); - view = next_view; - }, Event::Select(EntryId::About) => { let dialog = Dialog::new(ViewId::AboutDialog, None, @@ -867,7 +856,19 @@ pub fn run() -> Result<(), Error> { Event::Select(EntryId::SystemInfo) => { view.children_mut().retain(|child| !child.is::()); let html = sys_info_as_html(); - let r = Reader::from_html(context.fb.rect(), &html, &tx, &mut context); + let r = Reader::from_html(context.fb.rect(), &html, None, &tx, &mut context); + let mut next_view = Box::new(r) as Box; + transfer_notifications(view.as_mut(), next_view.as_mut(), &mut rq, &mut context); + history.push(HistoryItem { + view, + rotation: context.display.rotation, + monochrome: context.fb.monochrome(), + }); + view = next_view; + }, + Event::OpenHtml(ref html, ref link_uri) => { + view.children_mut().retain(|child| !child.is::()); + let r = Reader::from_html(context.fb.rect(), html, link_uri.as_deref(), &tx, &mut context); let mut next_view = Box::new(r) as Box; transfer_notifications(view.as_mut(), next_view.as_mut(), &mut rq, &mut context); history.push(HistoryItem { diff --git a/src/document/mod.rs b/src/document/mod.rs index f62accc..e258c57 100644 --- a/src/document/mod.rs +++ b/src/document/mod.rs @@ -10,6 +10,7 @@ use std::env; use std::process::Command; use std::path::Path; use std::ffi::OsStr; +use std::collections::BTreeSet; use anyhow::{Error, format_err}; use regex::Regex; use nix::sys::statvfs; @@ -25,7 +26,7 @@ use self::pdf::PdfOpener; use self::epub::EpubDocument; use self::html::HtmlDocument; use crate::geom::{Boundary, CycleDir}; -use crate::metadata::{TextAlign}; +use crate::metadata::{TextAlign, Annotation}; use crate::framebuffer::Pixmap; use crate::settings::INTERNAL_CARD_ROOT; use crate::device::CURRENT_DEVICE; @@ -274,6 +275,53 @@ pub fn toc_as_html_aux(toc: &[TocEntry], chap_index: usize, depth: usize, buf: & buf.push_str("\n"); } +pub fn annotations_as_html(annotations: &[Annotation], active_range: Option<(TextLocation, TextLocation)>) -> String { + let mut buf = "\n\t\n\t\tAnnotations\n\t\t\ + \n\t\ + \n\t\n".to_string(); + buf.push_str("\t\t
    \n"); + for annot in annotations { + let mut note = annot.note.replace('<', "<").replace('>', ">"); + let mut text = annot.text.replace('<', "<").replace('>', ">"); + let start = annot.selection[0]; + if active_range.map_or(false, |(first, last)| start >= first && start <= last) { + if !note.is_empty() { + note = format!("{}", note); + } + text = format!("{}", text); + } + if note.is_empty() { + buf.push_str(&format!("\t\t
  • {}
  • \n", start.location(), text)); + } else { + buf.push_str(&format!("\t\t
  • {} — {}
  • \n", start.location(), note, text)); + } + } + buf.push_str("\t\t
\n"); + buf.push_str("\t\n"); + buf +} + +pub fn bookmarks_as_html(bookmarks: &BTreeSet, index: usize, synthetic: bool) -> String { + let mut buf = "\n\t\n\t\tBookmarks\n\t\t\ + \n\t\ + \n\t\n".to_string(); + buf.push_str("\t\t
    \n"); + for bkm in bookmarks { + let mut text = if synthetic { + format!("{:.1}", *bkm as f64 / BYTES_PER_PAGE) + } else { + format!("{}", bkm + 1) + }; + if *bkm == index { + text = format!("{}", text); + } + buf.push_str(&format!("\t\t
  • {}
  • \n", bkm, text)); + } + buf.push_str("\t\t
\n"); + buf.push_str("\t\n"); + buf +} + #[inline] fn chapter(index: usize, toc: &[TocEntry]) -> Option<&TocEntry> { let mut chap = None; @@ -356,19 +404,6 @@ fn next_chapter<'a>(chap: Option<&TocEntry>, index: usize, toc: &'a [TocEntry]) None } -pub fn chapter_from_index(index: usize, toc: &[TocEntry]) -> Option<&TocEntry> { - for entry in toc { - if entry.index == index { - return Some(entry); - } - let result = chapter_from_index(index, &entry.children); - if result.is_some() { - return result; - } - } - None -} - pub fn chapter_from_uri<'a>(target_uri: &str, toc: &'a [TocEntry]) -> Option<&'a TocEntry> { for entry in toc { if let Location::Uri(ref uri) = entry.location { diff --git a/src/emulator.rs b/src/emulator.rs index 008baae..978da9b 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -354,8 +354,9 @@ fn main() -> Result<(), Error> { handle_event(view.as_mut(), &Event::Invalid(info2), &tx, &mut bus, &mut rq, &mut context); } }, - Event::OpenToc(ref toc, chap_index) => { - let r = Reader::from_toc(context.fb.rect(), toc, chap_index, &tx, &mut context); + Event::OpenHtml(ref html, ref link_uri) => { + view.children_mut().retain(|child| !child.is::()); + let r = Reader::from_html(context.fb.rect(), html, link_uri.as_deref(), &tx, &mut context); let mut next_view = Box::new(r) as Box; transfer_notifications(view.as_mut(), next_view.as_mut(), &mut rq, &mut context); history.push(view as Box); @@ -456,7 +457,7 @@ fn main() -> Result<(), Error> { Event::Select(EntryId::SystemInfo) => { view.children_mut().retain(|child| !child.is::()); let html = sys_info_as_html(); - let r = Reader::from_html(context.fb.rect(), &html, &tx, &mut context); + let r = Reader::from_html(context.fb.rect(), &html, None, &tx, &mut context); let mut next_view = Box::new(r) as Box; transfer_notifications(view.as_mut(), next_view.as_mut(), &mut rq, &mut context); history.push(view as Box); diff --git a/src/view/mod.rs b/src/view/mod.rs index 9839491..582136a 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -53,7 +53,7 @@ use std::fmt::{self, Debug}; use fxhash::FxHashMap; use downcast_rs::{Downcast, impl_downcast}; use crate::font::Fonts; -use crate::document::{Location, TextLocation, TocEntry}; +use crate::document::{Location, TextLocation}; use crate::settings::{ButtonScheme, FirstColumn, SecondColumn, RotationLock}; use crate::metadata::{Info, ZoomMode, SortMethod, TextAlign, SimpleStatus, PageScheme, Margin}; use crate::geom::{LinearDir, CycleDir, Rectangle, Boundary}; @@ -278,7 +278,7 @@ pub enum Event { Key(KeyKind), AddDocument(Box), Open(Box), - OpenToc(Vec, usize), + OpenHtml(String, Option), LoadPixmap(usize), Update(UpdateMode), Invalid(Box), @@ -516,6 +516,8 @@ pub enum EntryId { DefineSelection, SearchForSelection, AdjustSelection, + Annotations, + Bookmarks, RemoveAnnotation([TextLocation; 2]), EditAnnotationNote([TextLocation; 2]), RemoveAnnotationNote([TextLocation; 2]), diff --git a/src/view/reader/mod.rs b/src/view/reader/mod.rs index 5ed3c2e..9ee4a97 100644 --- a/src/view/reader/mod.rs +++ b/src/view/reader/mod.rs @@ -44,7 +44,7 @@ use crate::settings::{DEFAULT_FONT_FAMILY, DEFAULT_TEXT_ALIGN, DEFAULT_LINE_HEIG use crate::frontlight::LightLevels; use crate::gesture::GestureEvent; use crate::document::{Document, open, Location, TextLocation, BoundedText, Neighbors, BYTES_PER_PAGE}; -use crate::document::{TocEntry, SimpleTocEntry, TocLocation, toc_as_html, chapter_from_index}; +use crate::document::{TocEntry, SimpleTocEntry, TocLocation, toc_as_html, annotations_as_html, bookmarks_as_html}; use crate::document::html::HtmlDocument; use crate::metadata::{Info, FileInfo, ReaderInfo, Annotation, TextAlign, ZoomMode, PageScheme}; use crate::metadata::{Margin, CroppingMargins, make_query}; @@ -58,7 +58,6 @@ const HISTORY_SIZE: usize = 32; const RECT_DIST_JITTER: f32 = 24.0; const ANNOTATION_DRIFT: u8 = 0x44; const HIGHLIGHT_DRIFT: u8 = 0x22; -const TOC_SCHEME: &str = "toc:"; const MEM_SCHEME: &str = "mem:"; pub struct Reader { @@ -66,10 +65,10 @@ pub struct Reader { rect: Rectangle, children: Vec>, doc: Arc>>, - cache: BTreeMap, - text: FxHashMap>, - annotations: FxHashMap>, - chunks: Vec, + cache: BTreeMap, // Cached page pixmaps. + chunks: Vec, // Chunks of pages being rendered. + text: FxHashMap>, // Text of the current chunks. + annotations: FxHashMap>, // Annotations for the current chunks. focus: Option, search: Option, search_direction: LinearDir, @@ -349,34 +348,27 @@ impl Reader { }) } - pub fn from_toc(rect: Rectangle, toc: &[TocEntry], chap_index: usize, hub: &Hub, context: &mut Context) -> Reader { + pub fn from_html(rect: Rectangle, html: &str, link_uri: Option<&str>, hub: &Hub, context: &mut Context) -> Reader { let id = ID_FEEDER.next(); - let html = toc_as_html(toc, chap_index); - let info = Info { - title: "Table of Contents".to_string(), + let mut info = Info { file: FileInfo { - path: PathBuf::from(TOC_SCHEME), + path: PathBuf::from(MEM_SCHEME), kind: "html".to_string(), size: html.len() as u64, }, .. Default::default() }; - let mut doc = HtmlDocument::new_from_memory(&html); + let mut doc = HtmlDocument::new_from_memory(html); let (width, height) = context.display.dims; let font_size = context.settings.reader.font_size; doc.layout(width, height, font_size, CURRENT_DEVICE.dpi); let pages_count = doc.pages_count(); + info.title = doc.title().unwrap_or_default(); let mut current_page = 0; - - if let Some(chap) = chapter_from_index(chap_index, toc) { - let link_uri = match chap.location { - Location::Uri(ref uri) => format!("@{}", uri), - Location::Exact(offset) => format!("@{}", offset), - _ => "#".to_string(), - }; + if let Some(link_uri) = link_uri { let mut loc = Location::Exact(0); while let Some((links, offset)) = doc.links(loc) { if links.iter().any(|link| link.text == link_uri) { @@ -419,57 +411,6 @@ impl Reader { } } - pub fn from_html(rect: Rectangle, html: &str, hub: &Hub, context: &mut Context) -> Reader { - let id = ID_FEEDER.next(); - - let mut info = Info { - file: FileInfo { - path: PathBuf::from(MEM_SCHEME), - kind: "html".to_string(), - size: html.len() as u64, - }, - .. Default::default() - }; - - let mut doc = HtmlDocument::new_from_memory(html); - let (width, height) = context.display.dims; - let font_size = context.settings.reader.font_size; - doc.layout(width, height, font_size, CURRENT_DEVICE.dpi); - let pages_count = doc.pages_count(); - info.title = doc.title().unwrap_or_default(); - - hub.send(Event::Update(UpdateMode::Partial)).ok(); - - Reader { - id, - rect, - children: vec![], - doc: Arc::new(Mutex::new(Box::new(doc))), - cache: BTreeMap::new(), - text: FxHashMap::default(), - annotations: FxHashMap::default(), - chunks: Vec::new(), - focus: None, - search: None, - search_direction: LinearDir::Forward, - held_buttons: FxHashSet::default(), - selection: None, - target_annotation: None, - history: VecDeque::new(), - state: State::Idle, - info, - current_page: 0, - pages_count, - view_port: ViewPort::default(), - synthetic: false, - page_turns: 0, - contrast: Contrast::default(), - ephemeral: true, - reflowable: true, - finished: false, - } - } - fn load_pixmap(&mut self, location: usize) { if self.cache.contains_key(&location) { return; @@ -1752,7 +1693,7 @@ impl Reader { return; } - let entries = if self.reflowable { + let mut entries = if self.reflowable { if self.ephemeral { vec![EntryKind::Command("Save".to_string(), EntryId::Save)] } else { @@ -1773,6 +1714,14 @@ impl Reader { zoom_mode == ZoomMode::Custom(sf))])] }; + if self.info.reader.as_ref().map_or(false, |r| !r.annotations.is_empty()) { + entries.push(EntryKind::Command("Annotations".to_string(), EntryId::Annotations)); + } + + if self.info.reader.as_ref().map_or(false, |r| !r.bookmarks.is_empty()) { + entries.push(EntryKind::Command("Bookmarks".to_string(), EntryId::Bookmarks)); + } + if !entries.is_empty() { let title_menu = Menu::new(rect, ViewId::TitleMenu, MenuKind::DropDown, entries, context); rq.add(RenderData::new(title_menu.id(), *title_menu.rect(), UpdateMode::Gui)); @@ -3041,7 +2990,7 @@ impl View for Reader { }, DiagDir::SouthWest => { if self.search.is_none() { - if self.ephemeral && self.info.file.path == PathBuf::from(TOC_SCHEME) { + if self.ephemeral && self.info.file.path == PathBuf::from(MEM_SCHEME) { self.quit(context); hub.send(Event::Back).ok(); } else { @@ -3404,10 +3353,44 @@ impl View for Reader { if let Some(toc) = self.toc() .or_else(|| doc.toc()) .filter(|toc| !toc.is_empty()) { - let chap_index = doc.chapter(self.current_page, &toc) - .map(|chap| chap.index) - .unwrap_or(usize::MAX); - hub.send(Event::OpenToc(toc, chap_index)).ok(); + let chap = doc.chapter(self.current_page, &toc); + let chap_index = chap.map_or(usize::MAX, |chap| chap.index); + let html = toc_as_html(&toc, chap_index); + let link_uri = chap.and_then(|chap| { + match chap.location { + Location::Uri(ref uri) => Some(format!("@{}", uri)), + Location::Exact(offset) => Some(format!("@{}", offset)), + _ => None, + } + }); + hub.send(Event::OpenHtml(html, link_uri)).ok(); + } + true + }, + Event::Select(EntryId::Annotations) => { + self.toggle_bars(Some(false), hub, rq, context); + let mut starts = self.annotations.values().flatten() + .map(|annot| annot.selection[0]).collect::>(); + starts.sort(); + let active_range = starts.first().cloned().zip(starts.last().cloned()); + if let Some(mut annotations) = self.info.reader.as_ref().map(|r| &r.annotations).cloned() { + annotations.sort_by(|a, b| a.selection[0].cmp(&b.selection[0])); + let html = annotations_as_html(&annotations, active_range); + let link_uri = annotations.iter() + .filter(|annot| annot.selection[0].location() <= self.current_page) + .max_by_key(|annot| annot.selection[0]) + .map(|annot| format!("@{}", annot.selection[0].location())); + hub.send(Event::OpenHtml(html, link_uri)).ok(); + } + true + }, + Event::Select(EntryId::Bookmarks) => { + self.toggle_bars(Some(false), hub, rq, context); + if let Some(bookmarks) = self.info.reader.as_ref().map(|r| &r.bookmarks) { + let html = bookmarks_as_html(&bookmarks, self.current_page, self.synthetic); + let link_uri = bookmarks.range(..= self.current_page).next_back() + .map(|index| format!("@{}", index)); + hub.send(Event::OpenHtml(html, link_uri)).ok(); } true }, -- 2.51.2