pinakes/crates/pinakes-tui/src/ui/detail.rs
NotAShelf 66861b8a20
pinakes-tui: add book management view and api key authentication
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I20f205d9e06a93a89e8f4433ed6f80576a6a6964
2026-03-08 00:43:31 +03:00

311 lines
8.7 KiB
Rust

use ratatui::{
Frame,
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph},
};
use super::{format_date, format_duration, format_size, media_type_color};
use crate::app::AppState;
pub fn render(f: &mut Frame, state: &AppState, area: Rect) {
let Some(item) = &state.selected_media else {
let msg = Paragraph::new("No item selected")
.block(Block::default().borders(Borders::ALL).title(" Detail "));
f.render_widget(msg, area);
return;
};
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(0)])
.split(area);
let label_style = Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD);
let value_style = Style::default().fg(Color::White);
let dim_style = Style::default().fg(Color::DarkGray);
let pad = " ";
let label_width = 14;
let make_label = |name: &str| -> String { format!("{name:<label_width$}") };
let mut lines: Vec<Line> = Vec::new();
// Section: File Info
lines.push(Line::from(Span::styled(
"--- File Info ---",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)));
lines.push(Line::from(vec![
Span::raw(pad),
Span::styled(make_label("Name"), label_style),
Span::styled(&item.file_name, value_style),
]));
lines.push(Line::from(vec![
Span::raw(pad),
Span::styled(make_label("Path"), label_style),
Span::styled(&item.path, dim_style),
]));
let type_color = media_type_color(&item.media_type);
lines.push(Line::from(vec![
Span::raw(pad),
Span::styled(make_label("Type"), label_style),
Span::styled(&item.media_type, Style::default().fg(type_color)),
]));
lines.push(Line::from(vec![
Span::raw(pad),
Span::styled(make_label("Size"), label_style),
Span::styled(format_size(item.file_size), value_style),
]));
lines.push(Line::from(vec![
Span::raw(pad),
Span::styled(make_label("Hash"), label_style),
Span::styled(&item.content_hash, dim_style),
]));
if item.has_thumbnail {
lines.push(Line::from(vec![
Span::raw(pad),
Span::styled(make_label("Thumbnail"), label_style),
Span::styled("Yes", Style::default().fg(Color::Green)),
]));
}
lines.push(Line::default()); // blank line
// Section: Metadata
lines.push(Line::from(Span::styled(
"--- Metadata ---",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)));
lines.push(Line::from(vec![
Span::raw(pad),
Span::styled(make_label("Title"), label_style),
Span::styled(item.title.as_deref().unwrap_or("-"), value_style),
]));
lines.push(Line::from(vec![
Span::raw(pad),
Span::styled(make_label("Artist"), label_style),
Span::styled(item.artist.as_deref().unwrap_or("-"), value_style),
]));
lines.push(Line::from(vec![
Span::raw(pad),
Span::styled(make_label("Album"), label_style),
Span::styled(item.album.as_deref().unwrap_or("-"), value_style),
]));
lines.push(Line::from(vec![
Span::raw(pad),
Span::styled(make_label("Genre"), label_style),
Span::styled(item.genre.as_deref().unwrap_or("-"), value_style),
]));
lines.push(Line::from(vec![
Span::raw(pad),
Span::styled(make_label("Year"), label_style),
Span::styled(
item.year.map_or_else(|| "-".to_string(), |y| y.to_string()),
value_style,
),
]));
lines.push(Line::from(vec![
Span::raw(pad),
Span::styled(make_label("Duration"), label_style),
Span::styled(
item
.duration_secs
.map_or_else(|| "-".to_string(), format_duration),
value_style,
),
]));
// Description
if let Some(ref desc) = item.description
&& !desc.is_empty()
{
lines.push(Line::default());
lines.push(Line::from(vec![
Span::raw(pad),
Span::styled(make_label("Description"), label_style),
Span::styled(desc.as_str(), value_style),
]));
}
// Custom fields
if !item.custom_fields.is_empty() {
lines.push(Line::default());
lines.push(Line::from(Span::styled(
"--- Custom Fields ---",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)));
let mut fields: Vec<_> = item.custom_fields.iter().collect();
fields.sort_by_key(|(k, _)| k.as_str());
for (key, field) in fields {
lines.push(Line::from(vec![
Span::raw(pad),
Span::styled(format!("{key:<label_width$}"), label_style),
Span::styled(
format!("{} ({})", field.value, field.field_type),
value_style,
),
]));
}
}
// Tags section
if !state.tags.is_empty() {
lines.push(Line::default());
lines.push(Line::from(Span::styled(
"--- Tags ---",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)));
let tag_names: Vec<&str> =
state.tags.iter().map(|t| t.name.as_str()).collect();
lines.push(Line::from(vec![
Span::raw(pad),
Span::styled(tag_names.join(", "), Style::default().fg(Color::Green)),
]));
}
// Book metadata section
if let Some(ref book) = state.book_metadata {
lines.push(Line::default());
lines.push(Line::from(Span::styled(
"--- Book Metadata ---",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)));
if let Some(ref subtitle) = book.subtitle {
lines.push(Line::from(vec![
Span::raw(pad),
Span::styled(make_label("Subtitle"), label_style),
Span::styled(subtitle.as_str(), value_style),
]));
}
if !book.authors.is_empty() {
let authors: Vec<&str> =
book.authors.iter().map(|a| a.name.as_str()).collect();
lines.push(Line::from(vec![
Span::raw(pad),
Span::styled(make_label("Authors"), label_style),
Span::styled(authors.join(", "), value_style),
]));
}
if let Some(ref publisher) = book.publisher {
lines.push(Line::from(vec![
Span::raw(pad),
Span::styled(make_label("Publisher"), label_style),
Span::styled(publisher.as_str(), value_style),
]));
}
if let Some(isbn) = book.isbn13.as_ref().or(book.isbn.as_ref()) {
lines.push(Line::from(vec![
Span::raw(pad),
Span::styled(make_label("ISBN"), label_style),
Span::styled(isbn.as_str(), value_style),
]));
}
if let Some(ref language) = book.language {
lines.push(Line::from(vec![
Span::raw(pad),
Span::styled(make_label("Language"), label_style),
Span::styled(language.as_str(), value_style),
]));
}
if let Some(pages) = book.page_count {
lines.push(Line::from(vec![
Span::raw(pad),
Span::styled(make_label("Pages"), label_style),
Span::styled(pages.to_string(), value_style),
]));
}
if let Some(ref series) = book.series {
let series_display = book
.series_index
.map_or_else(|| series.clone(), |idx| format!("{series} #{idx}"));
lines.push(Line::from(vec![
Span::raw(pad),
Span::styled(make_label("Series"), label_style),
Span::styled(series_display, value_style),
]));
}
}
// Reading progress section
if let Some(ref progress) = state.reading_progress {
lines.push(Line::default());
lines.push(Line::from(Span::styled(
"--- Reading Progress ---",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)));
let page_display = progress.total_pages.map_or_else(
|| format!("Page {}", progress.current_page),
|total| format!("Page {} / {total}", progress.current_page),
);
lines.push(Line::from(vec![
Span::raw(pad),
Span::styled(make_label("Progress"), label_style),
Span::styled(page_display, value_style),
]));
lines.push(Line::from(vec![
Span::raw(pad),
Span::styled(make_label("Status"), label_style),
Span::styled(&progress.status, value_style),
]));
}
lines.push(Line::default());
// Section: Timestamps
lines.push(Line::from(Span::styled(
"--- Timestamps ---",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)));
lines.push(Line::from(vec![
Span::raw(pad),
Span::styled(make_label("Created"), label_style),
Span::styled(format_date(&item.created_at), dim_style),
]));
lines.push(Line::from(vec![
Span::raw(pad),
Span::styled(make_label("Updated"), label_style),
Span::styled(format_date(&item.updated_at), dim_style),
]));
let title = item.title.as_ref().map_or_else(
|| format!(" Detail: {} ", item.file_name),
|title_str| format!(" Detail: {title_str} "),
);
let detail = Paragraph::new(lines)
.block(Block::default().borders(Borders::ALL).title(title));
f.render_widget(detail, chunks[0]);
}