changxi 0.3.0

TUI EPUB Reader
use crate::app::App;
use crate::core::{Chapter, ContentElement, Reader};
use crate::ui::{Component, centered_rect, get_border_type};
use ratatui::style::{Color, Modifier, Style};
use ratatui::{
    Frame,
    layout::{Constraint, Direction, Layout, Rect},
    text::{Line, Span, Text},
    widgets::{Block, Borders, List, ListItem, Paragraph},
};
use ratatui_image::picker::Picker;

pub struct ChapterBrowser;

impl Component for ChapterBrowser {
    fn render(&self, f: &mut Frame, area: Rect, app: &mut App, _picker: &mut Picker) {
        let area = centered_rect(80, 90, area);

        let chunks = Layout::default()
            .direction(Direction::Horizontal)
            .constraints([Constraint::Percentage(40), Constraint::Percentage(60)])
            .split(area);

        let list_area = chunks[0];
        let preview_area = chunks[1];

        // Render chapter list
        self.render_chapter_list(f, list_area, app);

        // Render chapter preview
        self.render_chapter_preview(f, preview_area, app);
    }
}

impl ChapterBrowser {
    fn render_chapter_list(&self, f: &mut Frame, area: Rect, app: &mut App) {
        let mut list_items = Vec::new();
        let visible_indices = app.get_visible_chapter_indices();

        for &i in &visible_indices {
            let title = &app.chapter_titles[i];
            let level = app.chapter_levels.get(i).cloned().unwrap_or(0);
            let is_current = i == app.current_chapter;
            let is_expanded = app.expanded_chapters.get(&i).cloned().unwrap_or(false);
            let has_subchapters = i + 1 < app.chapter_levels.len()
                && app.chapter_levels[i + 1] > app.chapter_levels[i];

            let indent = "  ".repeat(level);
            let indicator = if has_subchapters {
                if is_expanded { "" } else { "" }
            } else {
                "  "
            };

            let prefix = if is_current { "* " } else { "  " };
            let content = format!("{}{}{}{}", indent, prefix, indicator, title);

            let mut text = Text::from(content);

            // Style the current chapter
            if is_current {
                text = text.style(Style::default().fg(Color::Green));
            }

            list_items.push(ListItem::new(text));
        }

        let border_type = get_border_type(&app.config);

        let list = List::new(list_items)
            .block(
                Block::default()
                    .borders(Borders::ALL)
                    .border_type(border_type)
                    .title(" Chapters "),
            )
            .highlight_style(Style::default().fg(Color::Yellow))
            .highlight_symbol("> ");

        f.render_stateful_widget(list, area, &mut app.chapter_list_state);
    }

    fn render_chapter_preview(&self, f: &mut Frame, area: Rect, app: &mut App) {
        // Get the current chapter for preview
        let visible_indices = app.get_visible_chapter_indices();
        let chapter_index = if let Some(selected) = app.chapter_list_state.selected() {
            visible_indices
                .get(selected)
                .cloned()
                .unwrap_or(app.current_chapter)
        } else {
            app.current_chapter
        };

        let preview_content = if chapter_index < app.chapter_titles.len() {
            match app.reader.get_chapter(chapter_index) {
                Ok(chapter) => self.format_chapter_preview(&chapter),
                Err(_) => Text::from("Failed to load chapter content."),
            }
        } else {
            Text::from("No chapter selected.")
        };

        let border_type = get_border_type(&app.config);

        let preview_paragraph = Paragraph::new(preview_content)
            .block(
                Block::default()
                    .borders(Borders::ALL)
                    .border_type(border_type)
                    .title(" Preview "),
            )
            .style(Style::default().fg(Color::White))
            .scroll((0, 0));

        f.render_widget(preview_paragraph, area);
    }

    fn format_chapter_preview(&self, chapter: &Chapter) -> Text<'static> {
        let mut lines = Vec::new();
        let mut line_count = 0;
        let max_lines = 15;
        let max_line_width = 80;

        let mut current_line_spans: Vec<Span> = Vec::new();
        let mut current_line_width = 0;

        for element in &chapter.elements {
            match element {
                ContentElement::Text(styled_texts) => {
                    for styled in styled_texts {
                        let mut style = Style::default();

                        if styled.style.bold {
                            style = style.add_modifier(Modifier::BOLD);
                        }
                        if styled.style.italic {
                            style = style.add_modifier(Modifier::ITALIC);
                        }
                        if styled.style.underline {
                            style = style.add_modifier(Modifier::UNDERLINED);
                        }
                        if styled.style.strikethrough {
                            style = style.add_modifier(Modifier::CROSSED_OUT);
                        }

                        // Process each character to handle newlines and wrapping
                        let mut current_word = String::new();

                        for ch in styled.text.chars() {
                            if ch == '\n' {
                                // Push the current word if any
                                if !current_word.is_empty() {
                                    if current_line_width + current_word.len() > max_line_width
                                        && !current_line_spans.is_empty()
                                    {
                                        // Wrap to new line
                                        lines.push(Line::from(current_line_spans));
                                        line_count += 1;
                                        current_line_spans = Vec::new();
                                        current_line_width = 0;
                                    }
                                    current_line_spans
                                        .push(Span::styled(current_word.clone(), style));
                                    current_line_width += current_word.len();
                                    current_word.clear();
                                }

                                // Force new line
                                if !current_line_spans.is_empty() {
                                    lines.push(Line::from(current_line_spans));
                                    current_line_spans = Vec::new();
                                    current_line_width = 0;
                                } else {
                                    lines.push(Line::from(""));
                                }
                                line_count += 1;

                                if line_count >= max_lines {
                                    break;
                                }
                            } else if ch.is_whitespace() {
                                // Push the current word
                                if !current_word.is_empty() {
                                    if current_line_width + current_word.len() > max_line_width
                                        && !current_line_spans.is_empty()
                                    {
                                        // Wrap to new line
                                        lines.push(Line::from(current_line_spans));
                                        line_count += 1;
                                        current_line_spans = Vec::new();
                                        current_line_width = 0;
                                    }
                                    current_line_spans
                                        .push(Span::styled(current_word.clone(), style));
                                    current_line_width += current_word.len();
                                    current_word.clear();
                                }

                                // Add the space
                                current_line_spans.push(Span::styled(" ".to_string(), style));
                                current_line_width += 1;
                            } else {
                                current_word.push(ch);
                            }
                        }

                        // Push any remaining word
                        if !current_word.is_empty() {
                            if current_line_width + current_word.len() > max_line_width
                                && !current_line_spans.is_empty()
                            {
                                lines.push(Line::from(current_line_spans));
                                line_count += 1;
                                current_line_spans = Vec::new();
                                current_line_width = 0;
                            }
                            current_line_spans.push(Span::styled(current_word.clone(), style));
                            current_line_width += current_word.len();
                        }

                        if line_count >= max_lines {
                            break;
                        }
                    }
                }
                ContentElement::Image(path) => {
                    if line_count < max_lines {
                        // Complete current line before adding image
                        if !current_line_spans.is_empty() {
                            lines.push(Line::from(current_line_spans));
                            current_line_spans = Vec::new();
                            current_line_width = 0;
                        }

                        lines.push(Line::from(Span::styled(
                            format!("[Image: {}]", path),
                            Style::default()
                                .fg(Color::DarkGray)
                                .add_modifier(Modifier::ITALIC),
                        )));
                        line_count += 1;
                    }
                }
                ContentElement::BlankLine => {
                    // Add a blank line in the preview
                    if line_count < max_lines {
                        // Complete current line if there's any content
                        if !current_line_spans.is_empty() {
                            lines.push(Line::from(current_line_spans));
                            current_line_spans = Vec::new();
                            current_line_width = 0;
                        }

                        // Add an empty line
                        lines.push(Line::from(""));
                        line_count += 1;
                    }
                }
            }

            if line_count >= max_lines {
                lines.push(Line::from(Span::styled(
                    "...",
                    Style::default().fg(Color::Yellow),
                )));
                break;
            }
        }

        // Add any remaining content
        if !current_line_spans.is_empty() && line_count < max_lines {
            lines.push(Line::from(current_line_spans));
        }

        if lines.is_empty() {
            Text::from("No preview content available.")
        } else {
            Text::from(lines)
        }
    }
}