fser 0.2.2

A dumbed-down TUI EPUB reader.
use std::{collections::hash_map::Entry, io::Cursor};

use crossterm::event::{KeyCode, KeyEvent};
use html2text::from_read;
use ratatui::{Frame, layout::Rect, style::Stylize, text::Line, widgets::Paragraph};

use crate::{
    bookmark::{Bookmark, save_bookmarks},
    fser::{Fser, FserState},
};

pub fn render_reader(fser: &mut Fser, frame: &mut Frame, area: Rect) {
    let Some(content) = &fser.current_content else {
        let error = Paragraph::new("Load failed")
            .red()
            .bold();
        frame.render_widget(error, area);
        return;
    };
    if fser.cached_lines.is_none() || fser.cached_width != area.width {
        let Ok(parsed) = from_read(Cursor::new(content.as_bytes()), area.width as usize) else {
            return;
        };
        let lines: Vec<String> = parsed.lines().map(|l| l.to_string()).collect();
        fser.cached_lines = Some(lines);
        fser.cached_width = area.width;
    }
    let Some(cur_lines) = &fser.cached_lines else {
        return;
    };
    let height = area.height as usize;
    let max_scroll = cur_lines.len().saturating_sub(height);
    if fser.scroll as usize > max_scroll {
        fser.scroll = max_scroll as u16;
    }
    let start = fser.scroll as usize;
    let end = (start + height).min(cur_lines.len());
    let visible: Vec<Line> = cur_lines[start..end]
        .iter()
        .map(|l| Line::from(l.as_str()))
        .collect();

    let paragraph = Paragraph::new(visible);
    frame.render_widget(paragraph, area);
}

pub fn handle_keys(fser: &mut Fser, key: &KeyEvent) {
    match key.code {
        KeyCode::Char('n') => fser.next_chapter(),
        KeyCode::Char('p') => fser.prev_chapter(),
        KeyCode::Char('j') | KeyCode::Down => fser.scroll = fser.scroll.saturating_add(1),
        KeyCode::Char('d') => fser.scroll = fser.scroll.saturating_add(5),
        KeyCode::Char('k') | KeyCode::Up => fser.scroll = fser.scroll.saturating_sub(1),
        KeyCode::Char('u') => fser.scroll = fser.scroll.saturating_sub(5),
        KeyCode::Char('m') => fser.state = FserState::Meta,
        KeyCode::Char('b') => add_or_remove_bookmark(fser),
        KeyCode::Char('B') => fser.state = FserState::Bookmark,
        _ => {}
    };
}

fn add_or_remove_bookmark(fser: &mut Fser) {
    let new_bookmark = Bookmark {
        page_index: fser.page_index,
        scroll: fser.scroll,
    };
    let path = fser.ebook_path.clone();
    let bookmark_added: bool;
    match fser.bookmarks.bookmarks.entry(path) {
        Entry::Occupied(mut entry) => {
            let bookmarks = entry.get_mut();
            let before = bookmarks.len();
            bookmarks.retain(|bm| {
                !(bm.page_index == new_bookmark.page_index && bm.scroll == new_bookmark.scroll)
            });
            bookmark_added = if bookmarks.len() == before {
                bookmarks.push(new_bookmark);
                true
            } else {
                false
            }
        }
        Entry::Vacant(entry) => {
            entry.insert(vec![new_bookmark]);
            bookmark_added = true;
        }
    }
    if let Err(err) = save_bookmarks(&fser.bookmarks) {
        fser.bookmark_just_added = (true, bookmark_added, Some(err));
    } else {
        fser.bookmark_just_added = (true, bookmark_added, None);
    }
}