diff --git a/doc/MANUAL.md b/doc/MANUAL.md index 6d90d1a..f742c77 100644 --- a/doc/MANUAL.md +++ b/doc/MANUAL.md @@ -27,6 +27,37 @@ This is a view of the books within the current directory. - Swipe west/east to go to the next/previous page. - Tap on a book entry to open it. +## Search bar + +The input's text is interpreted as a regular expression, and a book will match if any of its title, subtitle, author, series or file path matches. + +One can target specific fields or states using selectors. A selector is a sequence of characters that starts with a single quote and is followed by a letter which represent a field or a state. + +### Field selectors + +The text that will be searched for is given after the selector. + +- *t*: title. +- *u*: subtitle. +- *a*: author. +- *y*: year. +- *l*: language. +- *p*: publisher. +- *s*: series. +- *e*: edition. +- *v*: volume. +- *n*: number. + +### State selectors + +An exclamation mark can be prepended to a state selector's letter to invert its meaning. + +- *R*: reading. +- *N*: new. +- *F*: finished. +- *A*: has annotations. +- *B*: has bookmarks. + ## Bottom bar Tap and hold the next/previous page icon to go the last/first page. diff --git a/doc/TODO.md b/doc/TODO.md index 12a99f6..dc59b81 100644 --- a/doc/TODO.md +++ b/doc/TODO.md @@ -1,4 +1,3 @@ - ePUB renderer: RTL. - Metadata view. -- Complex/fuzzy search queries? - Applications: Notes, Terminal, Browser. diff --git a/src/library.rs b/src/library.rs index 9842988..60d5bed 100644 --- a/src/library.rs +++ b/src/library.rs @@ -2,14 +2,13 @@ use std::fs::{self, File}; use std::time::{SystemTime, Duration}; use std::path::{PathBuf, Path}; use std::collections::BTreeSet; -use regex::Regex; use walkdir::WalkDir; use indexmap::IndexMap; use fxhash::{FxHashMap, FxHashSet, FxBuildHasher}; use chrono::{Local, TimeZone}; use filetime::{FileTime, set_file_handle_times}; use anyhow::{Error, format_err}; -use crate::metadata::{Info, ReaderInfo, FileInfo, SimpleStatus, SortMethod}; +use crate::metadata::{Info, ReaderInfo, FileInfo, BookQuery, SimpleStatus, SortMethod}; use crate::metadata::{sort, sorter, extract_metadata_from_epub}; use crate::settings::{LibraryMode, ImportSettings}; use crate::document::file_kind; @@ -109,7 +108,7 @@ impl Library { } } - pub fn list>(&self, prefix: P, query: Option<&Regex>, skip_files: bool) -> (Vec, BTreeSet) { + pub fn list>(&self, prefix: P, query: Option<&BookQuery>, skip_files: bool) -> (Vec, BTreeSet) { let mut dirs = BTreeSet::new(); let mut files = Vec::new(); @@ -131,14 +130,7 @@ impl Library { if skip_files { continue; } - if query.map_or(true, |q| { - q.is_match(&info.title) || - q.is_match(&info.subtitle) || - q.is_match(&info.author) || - q.is_match(&info.series) || - info.file.path.to_str() - .map_or(false, |s| q.is_match(s)) - }) { + if query.map_or(true, |q| q.is_match(info)) { files.push(info.clone()); } } @@ -174,7 +166,7 @@ impl Library { let relat = path.strip_prefix(&self.home) .unwrap_or_else(|_| path.as_ref()); if skip_files || query.map_or(false, |q| { - relat.to_str().map_or(true, |s| !q.is_match(s)) + relat.to_str().map_or(true, |s| !q.is_simple_match(s)) }) { continue; } diff --git a/src/metadata.rs b/src/metadata.rs index 922e7ef..63215cd 100644 --- a/src/metadata.rs +++ b/src/metadata.rs @@ -369,20 +369,6 @@ impl Info { title } - #[inline] - pub fn is_match(&self, query: &Option) -> bool { - if let Some(ref query) = *query { - query.is_match(&self.title) || - query.is_match(&self.subtitle) || - query.is_match(&self.author) || - query.is_match(&self.series) || - self.categories.iter().any(|c| query.is_match(c)) || - self.file.path.to_str().map(|s| query.is_match(s)).unwrap_or(false) - } else { - true - } - } - // TODO: handle the following case: *Walter M. Miller Jr.*? // NOTE: e.g.: John Le Carré: the space between *Le* and *Carré* // is a non-breaking space @@ -440,6 +426,118 @@ pub fn make_query(text: &str) -> Option { .ok() } +#[derive(Debug, Clone, Default)] +pub struct BookQuery { + pub free: Option, + pub title: Option, + pub subtitle: Option, + pub author: Option, + pub year: Option, + pub language: Option, + pub publisher: Option, + pub series: Option, + pub edition: Option, + pub volume: Option, + pub number: Option, + pub reading: Option, + pub new: Option, + pub finished: Option, + pub annotations: Option, + pub bookmarks: Option, +} + +impl BookQuery { + pub fn new(text: &str) -> Option { + let mut buf = Vec::new(); + let mut query = BookQuery::default(); + for word in text.rsplit(' ') { + let mut chars = word.chars().peekable(); + match chars.next() { + Some('\'') => { + let mut invert = false; + if chars.peek() == Some(&'!') { + invert = true; + chars.next(); + } + match chars.next() { + Some('t') => { buf.reverse(); query.title = make_query(&buf.join(" ")); buf.clear(); }, + Some('u') => { buf.reverse(); query.subtitle = make_query(&buf.join(" ")); buf.clear(); }, + Some('a') => { buf.reverse(); query.author = make_query(&buf.join(" ")); buf.clear(); }, + Some('y') => { buf.reverse(); query.year = make_query(&buf.join(" ")); buf.clear(); }, + Some('l') => { buf.reverse(); query.language = make_query(&buf.join(" ")); buf.clear(); }, + Some('p') => { buf.reverse(); query.publisher = make_query(&buf.join(" ")); buf.clear(); }, + Some('s') => { buf.reverse(); query.series = make_query(&buf.join(" ")); buf.clear(); }, + Some('e') => { buf.reverse(); query.edition = make_query(&buf.join(" ")); buf.clear(); }, + Some('v') => { buf.reverse(); query.volume = make_query(&buf.join(" ")); buf.clear(); }, + Some('n') => { buf.reverse(); query.number = make_query(&buf.join(" ")); buf.clear(); }, + Some('R') => query.reading = Some(!invert), + Some('N') => query.new = Some(!invert), + Some('F') => query.finished = Some(!invert), + Some('A') => query.annotations = Some(!invert), + Some('B') => query.bookmarks = Some(!invert), + Some('\'') => buf.push(&word[1..]), + _ => (), + } + }, + _ => buf.push(word), + } + } + buf.reverse(); + query.free = make_query(&buf.join(" ")); + if query.free.is_none() && + query.title.is_none() && + query.subtitle.is_none() && + query.author.is_none() && + query.year.is_none() && + query.language.is_none() && + query.publisher.is_none() && + query.series.is_none() && + query.edition.is_none() && + query.volume.is_none() && + query.number.is_none() && + query.reading.is_none() && + query.new.is_none() && + query.finished.is_none() && + query.annotations.is_none() && + query.bookmarks.is_none() { + None + } else { + Some(query) + } + } + + #[inline] + pub fn is_match(&self, info: &Info) -> bool { + self.free.as_ref().map(|re| re.is_match(&info.title) || + re.is_match(&info.subtitle) || + re.is_match(&info.author) || + re.is_match(&info.series) || + info.file.path.to_str() + .map_or(false, |s| re.is_match(s))) != Some(false) && + self.title.as_ref().map(|re| re.is_match(&info.title)) != Some(false) && + self.subtitle.as_ref().map(|re| re.is_match(&info.subtitle)) != Some(false) && + self.author.as_ref().map(|re| re.is_match(&info.author)) != Some(false) && + self.year.as_ref().map(|re| re.is_match(&info.year)) != Some(false) && + self.language.as_ref().map(|re| re.is_match(&info.language)) != Some(false) && + self.publisher.as_ref().map(|re| re.is_match(&info.publisher)) != Some(false) && + self.series.as_ref().map(|re| re.is_match(&info.series)) != Some(false) && + self.edition.as_ref().map(|re| re.is_match(&info.edition)) != Some(false) && + self.volume.as_ref().map(|re| re.is_match(&info.volume)) != Some(false) && + self.number.as_ref().map(|re| re.is_match(&info.number)) != Some(false) && + self.reading.as_ref().map(|eq| info.simple_status().eq(&SimpleStatus::Reading) == *eq) != Some(false) && + self.new.as_ref().map(|eq| info.simple_status().eq(&SimpleStatus::New) == *eq) != Some(false) && + self.finished.as_ref().map(|eq| info.simple_status().eq(&SimpleStatus::Finished) == *eq) != Some(false) && + self.annotations.as_ref().map(|eq| info.reader.as_ref().map_or(false, |r| !r.annotations.is_empty()) == *eq) != Some(false) && + self.bookmarks.as_ref().map(|eq| info.reader.as_ref().map_or(false, |r| !r.bookmarks.is_empty()) == *eq) != Some(false) + } + + + #[inline] + pub fn is_simple_match(&self, text: &str) -> bool { + self.free.as_ref().map_or(true, |q| q.is_match(text)) + } +} + #[derive(Serialize, Deserialize, Debug, Copy, Clone, Eq, PartialEq)] #[serde(rename_all = "kebab-case")] pub enum SortMethod { diff --git a/src/view/home/mod.rs b/src/view/home/mod.rs index a5c3c62..2f776db 100644 --- a/src/view/home/mod.rs +++ b/src/view/home/mod.rs @@ -15,12 +15,11 @@ use std::process::{Command, Child, Stdio}; use std::io::{BufRead, BufReader}; use fxhash::FxHashMap; use rand_core::RngCore; -use regex::Regex; use serde_json::Value as JsonValue; use anyhow::{Error, format_err}; use crate::library::Library; use crate::framebuffer::{Framebuffer, UpdateMode}; -use crate::metadata::{Info, Metadata, SortMethod, SimpleStatus, sort, make_query}; +use crate::metadata::{Info, Metadata, SortMethod, BookQuery, SimpleStatus, sort}; use crate::view::{View, Event, Hub, Bus, RenderQueue, RenderData}; use crate::view::{Id, ID_FEEDER, ViewId, EntryId, EntryKind}; use crate::view::{SMALL_BAR_HEIGHT, BIG_BAR_HEIGHT, THICKNESS_MEDIUM}; @@ -60,7 +59,7 @@ pub struct Home { pages_count: usize, shelf_index: usize, focus: Option, - query: Option, + query: Option, sort_method: SortMethod, reverse_order: bool, visible_books: Metadata, @@ -626,6 +625,7 @@ impl Home { let thickness = scale_by_dpi(THICKNESS_MEDIUM, dpi) as i32; let delta_y = small_height; let search_visible: bool; + let mut has_keyboard = false; if let Some(index) = rlocate::(self) { if let Some(true) = enable { @@ -685,11 +685,14 @@ impl Home { } } - if rlocate::(self).is_none() { - self.toggle_keyboard(true, false, Some(ViewId::HomeSearchInput), hub, rq, context); - } + if self.query.is_none() { + if rlocate::(self).is_none() { + self.toggle_keyboard(true, false, Some(ViewId::HomeSearchInput), hub, rq, context); + has_keyboard = true; + } - hub.send(Event::Focus(Some(ViewId::HomeSearchInput))).ok(); + hub.send(Event::Focus(Some(ViewId::HomeSearchInput))).ok(); + } search_visible = true; } @@ -710,7 +713,8 @@ impl Home { rq.add(RenderData::new(self.child(self.shelf_index).id(), rect, UpdateMode::Partial)); // Render the views on top of the shelf. rect.min.y = rect.max.y; - rect.max.y = self.child(self.shelf_index+4).rect().max.y; + let end_index = self.shelf_index + if has_keyboard { 4 } else { 2 }; + rect.max.y = self.child(end_index).rect().max.y; rq.add(RenderData::expose(rect, UpdateMode::Partial)); } else { for i in self.shelf_index - 1 ..= self.shelf_index + 1 { @@ -831,6 +835,14 @@ impl Home { if let Some(parent) = path.parent() { entries.push(EntryKind::Command("Select Parent".to_string(), EntryId::SelectDirectory(context.library.home.join(parent)))); + } + + if !info.author.is_empty() { + entries.push(EntryKind::Command("Search Author".to_string(), + EntryId::SearchAuthor(info.author.clone()))); + } + + if !entries.is_empty() { entries.push(EntryKind::Separator); } @@ -1391,7 +1403,7 @@ impl View for Home { true }, Event::Submit(ViewId::HomeSearchInput, ref text) => { - self.query = make_query(text); + self.query = BookQuery::new(text); if self.query.is_some() { self.toggle_keyboard(false, false, None, hub, rq, context); // Render the search bar and its separator. @@ -1458,6 +1470,24 @@ impl View for Home { self.toggle_select_directory(path, hub, rq, context); true }, + Event::Select(EntryId::SearchAuthor(ref author)) => { + let text = format!("'a {}", author); + let query = BookQuery::new(&text); + if query.is_some() { + self.query = query; + self.toggle_search_bar(Some(true), false, hub, rq, context); + self.toggle_keyboard(false, false, None, hub, rq, context); + if let Some(search_bar) = self.children[self.shelf_index+2].downcast_mut::() { + search_bar.set_text(&text, rq, context); + } + // Render the search bar and its separator. + for i in self.shelf_index + 1 ..= self.shelf_index + 2 { + rq.add(RenderData::new(self.child(i).id(), *self.child(i).rect(), UpdateMode::Gui)); + } + self.refresh_visibles(true, true, rq, context); + } + true + }, Event::GoTo(location) => { self.go_to_page(location as usize, rq, context); true diff --git a/src/view/mod.rs b/src/view/mod.rs index e5a3c59..cbd534c 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -497,6 +497,7 @@ pub enum EntryId { SelectDirectory(PathBuf), ToggleSelectDirectory(PathBuf), SetStatus(PathBuf, SimpleStatus), + SearchAuthor(String), ToggleIntermissionImage(IntermKind, PathBuf), RemovePreset(usize), FirstColumn(FirstColumn),