use crate::config::Config;
use crate::core::models::{ContentElement, SearchResult, StyledText, TextStyle};
use crate::core::search::{SearchType, search_bookmarks, search_chapters, search_content};
use crate::core::{EpubReader, Reader};
use crate::progress::{BookProgress, Bookmark, ProgressStore};
use crate::ui::count_lines;
use ratatui::widgets::ListState;
use ratatui_image::FilterType;
use ratatui_image::picker::Picker;
use ratatui_image::protocol::StatefulProtocol;
use ratatui_image::sliced::SlicedProtocol;
use std::cmp::Reverse;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(PartialEq, Clone, Copy)]
pub enum SortCriteria {
Recent,
Title,
Author,
}
#[derive(PartialEq, Clone, Copy)]
pub enum ViewMode {
Reading,
Cover,
ChapterBrowser,
BookmarkBrowser,
BookmarkRenaming,
Library,
Search,
Visual,
}
#[derive(PartialEq, Clone, Copy, Debug)]
pub enum SortOrder {
Ascending,
Descending,
}
pub struct App {
pub reader: EpubReader,
pub book_id: String,
pub book_path: String,
pub current_chapter: usize,
pub scroll: usize,
pub scroll_to_start: bool,
pub scroll_to_end: bool,
pub chapter_scrolls: HashMap<usize, usize>,
pub image_protocols: HashMap<String, StatefulProtocol>,
pub sliced_protocols: HashMap<String, SlicedProtocol>,
pub mode: ViewMode,
pub cover_protocol: Option<StatefulProtocol>,
pub chapter_list_state: ListState,
pub bookmark_list_state: ListState,
pub bookmarks: Vec<Bookmark>,
pub bookmark_rename_input: String,
pub bookmark_rename_cursor_position: usize,
pub chapter_titles: Vec<String>,
pub chapter_levels: Vec<usize>,
pub expanded_chapters: HashMap<usize, bool>,
pub library_state: ListState,
pub library_books: Vec<BookProgress>,
pub library_cover_protocol: Option<StatefulProtocol>,
pub sort_criteria: SortCriteria,
pub sort_order: SortOrder,
pub total_height: usize,
pub viewport_height: usize,
pub config: Config,
pub chapter_heights: Vec<usize>,
pub last_layout_width: usize,
pub profile_dir: Option<PathBuf>,
pub search_query: String,
pub search_results: Vec<SearchResult>,
pub search_list_state: ListState,
pub search_type: SearchType,
pub search_cursor_position: usize,
pub search_case_sensitive: bool,
pub visual_cursor: (usize, usize),
pub visual_anchor: Option<(usize, usize)>,
}
impl App {
pub fn new(
reader: EpubReader,
book_id: String,
book_path: String,
picker: &mut Picker,
progress: BookProgress,
config: Config,
profile_dir: Option<PathBuf>,
) -> Self {
let cover_protocol = if let Ok(Some(data)) = reader.cover_image() {
if let Ok(img) = image::load_from_memory(&data) {
Some(picker.new_resize_protocol(img))
} else {
None
}
} else {
None
};
let current_chapter = progress.current_chapter;
let scroll = progress
.chapter_scrolls
.get(¤t_chapter)
.cloned()
.unwrap_or(0);
let (scroll_to_end, scroll_to_start) = (false, false);
let mut chapter_titles = Vec::new();
let mut chapter_levels = Vec::new();
let chapters_info = reader.get_chapters_info();
let chapter_count = reader.chapter_count();
if !chapters_info.is_empty() {
for (i, info) in chapters_info.into_iter().enumerate() {
let title = if !info.title.is_empty() {
info.title.clone()
} else if let Ok(chapter) = reader.get_chapter(i) {
chapter.title.clone()
} else {
format!("Chapter {}", i + 1)
};
chapter_titles.push(title);
chapter_levels.push(info.level);
}
} else {
for i in 0..chapter_count {
if let Ok(chapter) = reader.get_chapter(i) {
chapter_titles.push(chapter.title.to_string());
} else {
chapter_titles.push(format!("Chapter {}", i + 1));
}
chapter_levels.push(0);
}
}
let mut list_state = ListState::default();
list_state.select(Some(current_chapter));
let mut bookmark_list_state = ListState::default();
if !progress.bookmarks.is_empty() {
bookmark_list_state.select(Some(0));
}
let progress_path = profile_dir.as_ref().map(|p| {
let mut p = p.clone();
p.push("progress.json");
p
});
let store = ProgressStore::load(progress_path);
let mut library_books: Vec<BookProgress> = store.books.values().cloned().collect();
library_books.sort_by_key(|b| Reverse(b.last_read));
let library_state = ListState::default();
App {
reader,
book_id,
book_path,
current_chapter,
scroll,
scroll_to_end,
scroll_to_start,
chapter_scrolls: progress.chapter_scrolls,
image_protocols: HashMap::new(),
sliced_protocols: HashMap::new(),
mode: if cover_protocol.is_some() && current_chapter == 0 && scroll == 0 {
ViewMode::Cover
} else {
ViewMode::Reading
},
cover_protocol,
chapter_list_state: list_state,
bookmark_list_state,
bookmarks: progress.bookmarks,
bookmark_rename_input: String::new(),
bookmark_rename_cursor_position: 0,
chapter_titles,
chapter_levels,
expanded_chapters: HashMap::new(),
library_state,
library_books,
library_cover_protocol: None,
sort_criteria: SortCriteria::Recent,
sort_order: SortOrder::Ascending,
total_height: 0,
viewport_height: 0,
config,
chapter_heights: vec![0; chapter_count],
last_layout_width: 0,
profile_dir,
search_query: String::new(),
search_results: Vec::new(),
search_list_state: ListState::default(),
search_type: SearchType::Local,
search_cursor_position: 0,
search_case_sensitive: false,
visual_cursor: (0, 0),
visual_anchor: None,
}
}
fn get_progress_path(&self) -> Option<std::path::PathBuf> {
self.profile_dir.as_ref().map(|p| {
let mut p = p.clone();
p.push("progress.json");
p
})
}
pub fn save_progress(&self) {
let mut store = ProgressStore::load(self.get_progress_path());
let mut chapter_scrolls = self.chapter_scrolls.clone();
chapter_scrolls.insert(self.current_chapter, self.scroll);
let last_read = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
store.last_book_path = Some(self.book_path.clone());
store.set_book(
self.book_id.clone(),
BookProgress {
current_chapter: self.current_chapter,
chapter_scrolls,
title: self.reader.title().to_string(),
author: self.reader.author().to_string(),
path: self.book_path.clone(),
last_read,
view_type: self.config.view_type.clone(),
bookmarks: self.bookmarks.clone(),
},
);
let _ = store.save();
}
pub fn update_layout(&mut self, width: usize) {
if self.last_layout_width == width {
if self.current_chapter < self.chapter_heights.len() {
self.total_height = self.chapter_heights[self.current_chapter];
}
return;
}
for i in 0..self.reader.chapter_count() {
if let Ok(chapter) = self.reader.get_chapter(i) {
let mut heights = Vec::new();
for element in &chapter.elements {
heights.push(count_lines(element, width));
}
self.chapter_heights[i] = heights.iter().sum();
}
}
self.last_layout_width = width;
if self.current_chapter < self.chapter_heights.len() {
self.total_height = self.chapter_heights[self.current_chapter];
}
}
pub fn get_global_scroll(&self) -> usize {
self.chapter_heights[..self.current_chapter]
.iter()
.sum::<usize>()
+ self.scroll
}
pub fn get_total_book_height(&self) -> usize {
self.chapter_heights.iter().sum()
}
pub fn set_global_scroll(&mut self, global: usize) {
let mut accumulated = 0;
for (i, &h) in self.chapter_heights.iter().enumerate() {
if global < accumulated + h {
self.current_chapter = i;
self.scroll = global - accumulated;
self.save_progress();
return;
}
accumulated += h;
}
self.current_chapter = self.chapter_heights.len().saturating_sub(1);
self.scroll = self.chapter_heights.last().cloned().unwrap_or(0);
self.save_progress();
}
pub fn jump_to_chapter(&mut self, index: usize) {
if index < self.reader.chapter_count() {
self.chapter_scrolls
.insert(self.current_chapter, self.scroll);
self.current_chapter = index;
self.scroll = 0;
self.mode = ViewMode::Reading;
self.chapter_list_state.select(Some(self.current_chapter));
self.save_progress();
}
}
pub fn next_page(&mut self, i: usize) {
if self.config.view_type == "continuous" {
let jump = self.viewport_height;
let current = self.get_global_scroll();
self.set_global_scroll(current + jump);
return;
}
let jump = if self.config.view_type == "book" && i == 2 {
2 * self.viewport_height
} else {
self.viewport_height
};
if self.scroll + jump < self.total_height {
self.scroll += jump;
self.save_progress();
} else {
self.next_chapter();
}
}
pub fn prev_page(&mut self, i: usize) {
if self.config.view_type == "continuous" {
let jump = self.viewport_height;
let current = self.get_global_scroll();
self.set_global_scroll(current.saturating_sub(jump));
return;
}
let jump = if self.config.view_type == "book" && i == 2 {
2 * self.viewport_height
} else {
self.viewport_height
};
if self.scroll >= jump {
self.scroll -= jump;
self.save_progress();
} else if self.scroll > 0 {
self.scroll = 0;
self.save_progress();
} else {
self.previous_chapter_end();
}
}
pub fn previous_chapter_end(&mut self) {
if self.current_chapter > 0 {
self.chapter_scrolls
.insert(self.current_chapter, self.scroll);
self.current_chapter -= 1;
if self.config.view_type == "continuous" {
if self.current_chapter < self.chapter_heights.len() {
self.scroll = self.chapter_heights[self.current_chapter]
.saturating_sub(self.viewport_height);
} else {
self.scroll = 0;
self.scroll_to_end = true;
}
} else {
self.scroll = 0;
self.scroll_to_end = true;
}
self.mode = ViewMode::Reading;
self.chapter_list_state.select(Some(self.current_chapter));
self.save_progress();
}
}
pub fn previous_chapter(&mut self) {
if self.current_chapter > 0 {
self.chapter_scrolls
.insert(self.current_chapter, self.scroll);
self.current_chapter -= 1;
if self.config.view_type == "continuous" {
self.scroll = 0;
} else {
self.scroll = self
.chapter_scrolls
.get(&self.current_chapter)
.cloned()
.unwrap_or(0);
}
self.mode = ViewMode::Reading;
self.chapter_list_state.select(Some(self.current_chapter));
self.save_progress();
}
}
pub fn next_chapter(&mut self) {
if self.current_chapter + 1 < self.reader.chapter_count() {
self.chapter_scrolls
.insert(self.current_chapter, self.scroll);
self.current_chapter += 1;
if self.config.view_type == "continuous" {
self.scroll = 0;
} else {
self.scroll = self
.chapter_scrolls
.get(&self.current_chapter)
.cloned()
.unwrap_or(0);
}
self.mode = ViewMode::Reading;
self.chapter_list_state.select(Some(self.current_chapter));
self.save_progress();
}
}
pub fn toggle_mode(&mut self) {
self.mode = match self.mode {
ViewMode::Reading => ViewMode::Cover,
ViewMode::Cover => ViewMode::Reading,
ViewMode::ChapterBrowser => ViewMode::Reading,
ViewMode::BookmarkBrowser => ViewMode::Reading,
ViewMode::BookmarkRenaming => ViewMode::Reading,
ViewMode::Library => ViewMode::Reading,
ViewMode::Search => ViewMode::Reading,
ViewMode::Visual => ViewMode::Reading,
};
}
pub fn enter_visual_mode(&mut self) {
self.mode = ViewMode::Visual;
self.visual_cursor = self.find_position_for_scroll();
self.visual_anchor = None;
}
pub fn find_position_for_scroll(&self) -> (usize, usize) {
if let Ok(chapter) = self.reader.get_chapter(self.current_chapter) {
let width = if self.last_layout_width > 0 {
self.last_layout_width
} else {
80
};
let mut current_y = 0;
for (i, element) in chapter.elements.iter().enumerate() {
let h = count_lines(element, width);
if current_y + h > self.scroll {
let mut char_idx = 0;
if let ContentElement::Text(spans) = element {
let text: String = spans.iter().map(|s| s.text.as_str()).collect();
let lines_to_skip = self.scroll.saturating_sub(current_y);
if lines_to_skip > 0 {
let mut skipped_lines = 0;
let mut current_pos = 0;
for line in text.lines() {
let line_len = line.chars().count();
let wrapped_lines =
(line_len as f64 / width as f64).ceil() as usize;
let wrapped_lines = wrapped_lines.max(1);
if skipped_lines + wrapped_lines > lines_to_skip {
let sub_line_skip = lines_to_skip - skipped_lines;
char_idx = current_pos + (sub_line_skip * width);
char_idx = char_idx.min(current_pos + line_len);
break;
}
skipped_lines += wrapped_lines;
current_pos += line_len + 1; }
}
}
return (i, char_idx);
}
current_y += h;
}
}
(0, 0)
}
pub fn move_visual_cursor(&mut self, de: i32, dc: i32) {
if let Ok(chapter) = self.reader.get_chapter(self.current_chapter) {
let (mut ei, mut ci) = self.visual_cursor;
if dc != 0 {
if dc > 0 {
for _ in 0..dc {
if let Some(element) = chapter.elements.get(ei) {
let count = match element {
ContentElement::Text(spans) => spans
.iter()
.map(|s| s.text.as_str())
.collect::<String>()
.chars()
.count(),
_ => 0,
};
if ci < count {
ci += 1;
} else if ei + 1 < chapter.elements.len() {
ei += 1;
ci = 0;
}
}
}
} else {
for _ in 0..(-dc) {
if ci > 0 {
ci -= 1;
} else if ei > 0 {
ei -= 1;
ci = match chapter.elements.get(ei) {
Some(ContentElement::Text(spans)) => spans
.iter()
.map(|s| s.text.as_str())
.collect::<String>()
.chars()
.count(),
_ => 0,
};
}
}
}
} else if de != 0 {
let next_ei = (ei as i32 + de)
.max(0)
.min(chapter.elements.len() as i32 - 1) as usize;
if next_ei != ei {
ei = next_ei;
let count = match chapter.elements.get(ei) {
Some(ContentElement::Text(spans)) => spans
.iter()
.map(|s| s.text.as_str())
.collect::<String>()
.chars()
.count(),
_ => 0,
};
ci = ci.min(count);
}
}
self.visual_cursor = (ei, ci);
self.ensure_visual_cursor_visible();
}
}
pub fn move_visual_word(&mut self, forward: bool) {
if let Ok(chapter) = self.reader.get_chapter(self.current_chapter) {
let (mut ei, mut ci) = self.visual_cursor;
if forward {
if let Some(element) = chapter.elements.get(ei) {
let text = match element {
ContentElement::Text(spans) => {
spans.iter().map(|s| s.text.as_str()).collect::<String>()
}
_ => String::new(),
};
let chars: Vec<char> = text.chars().collect();
let mut i = ci;
while i < chars.len() && !chars[i].is_whitespace() {
i += 1;
}
while i < chars.len() && chars[i].is_whitespace() {
i += 1;
}
if i < chars.len() {
ci = i;
} else if ei + 1 < chapter.elements.len() {
ei += 1;
ci = 0;
} else {
ci = chars.len();
}
}
} else {
if let Some(element) = chapter.elements.get(ei) {
let text = match element {
ContentElement::Text(spans) => {
spans.iter().map(|s| s.text.as_str()).collect::<String>()
}
_ => String::new(),
};
let chars: Vec<char> = text.chars().collect();
if ci == 0 {
if ei > 0 {
ei -= 1;
let prev_text = match chapter.elements.get(ei) {
Some(ContentElement::Text(spans)) => {
spans.iter().map(|s| s.text.as_str()).collect::<String>()
}
_ => String::new(),
};
ci = prev_text.chars().count();
}
} else {
let mut i = ci.saturating_sub(1);
while i > 0 && chars[i].is_whitespace() {
i -= 1;
}
while i > 0 && !chars[i - 1].is_whitespace() {
i -= 1;
}
ci = i;
}
}
}
self.visual_cursor = (ei, ci);
self.ensure_visual_cursor_visible();
}
}
pub fn get_line_for_visual_cursor(&self) -> usize {
self.get_line_for_pos(self.visual_cursor)
}
pub fn get_line_for_pos(&self, pos: (usize, usize)) -> usize {
if let Ok(chapter) = self.reader.get_chapter(self.current_chapter) {
let width = if self.last_layout_width > 0 {
self.last_layout_width
} else {
80
};
let (target_ei, target_ci) = pos;
let mut current_line = 0;
for (i, element) in chapter.elements.iter().enumerate() {
if i == target_ei {
if let ContentElement::Text(spans) = element {
let text: String = spans.iter().map(|s| s.text.as_str()).collect();
let text_before: String = text.chars().take(target_ci).collect();
let temp_element = ContentElement::Text(vec![StyledText {
text: text_before,
style: TextStyle::default(),
}]);
let lines_before = count_lines(&temp_element, width);
return current_line + lines_before.saturating_sub(1);
}
return current_line;
}
current_line += count_lines(element, width);
}
}
0
}
pub fn ensure_visual_cursor_visible(&mut self) {
let line = self.get_line_for_visual_cursor();
if line < self.scroll {
self.scroll = line;
} else if line >= self.scroll + self.viewport_height {
self.scroll = line.saturating_sub(self.viewport_height).saturating_add(1);
}
}
pub fn set_visual_anchor(&mut self) {
if self.visual_anchor.is_some() {
self.visual_anchor = None;
} else {
self.visual_anchor = Some(self.visual_cursor);
}
}
pub fn get_selected_text(&self) -> String {
if let Some(anchor) = self.visual_anchor
&& let Ok(chapter) = self.reader.get_chapter(self.current_chapter)
{
let (start, end) = if anchor < self.visual_cursor {
(anchor, self.visual_cursor)
} else {
(self.visual_cursor, anchor)
};
let mut selected = String::new();
for (i, element) in chapter.elements.iter().enumerate() {
if i < start.0 || i > end.0 {
continue;
}
if let ContentElement::Text(spans) = element {
let text: String = spans.iter().map(|s| s.text.as_str()).collect();
let s_idx = if i == start.0 { start.1 } else { 0 };
let e_idx = if i == end.0 {
end.1.min(text.chars().count())
} else {
text.chars().count()
};
if s_idx < e_idx {
selected.push_str(
&text
.chars()
.skip(s_idx)
.take(e_idx - s_idx)
.collect::<String>(),
);
}
if i < end.0 {
selected.push('\n');
}
}
}
selected
} else {
String::new()
}
}
pub fn open_search(&mut self) {
self.search_query.clear();
self.search_results.clear();
self.search_list_state.select(None);
self.search_cursor_position = 0;
self.mode = ViewMode::Search;
}
pub fn perform_search(&mut self) {
self.search_results.clear();
match self.search_type {
SearchType::Local => {
if let Ok(chapter) = self.reader.get_chapter(self.current_chapter) {
let content = chapter.content_as_text();
self.search_results = search_content(
&self.search_query,
self.current_chapter,
&chapter.title,
&content,
self.search_case_sensitive,
);
}
}
SearchType::Global => {
for i in 0..self.reader.chapter_count() {
if let Ok(chapter) = self.reader.get_chapter(i) {
let content = chapter.content_as_text();
let results = search_content(
&self.search_query,
i,
&chapter.title,
&content,
self.search_case_sensitive,
);
self.search_results.extend(results);
}
}
}
SearchType::Bookmark => {
self.search_results = search_bookmarks(
&self.search_query,
&self.bookmarks,
self.search_case_sensitive,
);
}
SearchType::Chapter => {
self.search_results = search_chapters(
&self.search_query,
&self.chapter_titles,
self.search_case_sensitive,
);
}
}
if !self.search_results.is_empty() {
self.search_list_state.select(Some(0));
} else {
self.search_list_state.select(None);
}
}
pub fn jump_to_search_result(&mut self) {
if let Some(selected) = self.search_list_state.selected()
&& let Some(result) = self.search_results.get(selected)
{
let chapter_index = result.chapter_index;
self.chapter_scrolls
.insert(self.current_chapter, self.scroll);
self.current_chapter = chapter_index;
if self.search_type == SearchType::Bookmark {
self.scroll = result.position;
} else if self.search_type == SearchType::Chapter {
self.scroll = 0;
} else {
self.scroll = self.find_scroll_for_position(chapter_index, result.position);
}
self.mode = ViewMode::Reading;
self.chapter_list_state.select(Some(self.current_chapter));
self.save_progress();
}
}
pub fn next_search_result(&mut self) {
if self.search_results.is_empty() {
return;
}
let i = self.search_list_state.selected().unwrap_or(0);
let new_index = if self.search_list_state.selected().is_some() {
(i + 1) % self.search_results.len()
} else {
0
};
self.search_list_state.select(Some(new_index));
self.jump_to_search_result();
}
pub fn prev_search_result(&mut self) {
if self.search_results.is_empty() {
return;
}
let i = self.search_list_state.selected().unwrap_or(0);
let new_index = if self.search_list_state.selected().is_some() {
(i + self.search_results.len() - 1) % self.search_results.len()
} else {
self.search_results.len() - 1
};
self.search_list_state.select(Some(new_index));
self.jump_to_search_result();
}
pub fn find_scroll_for_position(&self, chapter_index: usize, byte_index: usize) -> usize {
if let Ok(chapter) = self.reader.get_chapter(chapter_index) {
let width = if self.last_layout_width > 0 {
self.last_layout_width
} else {
80
};
let mut current_byte_count = 0;
let mut current_line_count = 0;
for element in &chapter.elements {
match element {
ContentElement::Text(spans) => {
let text: String = spans.iter().map(|s| s.text.as_str()).collect();
let element_byte_count = text.len();
if current_byte_count + element_byte_count > byte_index {
let relative_byte_index = byte_index - current_byte_count;
let mut slice_end = relative_byte_index;
while !text.is_char_boundary(slice_end) {
slice_end = slice_end.saturating_sub(1);
}
let text_before = text[..slice_end].to_string();
let temp_element =
ContentElement::Text(vec![crate::core::models::StyledText {
text: text_before,
style: crate::core::models::TextStyle::default(),
}]);
let lines_before = count_lines(&temp_element, width);
return current_line_count + lines_before.saturating_sub(1);
}
current_byte_count += element_byte_count;
current_line_count += count_lines(element, width);
}
ContentElement::Image(_) => {
current_line_count += 30;
}
ContentElement::BlankLine => {
current_line_count += 1;
}
}
}
}
0
}
pub fn open_chapter_browser(&mut self) {
self.chapter_list_state.select(Some(self.current_chapter));
self.mode = ViewMode::ChapterBrowser;
}
pub fn add_bookmark(&mut self) {
let chapter_title = self
.chapter_titles
.get(self.current_chapter)
.cloned()
.unwrap_or_default();
let (text_preview, scroll_pos) = if self.mode == ViewMode::Visual
&& self.visual_anchor.is_some()
{
let anchor = self.visual_anchor.unwrap();
let (start, _) = if anchor < self.visual_cursor {
(anchor, self.visual_cursor)
} else {
(self.visual_cursor, anchor)
};
let selected = self.get_selected_text();
let preview = if selected.chars().count() > 100 {
selected.chars().take(100).collect::<String>() + "..."
} else {
selected
};
(preview, self.get_line_for_pos(start))
} else {
let text_preview = if let Ok(chapter) = self.reader.get_chapter(self.current_chapter) {
let full_text = chapter.content_as_text();
full_text
.chars()
.take(100)
.collect::<String>()
.replace('\n', " ")
+ "..."
} else {
"No preview available".to_string()
};
(text_preview, self.scroll)
};
let bookmark = Bookmark {
name: chapter_title.clone(),
chapter_index: self.current_chapter,
scroll_position: scroll_pos,
chapter_title,
text_preview: text_preview.replace('\n', " "),
created_at: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
};
self.bookmarks.push(bookmark);
self.mode = ViewMode::Reading; self.save_progress();
}
pub fn remove_bookmark(&mut self) {
if let Some(selected) = self.bookmark_list_state.selected()
&& selected < self.bookmarks.len()
{
self.bookmarks.remove(selected);
if self.bookmarks.is_empty() {
self.bookmark_list_state.select(None);
} else {
let new_selection = selected.min(self.bookmarks.len() - 1);
self.bookmark_list_state.select(Some(new_selection));
}
self.save_progress();
}
}
pub fn jump_to_bookmark(&mut self) {
if let Some(selected) = self.bookmark_list_state.selected()
&& let Some(bookmark) = self.bookmarks.get(selected)
{
let chapter_index = bookmark.chapter_index;
let scroll = bookmark.scroll_position;
self.chapter_scrolls
.insert(self.current_chapter, self.scroll);
self.current_chapter = chapter_index;
self.scroll = scroll;
self.mode = ViewMode::Reading;
self.chapter_list_state.select(Some(self.current_chapter));
self.save_progress();
}
}
pub fn open_bookmark_browser(&mut self) {
if !self.bookmarks.is_empty() && self.bookmark_list_state.selected().is_none() {
self.bookmark_list_state.select(Some(0));
}
self.mode = ViewMode::BookmarkBrowser;
}
pub fn start_renaming_bookmark(&mut self) {
if let Some(selected) = self.bookmark_list_state.selected()
&& let Some(bookmark) = self.bookmarks.get(selected)
{
self.bookmark_rename_input = bookmark.name.clone();
self.bookmark_rename_cursor_position = self.bookmark_rename_input.len();
self.mode = ViewMode::BookmarkRenaming;
}
}
pub fn finish_renaming_bookmark(&mut self) {
if let Some(selected) = self.bookmark_list_state.selected()
&& selected < self.bookmarks.len()
{
self.bookmarks[selected].name = self.bookmark_rename_input.clone();
self.save_progress();
}
self.mode = ViewMode::BookmarkBrowser;
}
pub fn expand_chapter(&mut self) {
if let Some(selected) = self.chapter_list_state.selected() {
let visible_indices = self.get_visible_chapter_indices();
if let Some(&real_index) = visible_indices.get(selected) {
if real_index + 1 < self.chapter_levels.len()
&& self.chapter_levels[real_index + 1] > self.chapter_levels[real_index]
{
self.expanded_chapters.insert(real_index, true);
}
}
}
}
pub fn collapse_chapter(&mut self) {
if let Some(selected) = self.chapter_list_state.selected() {
let visible_indices = self.get_visible_chapter_indices();
if let Some(&real_index) = visible_indices.get(selected) {
if self
.expanded_chapters
.get(&real_index)
.cloned()
.unwrap_or(false)
{
self.expanded_chapters.insert(real_index, false);
} else {
let current_level = self.chapter_levels[real_index];
if current_level > 0 {
for i in (0..real_index).rev() {
if self.chapter_levels[i] < current_level {
self.expanded_chapters.insert(i, false);
let new_visible = self.get_visible_chapter_indices();
if let Some(new_pos) = new_visible.iter().position(|&idx| idx == i)
{
self.chapter_list_state.select(Some(new_pos));
}
break;
}
}
}
}
}
}
}
pub fn get_visible_chapter_indices(&self) -> Vec<usize> {
let mut visible = Vec::new();
let mut i = 0;
while i < self.chapter_titles.len() {
visible.push(i);
let level = self.chapter_levels[i];
let is_expanded = self.expanded_chapters.get(&i).cloned().unwrap_or(false);
if !is_expanded {
let mut j = i + 1;
while j < self.chapter_titles.len() && self.chapter_levels[j] > level {
j += 1;
}
i = j;
} else {
i += 1;
}
}
visible
}
pub fn open_library(&mut self, picker: &mut Picker) {
let store = ProgressStore::load(self.get_progress_path());
self.library_books = store
.books
.values()
.filter(|b| Path::new(&b.path).exists())
.cloned()
.collect();
if self.sort_order == SortOrder::Ascending {
self.sort_library_asc();
} else {
self.sort_library_desc();
}
let current_index = self
.library_books
.iter()
.position(|b| b.path == self.book_path);
if self.library_state.selected().is_none() {
self.library_state
.select(current_index.or(if self.library_books.is_empty() {
None
} else {
Some(0)
}));
}
self.mode = ViewMode::Library;
self.update_library_cover(picker);
}
pub fn sort_library_asc(&mut self) {
match self.sort_criteria {
SortCriteria::Recent => self.library_books.sort_by_key(|b| Reverse(b.last_read)),
SortCriteria::Title => self.library_books.sort_by(|a, b| a.title.cmp(&b.title)),
SortCriteria::Author => self.library_books.sort_by(|a, b| a.author.cmp(&b.author)),
}
}
pub fn sort_library_desc(&mut self) {
match self.sort_criteria {
SortCriteria::Recent => self.library_books.sort_by_key(|b| b.last_read),
SortCriteria::Title => self.library_books.sort_by(|b, a| a.title.cmp(&b.title)),
SortCriteria::Author => self.library_books.sort_by(|b, a| a.author.cmp(&b.author)),
}
}
pub fn toggle_sort(&mut self, picker: &mut Picker) {
if self.sort_order == SortOrder::Ascending {
self.sort_order = SortOrder::Descending;
self.sort_library_desc();
} else {
self.sort_order = SortOrder::Ascending;
self.sort_library_asc();
}
self.library_state.select(Some(0));
self.update_library_cover(picker);
}
pub fn delete_book(&mut self, i: usize, store: &mut ProgressStore) {
if i < self.library_books.len() {
let book = &self.library_books[i];
store.remove_book(&book.title, &book.author);
self.library_books.remove(i);
let _ = store.save();
}
}
pub fn next_sort_criteria(&mut self, picker: &mut Picker) {
self.sort_criteria = match self.sort_criteria {
SortCriteria::Recent => SortCriteria::Title,
SortCriteria::Title => SortCriteria::Author,
SortCriteria::Author => SortCriteria::Recent,
};
if self.sort_order == SortOrder::Ascending {
self.sort_library_asc();
} else {
self.sort_library_desc();
}
self.library_state.select(Some(0));
self.update_library_cover(picker);
}
pub fn update_library_cover(&mut self, picker: &mut Picker) {
if let Some(selected) = self.library_state.selected()
&& let Some(book) = self.library_books.get(selected)
&& let Ok(reader) = EpubReader::new(&book.path)
&& let Ok(Some(data)) = reader.cover_image()
&& let Ok(img) = image::load_from_memory(&data)
{
self.library_cover_protocol = Some(picker.new_resize_protocol(img));
return;
}
self.library_cover_protocol = None;
}
pub fn select_book(&mut self) -> Option<String> {
if let Some(selected) = self.library_state.selected()
&& let Some(book) = self.library_books.get(selected)
{
if book.path != self.book_path {
return Some(book.path.clone());
} else {
self.mode = ViewMode::Reading;
}
}
None
}
pub fn go_to_end(&mut self) {
self.scroll_to_end = true;
}
pub fn go_to_start(&mut self) {
self.scroll_to_start = true;
}
pub fn scroll_down(&mut self, i: usize) {
if self.config.view_type == "continuous" {
let current = self.get_global_scroll();
self.set_global_scroll(current + i);
return;
}
if self.scroll + i < self.total_height {
self.scroll += i;
} else if self.current_chapter + 1 < self.reader.chapter_count() {
self.next_chapter();
self.scroll = 0;
} else {
let max_scroll = self.total_height.saturating_sub(self.viewport_height);
self.scroll = self.scroll.saturating_add(i).min(max_scroll);
}
self.save_progress();
}
pub fn scroll_up(&mut self, i: usize) {
if self.config.view_type == "continuous" {
let current = self.get_global_scroll();
self.set_global_scroll(current.saturating_sub(i));
return;
}
if self.scroll >= i {
self.scroll -= i;
} else if self.current_chapter > 0 {
self.previous_chapter_end();
} else {
self.scroll = 0;
}
self.save_progress();
}
pub fn get_image_protocol(
&mut self,
path: &str,
picker: &mut Picker,
) -> Option<&mut StatefulProtocol> {
if !self.image_protocols.contains_key(path)
&& let Ok(data) = self.reader.get_image_by_path(path)
&& let Ok(img) = image::load_from_memory(&data)
{
self.image_protocols
.insert(path.to_string(), picker.new_resize_protocol(img));
}
self.image_protocols.get_mut(path)
}
pub fn get_sliced_protocol(
&mut self,
path: &str,
width: u16,
height: u16,
picker: &mut Picker,
) -> Option<&SlicedProtocol> {
let key = format!("{}:{}:{}", path, width, height);
if !self.sliced_protocols.contains_key(&key)
&& let Ok(data) = self.reader.get_image_by_path(path)
&& let Ok(img) = image::load_from_memory(&data)
{
let font_size = picker.font_size();
let font_width = if font_size.width == 0 {
10
} else {
font_size.width
};
let font_height = if font_size.height == 0 {
20
} else {
font_size.height
};
let target_px_width = width as u32 * font_width as u32;
let target_px_height = height as u32 * font_height as u32;
let img = if (img.width() > target_px_width || img.height() > target_px_height)
&& target_px_width > 0
&& target_px_height > 0
{
img.resize(target_px_width, target_px_height, FilterType::Lanczos3)
} else {
img
};
if let Ok(protocol) = SlicedProtocol::new(picker, img, None) {
self.sliced_protocols.insert(key.clone(), protocol);
}
}
self.sliced_protocols.get(&key)
}
}