nyaa 0.9.1

A tui tool for browsing and downloading torrents from nyaa.si
Documentation
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind};
use ratatui::{
    layout::{Constraint, Margin, Rect},
    style::{Color, Style, Stylize as _},
    text::{Line, Text},
    widgets::{Row, ScrollbarOrientation, StatefulWidget, Table},
    Frame,
};

use crate::{
    app::{Context, LoadType, Mode},
    style,
    theme::Theme,
    title,
};

use super::{border_block, VirtualStatefulTable, Widget};

#[derive(Clone)]
pub struct CatEntry {
    pub name: String,
    pub cfg: String,
    pub id: usize,
    pub icon: CatIcon,
}

#[derive(Clone)]
pub struct CatIcon {
    pub label: &'static str,
    pub color: fn(&Theme) -> Color,
}

impl Default for CatIcon {
    fn default() -> Self {
        CatIcon {
            label: "???",
            color: |t: &Theme| t.fg,
        }
    }
}

impl CatEntry {
    pub fn new(
        name: &str,
        cfg: &str,
        id: usize,
        label: &'static str,
        color: fn(&Theme) -> Color,
    ) -> Self {
        CatEntry {
            name: name.to_string(),
            cfg: cfg.to_string(),
            id,
            icon: CatIcon { label, color },
        }
    }
}

#[derive(Clone)]
pub struct CatStruct {
    pub name: String,
    pub entries: Vec<CatEntry>,
}

#[derive(Default)]
pub struct CategoryPopup {
    pub selected: usize,
    pub major: usize,
    pub minor: usize,
    pub table: VirtualStatefulTable,
}

impl CategoryPopup {
    fn next_tab(&mut self, max_cat: usize) {
        self.major = match self.major + 1 >= max_cat {
            true => 0,
            false => self.major + 1,
        };
        self.minor = 0;
        if self.table.state.offset() > self.major {
            *self.table.state.offset_mut() = self.major;
        }
    }

    fn prev_tab(&mut self, max_cat: usize) {
        self.major = match self.major == 0 {
            true => max_cat - 1,
            false => self.major - 1,
        };
        self.minor = 0;
        if self.table.state.offset() > self.major {
            *self.table.state.offset_mut() = self.major;
        }
    }
}

impl Widget for CategoryPopup {
    fn draw(&mut self, f: &mut Frame, ctx: &Context, area: Rect) {
        if let Some(cat) = ctx.src_info.cats.get(self.major) {
            let mut tbl: Vec<Row> = ctx
                .src_info
                .cats
                .iter()
                .enumerate()
                .map(|(i, e)| match i == self.major {
                    false => Row::new(Text::raw(format!("{}", e.name))),
                    true => Row::new(Text::raw(format!("{}", e.name)))
                        .style(style!(bg:ctx.theme.solid_bg, fg:ctx.theme.solid_fg)),
                })
                .collect();

            let cat_rows = cat.entries.iter().map(|e| {
                Row::new(vec![Line::from(vec![
                    match e.id == self.selected {
                        true => "",
                        false => "   ",
                    }
                    .into(),
                    e.icon.label.fg((e.icon.color)(&ctx.theme)),
                    " ".into(),
                    e.name.to_owned().into(),
                ])])
            });
            let num_items = cat.entries.len() + ctx.src_info.cats.len();
            self.table.scrollbar_state = self.table.scrollbar_state.content_length(num_items);

            tbl.splice(self.major + 1..self.major + 1, cat_rows);

            let center = super::centered_rect(33, 14, area);

            super::scroll_padding(
                self.table.selected().unwrap_or(0),
                center.height as usize,
                2,
                num_items,
                1,
                self.table.state.offset_mut(),
            );

            super::clear(center, f.buffer_mut(), ctx.theme.bg);
            let table = Table::new(tbl, [Constraint::Percentage(100)])
                .block(border_block(&ctx.theme, true).title(title!("Category")))
                .highlight_style(Style::default().bg(ctx.theme.hl_bg));
            StatefulWidget::render(table, center, f.buffer_mut(), &mut self.table.state);

            let sb = super::scrollbar(ctx, ScrollbarOrientation::VerticalRight);
            let sb_area = center.inner(Margin {
                vertical: 1,
                horizontal: 0,
            });
            StatefulWidget::render(sb, sb_area, f.buffer_mut(), &mut self.table.scrollbar_state);
        }
    }

    fn handle_event(&mut self, ctx: &mut Context, e: &Event) {
        if let Event::Key(KeyEvent {
            code,
            kind: KeyEventKind::Press,
            ..
        }) = e
        {
            match code {
                KeyCode::Enter => {
                    if let Some(cat) = ctx.src_info.cats.get(self.major) {
                        if let Some(item) = cat.entries.get(self.minor) {
                            self.selected = item.id;
                            ctx.notify(format!("Category \"{}\"", item.name));
                        }
                    }
                    ctx.mode = Mode::Loading(LoadType::Categorizing);
                }
                KeyCode::Esc | KeyCode::Char('c') | KeyCode::Char('q') => {
                    ctx.mode = Mode::Normal;
                }
                KeyCode::Char('j') | KeyCode::Down => {
                    if let Some(cat) = ctx.src_info.cats.get(self.major) {
                        self.minor = match self.minor + 1 >= cat.entries.len() {
                            true => {
                                self.next_tab(ctx.src_info.cats.len());
                                0
                            }
                            false => self.minor + 1,
                        };
                        self.table.select(self.major + self.minor + 1);
                    }
                }
                KeyCode::Char('k') | KeyCode::Up => {
                    if ctx.src_info.cats.get(self.major).is_some() {
                        self.minor = match self.minor < 1 {
                            true => {
                                self.prev_tab(ctx.src_info.cats.len());
                                match ctx.src_info.cats.get(self.major) {
                                    Some(cat) => cat.entries.len() - 1,
                                    None => 0,
                                }
                            }
                            false => self.minor - 1,
                        };
                        self.table.select(self.major + self.minor + 1);
                    }
                }
                KeyCode::Char('G') => {
                    if let Some(cat) = ctx.src_info.cats.get(self.major) {
                        self.minor = cat.entries.len() - 1;
                        self.table.select(self.major + self.minor + 1);
                    }
                }
                KeyCode::Char('g') => {
                    self.minor = 0;
                    self.table.select(self.major + self.minor + 1);
                }
                KeyCode::Tab | KeyCode::Char('J') => {
                    self.next_tab(ctx.src_info.cats.len());
                    self.table.select(self.major + self.minor + 1);
                }
                KeyCode::BackTab | KeyCode::Char('K') => {
                    self.prev_tab(ctx.src_info.cats.len());
                    self.table.select(self.major + self.minor + 1);
                }
                _ => {}
            }
        }
    }

    fn get_help() -> Option<Vec<(&'static str, &'static str)>> {
        Some(vec![
            ("Enter", "Confirm"),
            ("Esc, c, q", "Close"),
            ("j, ↓", "Down"),
            ("k, ↑", "Up"),
            ("g", "Top"),
            ("G", "Bottom"),
            ("Tab, J", "Next Tab"),
            ("S-Tab, K", "Prev Tab"),
        ])
    }
}