trnovel 0.10.4

Terminal reader for novel
Documentation
use anyhow::anyhow;
use crossterm::event::{Event, KeyCode, KeyEventKind};
use parse_book_source::{BookInfo, BookListItem, Engine};
use ratatui::{
    layout::{Constraint, Margin},
    text::{Line, Span},
    widgets::{Paragraph, Wrap},
};
use ratatui_kit::prelude::*;

use crate::{
    book_source::BookSourceCache,
    components::{Loading, WarningModal},
    errors::Errors,
    hooks::{UseInitState, UseThemeConfig},
    novel::network_novel::NetworkNovel,
};

#[derive(Debug, Clone)]
pub enum BookDetailState {
    New {
        engine: Box<Engine>,
        book_list_item: BookListItem,
    },
    Cache {
        url: String,
    },
}

impl BookDetailState {
    pub fn new(book_list_item: BookListItem, engine: Engine) -> Self {
        Self::New {
            engine: Box::new(engine),
            book_list_item,
        }
    }

    pub fn from_cache(url: String) -> Self {
        Self::Cache { url }
    }
}

#[component]
pub fn BookDetail(mut hooks: Hooks) -> impl Into<AnyElement<'static>> {
    let book_detail_state = hooks.use_route_state::<BookDetailState>();
    let book_source_cache = *hooks.use_context::<State<Option<BookSourceCache>>>();

    let mut book_info = hooks.use_state(|| None::<BookInfo>);
    let size = hooks.use_previous_size();
    let theme = hooks.use_theme_config();
    let mut navigate = hooks.use_navigate();

    let (book_source_parser, loading, error) = hooks.use_init_state(async move {
        let mut novel = match &(*book_detail_state) {
            BookDetailState::New {
                engine,
                book_list_item,
            } => NetworkNovel::new(book_list_item.clone(), (**engine).clone()),
            BookDetailState::Cache { url } => {
                let book_source_cache = book_source_cache
                    .read()
                    .clone()
                    .ok_or(anyhow!("没有书源缓存"))?;
                NetworkNovel::from_url(url, &book_source_cache)?
            }
        };

        let res = novel
            .engine
            .book_info(&novel.book_list_item.book_url)
            .await?;

        novel.set_book_info(&res);

        book_info.set(Some(res));

        Ok::<NetworkNovel, Errors>(novel)
    });

    hooks.use_events(move |event| {
        if let Event::Key(key) = event
            && key.kind == KeyEventKind::Press
        {
            match key.code {
                KeyCode::Char('l') | KeyCode::Right | KeyCode::Enter => {
                    if let Some(novel) = book_source_parser.read().clone() {
                        navigate.push_with_state("/network-novel", novel);
                    }
                }
                _ => {}
            }
        }
    });

    let book_info = book_info.read().clone().unwrap_or_default();

    let title = vec![
        Span::from("名称:").style(theme.detail_info),
        Span::from(book_info.name.clone()).style(theme.basic.text),
    ];
    let author = vec![
        Span::from("作者:").style(theme.detail_info),
        Span::from(book_info.author.clone()).style(theme.basic.text),
    ];
    let kind = vec![
        Span::from("类型:").style(theme.detail_info),
        Span::from(book_info.kind.clone()).style(theme.basic.text),
    ];
    let word_count = vec![
        Span::from("字数:").style(theme.detail_info),
        Span::from(book_info.word_count.clone()).style(theme.basic.text),
    ];

    let last_chapter = vec![
        Span::from("最新章节:").style(theme.detail_info),
        Span::from(book_info.last_chapter.clone()).style(theme.basic.text),
    ];

    let text = vec![
        Line::from(title),
        Line::from(""),
        Line::from(author),
        Line::from(""),
        Line::from(kind),
        Line::from(""),
        Line::from(word_count),
        Line::from(""),
        Line::from(last_chapter),
    ];

    let paragraph = Paragraph::new(text).wrap(Wrap { trim: true });

    let intro = Paragraph::new(vec![
        Line::from("简介:").style(theme.detail_info),
        Line::from(book_info.intro),
    ])
    .wrap(Wrap { trim: true });

    element!(Border(
        top_title: Line::from("小说详情").centered().style(theme.basic.border_title),
        border_style: theme.basic.border,
    ){
        #(if loading.get(){
            element!(Loading(tip: "加载中...")).into_any()
        }else{
            element!(ScrollView(
                gap:1,
                scroll_bars:ScrollBars{
                    vertical_scrollbar_visibility: ScrollbarVisibility::Always,
                    ..Default::default()
                }
            ){
                View(height:Constraint::Length(paragraph.line_count(size.width.saturating_sub(4)) as u16), margin: Margin::new(1,0)){
                    Text(
                        text: paragraph,
                        style: theme.basic.text,
                    )
                }
                View(height:Constraint::Length(intro.line_count(size.width.saturating_sub(4)) as u16),margin: Margin::new(1,0)){
                    Text(
                        text: intro,
                        style: theme.basic.text,
                    )
                }
            }).into_any()
        })
        WarningModal(
            tip: format!("{:?}", error.read().as_ref()),
            is_error: error.read().is_some(),
            open: error.read().is_some(),
        )
    })
}