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
|
|
@ -47,17 +47,16 @@ pub fn render(f: &mut Frame, state: &AppState, area: Rect) {
|
|||
));
|
||||
|
||||
// Truncate media ID for display
|
||||
let media_display = entry
|
||||
.media_id
|
||||
.as_deref()
|
||||
.map(|id| {
|
||||
let media_display = entry.media_id.as_deref().map_or_else(
|
||||
|| "-".into(),
|
||||
|id| {
|
||||
if id.len() > 12 {
|
||||
format!("{}...", &id[..12])
|
||||
} else {
|
||||
id.to_string()
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| "-".into());
|
||||
},
|
||||
);
|
||||
|
||||
Row::new(vec![
|
||||
action_cell,
|
||||
|
|
|
|||
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);
|
||||
}
|
||||
|
|
@ -33,8 +33,7 @@ pub fn render(f: &mut Frame, state: &AppState, area: Rect) {
|
|||
col
|
||||
.filter_query
|
||||
.as_deref()
|
||||
.map(|q| format!("filter: {q}"))
|
||||
.unwrap_or_else(|| "-".to_string())
|
||||
.map_or_else(|| "-".to_string(), |q| format!("filter: {q}"))
|
||||
} else {
|
||||
"-".to_string()
|
||||
};
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ pub fn render(f: &mut Frame, state: &AppState, area: Rect) {
|
|||
lines.push(Line::from(vec![
|
||||
Span::raw(pad),
|
||||
Span::styled(format!("{key:<20}"), label_style),
|
||||
Span::styled(value.to_string(), value_style),
|
||||
Span::styled(value.clone(), value_style),
|
||||
]));
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -10,14 +10,11 @@ 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 item = match &state.selected_media {
|
||||
Some(item) => item,
|
||||
None => {
|
||||
let msg = Paragraph::new("No item selected")
|
||||
.block(Block::default().borders(Borders::ALL).title(" Detail "));
|
||||
f.render_widget(msg, area);
|
||||
return;
|
||||
},
|
||||
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()
|
||||
|
|
@ -122,10 +119,7 @@ pub fn render(f: &mut Frame, state: &AppState, area: Rect) {
|
|||
Span::raw(pad),
|
||||
Span::styled(make_label("Year"), label_style),
|
||||
Span::styled(
|
||||
item
|
||||
.year
|
||||
.map(|y| y.to_string())
|
||||
.unwrap_or_else(|| "-".to_string()),
|
||||
item.year.map_or_else(|| "-".to_string(), |y| y.to_string()),
|
||||
value_style,
|
||||
),
|
||||
]));
|
||||
|
|
@ -136,8 +130,7 @@ pub fn render(f: &mut Frame, state: &AppState, area: Rect) {
|
|||
Span::styled(
|
||||
item
|
||||
.duration_secs
|
||||
.map(format_duration)
|
||||
.unwrap_or_else(|| "-".to_string()),
|
||||
.map_or_else(|| "-".to_string(), format_duration),
|
||||
value_style,
|
||||
),
|
||||
]));
|
||||
|
|
@ -194,6 +187,96 @@ pub fn render(f: &mut Frame, state: &AppState, area: Rect) {
|
|||
]));
|
||||
}
|
||||
|
||||
// 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
|
||||
|
|
@ -216,11 +299,10 @@ pub fn render(f: &mut Frame, state: &AppState, area: Rect) {
|
|||
Span::styled(format_date(&item.updated_at), dim_style),
|
||||
]));
|
||||
|
||||
let title = if let Some(ref title_str) = item.title {
|
||||
format!(" Detail: {} ", title_str)
|
||||
} else {
|
||||
format!(" Detail: {} ", item.file_name)
|
||||
};
|
||||
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));
|
||||
|
|
|
|||
|
|
@ -39,8 +39,7 @@ pub fn render(f: &mut Frame, state: &AppState, area: Rect) {
|
|||
let line = format!(" {} - {}", item.file_name, item.path);
|
||||
let is_selected = state
|
||||
.duplicates_selected
|
||||
.map(|sel| sel == list_items.len())
|
||||
.unwrap_or(false);
|
||||
.is_some_and(|sel| sel == list_items.len());
|
||||
let style = if is_selected {
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
|
|
|
|||
|
|
@ -55,13 +55,9 @@ pub fn render(f: &mut Frame, state: &AppState, area: Rect) {
|
|||
|
||||
let duration = item
|
||||
.duration_secs
|
||||
.map(format_duration)
|
||||
.unwrap_or_else(|| "-".to_string());
|
||||
.map_or_else(|| "-".to_string(), format_duration);
|
||||
|
||||
let year = item
|
||||
.year
|
||||
.map(|y| y.to_string())
|
||||
.unwrap_or_else(|| "-".to_string());
|
||||
let year = item.year.map_or_else(|| "-".to_string(), |y| y.to_string());
|
||||
|
||||
Row::new(vec![
|
||||
Cell::from(Span::styled(marker, marker_style)),
|
||||
|
|
|
|||
|
|
@ -15,11 +15,10 @@ pub fn render(f: &mut Frame, state: &AppState, area: Rect) {
|
|||
.split(area);
|
||||
|
||||
// Header
|
||||
let title = if let Some(ref media) = state.selected_media {
|
||||
format!(" Edit: {} ", media.file_name)
|
||||
} else {
|
||||
" Edit Metadata ".to_string()
|
||||
};
|
||||
let title = state.selected_media.as_ref().map_or_else(
|
||||
|| " Edit Metadata ".to_string(),
|
||||
|media| format!(" Edit: {} ", media.file_name),
|
||||
);
|
||||
|
||||
let header = Paragraph::new(Line::from(Span::styled(
|
||||
&title,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
pub mod audit;
|
||||
pub mod books;
|
||||
pub mod collections;
|
||||
pub mod database;
|
||||
pub mod detail;
|
||||
|
|
@ -24,6 +25,10 @@ use ratatui::{
|
|||
use crate::app::{AppState, View};
|
||||
|
||||
/// Format a file size in bytes into a human-readable string.
|
||||
#[expect(
|
||||
clippy::cast_precision_loss,
|
||||
reason = "file sizes beyond 2^52 bytes are unlikely in practice"
|
||||
)]
|
||||
pub fn format_size(bytes: u64) -> String {
|
||||
if bytes < 1024 {
|
||||
format!("{bytes} B")
|
||||
|
|
@ -37,6 +42,11 @@ pub fn format_size(bytes: u64) -> String {
|
|||
}
|
||||
|
||||
/// Format duration in seconds into hh:mm:ss format.
|
||||
#[expect(
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_sign_loss,
|
||||
reason = "duration seconds are always non-negative and within u64 range"
|
||||
)]
|
||||
pub fn format_duration(secs: f64) -> String {
|
||||
let total = secs as u64;
|
||||
let h = total / 3600;
|
||||
|
|
@ -98,6 +108,7 @@ pub fn render(f: &mut Frame, state: &AppState) {
|
|||
View::Queue => queue::render(f, state, chunks[1]),
|
||||
View::Statistics => statistics::render(f, state, chunks[1]),
|
||||
View::Tasks => tasks::render(f, state, chunks[1]),
|
||||
View::Books => books::render(f, state, chunks[1]),
|
||||
}
|
||||
|
||||
render_status_bar(f, state, chunks[2]);
|
||||
|
|
@ -110,6 +121,7 @@ fn render_tabs(f: &mut Frame, state: &AppState, area: Rect) {
|
|||
"Tags",
|
||||
"Collections",
|
||||
"Audit",
|
||||
"Books",
|
||||
"Queue",
|
||||
"Stats",
|
||||
"Tasks",
|
||||
|
|
@ -128,9 +140,10 @@ fn render_tabs(f: &mut Frame, state: &AppState, area: Rect) {
|
|||
View::Tags => 2,
|
||||
View::Collections => 3,
|
||||
View::Audit | View::Duplicates | View::Database => 4,
|
||||
View::Queue => 5,
|
||||
View::Statistics => 6,
|
||||
View::Tasks => 7,
|
||||
View::Books => 5,
|
||||
View::Queue => 6,
|
||||
View::Statistics => 7,
|
||||
View::Tasks => 8,
|
||||
};
|
||||
|
||||
let tabs = Tabs::new(titles)
|
||||
|
|
@ -147,50 +160,60 @@ fn render_tabs(f: &mut Frame, state: &AppState, area: Rect) {
|
|||
}
|
||||
|
||||
fn render_status_bar(f: &mut Frame, state: &AppState, area: Rect) {
|
||||
let status = if let Some(ref msg) = state.status_message {
|
||||
msg.clone()
|
||||
} else {
|
||||
match state.current_view {
|
||||
View::Tags => {
|
||||
" q:Quit j/k:Nav Home/End:Top/Bot n:New d:Delete r:Refresh \
|
||||
Tab:Switch"
|
||||
.to_string()
|
||||
},
|
||||
View::Collections => {
|
||||
" q:Quit j/k:Nav Home/End:Top/Bot d:Delete r:Refresh Tab:Switch"
|
||||
.to_string()
|
||||
},
|
||||
View::Audit => {
|
||||
" q:Quit j/k:Nav Home/End:Top/Bot r:Refresh Tab:Switch".to_string()
|
||||
},
|
||||
View::Detail => {
|
||||
" q:Quit Esc:Back o:Open e:Edit +:Tag -:Untag r:Refresh ?:Help"
|
||||
.to_string()
|
||||
},
|
||||
View::Import => {
|
||||
" Enter:Import Esc:Cancel s:Scan libraries ?:Help".to_string()
|
||||
},
|
||||
View::Settings => " q:Quit Esc:Back ?:Help".to_string(),
|
||||
View::Duplicates => " q:Quit j/k:Nav r:Refresh Esc:Back".to_string(),
|
||||
View::Database => " q:Quit v:Vacuum r:Refresh Esc:Back".to_string(),
|
||||
View::MetadataEdit => {
|
||||
" Tab:Next field Enter:Save Esc:Cancel".to_string()
|
||||
},
|
||||
View::Queue => {
|
||||
" q:Quit j/k:Nav Enter:Play d:Remove N:Next P:Prev R:Repeat \
|
||||
S:Shuffle C:Clear"
|
||||
.to_string()
|
||||
},
|
||||
View::Statistics => " q:Quit r:Refresh Esc:Back ?:Help".to_string(),
|
||||
View::Tasks => {
|
||||
" q:Quit j/k:Nav Enter:Toggle R:Run Now r:Refresh Esc:Back"
|
||||
.to_string()
|
||||
},
|
||||
_ => " q:Quit /:Search i:Import o:Open t:Tags c:Coll a:Audit \
|
||||
D:Dupes B:DB Q:Queue X:Stats T:Tasks ?:Help"
|
||||
.to_string(),
|
||||
}
|
||||
};
|
||||
let status = state.status_message.as_ref().map_or_else(
|
||||
|| {
|
||||
match state.current_view {
|
||||
View::Tags => {
|
||||
" q:Quit j/k:Nav Home/End:Top/Bot n:New d:Delete r:Refresh \
|
||||
Tab:Switch"
|
||||
.to_string()
|
||||
},
|
||||
View::Collections => {
|
||||
" q:Quit j/k:Nav Home/End:Top/Bot d:Delete r:Refresh Tab:Switch"
|
||||
.to_string()
|
||||
},
|
||||
View::Audit => {
|
||||
" q:Quit j/k:Nav Home/End:Top/Bot r:Refresh Tab:Switch"
|
||||
.to_string()
|
||||
},
|
||||
View::Detail => {
|
||||
" q:Quit Esc:Back o:Open e:Edit p:Page +:Tag -:Untag \
|
||||
r:Refresh ?:Help"
|
||||
.to_string()
|
||||
},
|
||||
View::Import => {
|
||||
" Enter:Import Esc:Cancel s:Scan libraries ?:Help".to_string()
|
||||
},
|
||||
View::Settings => " q:Quit Esc:Back ?:Help".to_string(),
|
||||
View::Duplicates => " q:Quit j/k:Nav r:Refresh Esc:Back".to_string(),
|
||||
View::Database => " q:Quit v:Vacuum r:Refresh Esc:Back".to_string(),
|
||||
View::MetadataEdit => {
|
||||
" Tab:Next field Enter:Save Esc:Cancel".to_string()
|
||||
},
|
||||
View::Queue => {
|
||||
" q:Quit j/k:Nav Enter:Play d:Remove N:Next P:Prev R:Repeat \
|
||||
S:Shuffle C:Clear"
|
||||
.to_string()
|
||||
},
|
||||
View::Statistics => " q:Quit r:Refresh Esc:Back ?:Help".to_string(),
|
||||
View::Tasks => {
|
||||
" q:Quit j/k:Nav Enter:Toggle R:Run Now r:Refresh Esc:Back"
|
||||
.to_string()
|
||||
},
|
||||
View::Books => {
|
||||
" q:Quit j/k:Nav Home/End:Top/Bot Tab:Sub-view r:Refresh \
|
||||
Esc:Back"
|
||||
.to_string()
|
||||
},
|
||||
_ => {
|
||||
" q:Quit /:Search i:Import o:Open t:Tags c:Coll a:Audit \
|
||||
b:Books D:Dupes B:DB Q:Queue X:Stats T:Tasks ?:Help"
|
||||
.to_string()
|
||||
},
|
||||
}
|
||||
},
|
||||
String::clone,
|
||||
);
|
||||
|
||||
let paragraph = Paragraph::new(Line::from(Span::styled(
|
||||
status,
|
||||
|
|
|
|||
|
|
@ -29,11 +29,10 @@ pub fn render(f: &mut Frame, state: &AppState, area: Rect) {
|
|||
} else {
|
||||
&item.media_id
|
||||
};
|
||||
let text = if let Some(ref artist) = item.artist {
|
||||
format!("{prefix}{} - {} [{}]", item.title, artist, id_suffix)
|
||||
} else {
|
||||
format!("{prefix}{} [{}]", item.title, id_suffix)
|
||||
};
|
||||
let text = item.artist.as_ref().map_or_else(
|
||||
|| format!("{prefix}{} [{id_suffix}]", item.title),
|
||||
|artist| format!("{prefix}{} - {artist} [{id_suffix}]", item.title),
|
||||
);
|
||||
|
||||
let style = if is_selected {
|
||||
Style::default()
|
||||
|
|
|
|||
|
|
@ -73,21 +73,13 @@ pub fn render(f: &mut Frame, state: &AppState, area: Rect) {
|
|||
Line::from(vec![
|
||||
Span::styled(" Newest: ", Style::default().fg(Color::Gray)),
|
||||
Span::styled(
|
||||
stats
|
||||
.newest_item
|
||||
.as_deref()
|
||||
.map(super::format_date)
|
||||
.unwrap_or("-"),
|
||||
stats.newest_item.as_deref().map_or("-", super::format_date),
|
||||
Style::default().fg(Color::White),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::styled("Oldest: ", Style::default().fg(Color::Gray)),
|
||||
Span::styled(
|
||||
stats
|
||||
.oldest_item
|
||||
.as_deref()
|
||||
.map(super::format_date)
|
||||
.unwrap_or("-"),
|
||||
stats.oldest_item.as_deref().map_or("-", super::format_date),
|
||||
Style::default().fg(Color::White),
|
||||
),
|
||||
]),
|
||||
|
|
|
|||
|
|
@ -27,17 +27,15 @@ pub fn render(f: &mut Frame, state: &AppState, area: Rect) {
|
|||
};
|
||||
|
||||
// Resolve parent tag name from the tags list itself
|
||||
let parent_display = match &tag.parent_id {
|
||||
Some(pid) => {
|
||||
state
|
||||
.tags
|
||||
.iter()
|
||||
.find(|t| t.id == *pid)
|
||||
.map(|t| t.name.clone())
|
||||
.unwrap_or_else(|| pid.chars().take(8).collect::<String>() + "...")
|
||||
let parent_display = tag.parent_id.as_ref().map_or_else(
|
||||
|| "-".to_string(),
|
||||
|pid| {
|
||||
state.tags.iter().find(|t| t.id == *pid).map_or_else(
|
||||
|| pid.chars().take(8).collect::<String>() + "...",
|
||||
|t| t.name.clone(),
|
||||
)
|
||||
},
|
||||
None => "-".to_string(),
|
||||
};
|
||||
);
|
||||
|
||||
Row::new(vec![
|
||||
tag.name.clone(),
|
||||
|
|
|
|||
|
|
@ -28,16 +28,8 @@ pub fn render(f: &mut Frame, state: &AppState, area: Rect) {
|
|||
Color::DarkGray
|
||||
};
|
||||
|
||||
let last_run = task
|
||||
.last_run
|
||||
.as_deref()
|
||||
.map(super::format_date)
|
||||
.unwrap_or("-");
|
||||
let next_run = task
|
||||
.next_run
|
||||
.as_deref()
|
||||
.map(super::format_date)
|
||||
.unwrap_or("-");
|
||||
let last_run = task.last_run.as_deref().map_or("-", super::format_date);
|
||||
let next_run = task.next_run.as_deref().map_or("-", super::format_date);
|
||||
let status = task.last_status.as_deref().unwrap_or("-");
|
||||
// Show abbreviated task ID (first 8 chars)
|
||||
let task_id_short = if task.id.len() > 8 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue