pinakes-tui: add book management view and api key authentication
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I20f205d9e06a93a89e8f4433ed6f80576a6a6964
This commit is contained in:
parent
3d9f8933d2
commit
66861b8a20
18 changed files with 917 additions and 251 deletions
177
crates/pinakes-tui/src/ui/books.rs
Normal file
177
crates/pinakes-tui/src/ui/books.rs
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
use ratatui::{
|
||||
Frame,
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Borders, Row, Table, Tabs},
|
||||
};
|
||||
|
||||
use crate::app::{AppState, BooksSubView};
|
||||
|
||||
pub fn render(f: &mut Frame, state: &AppState, area: Rect) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(3), // Sub-tab headers
|
||||
Constraint::Min(0), // Content area
|
||||
])
|
||||
.split(area);
|
||||
|
||||
render_sub_tabs(f, state, chunks[0]);
|
||||
|
||||
match state.books_sub_view {
|
||||
BooksSubView::List => render_book_list(f, state, chunks[1]),
|
||||
BooksSubView::Series => render_series(f, state, chunks[1]),
|
||||
BooksSubView::Authors => render_authors(f, state, chunks[1]),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_sub_tabs(f: &mut Frame, state: &AppState, area: Rect) {
|
||||
let titles: Vec<Line> = vec!["List", "Series", "Authors"]
|
||||
.into_iter()
|
||||
.map(|t| Line::from(Span::styled(t, Style::default().fg(Color::White))))
|
||||
.collect();
|
||||
|
||||
let selected = match state.books_sub_view {
|
||||
BooksSubView::List => 0,
|
||||
BooksSubView::Series => 1,
|
||||
BooksSubView::Authors => 2,
|
||||
};
|
||||
|
||||
let tabs = Tabs::new(titles)
|
||||
.block(Block::default().borders(Borders::ALL).title(" Books "))
|
||||
.select(selected)
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
);
|
||||
|
||||
f.render_widget(tabs, area);
|
||||
}
|
||||
|
||||
fn render_book_list(f: &mut Frame, state: &AppState, area: Rect) {
|
||||
let header = Row::new(vec!["Title", "Author", "Format", "Pages"]).style(
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
);
|
||||
|
||||
let rows: Vec<Row> = state
|
||||
.books_list
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, media)| {
|
||||
let style = if Some(i) == state.books_selected {
|
||||
Style::default().fg(Color::Black).bg(Color::Cyan)
|
||||
} else {
|
||||
Style::default()
|
||||
};
|
||||
|
||||
let title = media
|
||||
.title
|
||||
.as_deref()
|
||||
.unwrap_or(&media.file_name)
|
||||
.to_string();
|
||||
|
||||
let author = media.artist.as_deref().unwrap_or("-").to_string();
|
||||
|
||||
// Extract format from media_type or file extension
|
||||
let format = media
|
||||
.file_name
|
||||
.rsplit('.')
|
||||
.next()
|
||||
.map_or_else(|| media.media_type.clone(), str::to_uppercase);
|
||||
|
||||
// Page count from custom fields if available
|
||||
let pages = media
|
||||
.custom_fields
|
||||
.get("page_count")
|
||||
.map_or_else(|| "-".to_string(), |f| f.value.clone());
|
||||
|
||||
Row::new(vec![title, author, format, pages]).style(style)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let title = format!(" Book List ({}) ", state.books_list.len());
|
||||
|
||||
let table = Table::new(rows, [
|
||||
Constraint::Percentage(40),
|
||||
Constraint::Percentage(30),
|
||||
Constraint::Percentage(15),
|
||||
Constraint::Percentage(15),
|
||||
])
|
||||
.header(header)
|
||||
.block(Block::default().borders(Borders::ALL).title(title));
|
||||
|
||||
f.render_widget(table, area);
|
||||
}
|
||||
|
||||
fn render_series(f: &mut Frame, state: &AppState, area: Rect) {
|
||||
let header = Row::new(vec!["Series Name", "Books"]).style(
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
);
|
||||
|
||||
let rows: Vec<Row> = state
|
||||
.books_series
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, series)| {
|
||||
let style = if Some(i) == state.books_selected {
|
||||
Style::default().fg(Color::Black).bg(Color::Cyan)
|
||||
} else {
|
||||
Style::default()
|
||||
};
|
||||
|
||||
Row::new(vec![series.name.clone(), series.count.to_string()]).style(style)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let title = format!(" Series ({}) ", state.books_series.len());
|
||||
|
||||
let table = Table::new(rows, [
|
||||
Constraint::Percentage(70),
|
||||
Constraint::Percentage(30),
|
||||
])
|
||||
.header(header)
|
||||
.block(Block::default().borders(Borders::ALL).title(title));
|
||||
|
||||
f.render_widget(table, area);
|
||||
}
|
||||
|
||||
fn render_authors(f: &mut Frame, state: &AppState, area: Rect) {
|
||||
let header = Row::new(vec!["Author Name", "Books"]).style(
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
);
|
||||
|
||||
let rows: Vec<Row> = state
|
||||
.books_authors
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, author)| {
|
||||
let style = if Some(i) == state.books_selected {
|
||||
Style::default().fg(Color::Black).bg(Color::Cyan)
|
||||
} else {
|
||||
Style::default()
|
||||
};
|
||||
|
||||
Row::new(vec![author.name.clone(), author.count.to_string()]).style(style)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let title = format!(" Authors ({}) ", state.books_authors.len());
|
||||
|
||||
let table = Table::new(rows, [
|
||||
Constraint::Percentage(70),
|
||||
Constraint::Percentage(30),
|
||||
])
|
||||
.header(header)
|
||||
.block(Block::default().borders(Borders::ALL).title(title));
|
||||
|
||||
f.render_widget(table, area);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue