initial commit
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I4a6b498153eccd5407510dd541b7f4816a6a6964
This commit is contained in:
commit
6a73d11c4b
124 changed files with 26877 additions and 0 deletions
190
crates/pinakes-tui/src/ui/mod.rs
Normal file
190
crates/pinakes-tui/src/ui/mod.rs
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
pub mod audit;
|
||||
pub mod collections;
|
||||
pub mod database;
|
||||
pub mod detail;
|
||||
pub mod duplicates;
|
||||
pub mod import;
|
||||
pub mod library;
|
||||
pub mod metadata_edit;
|
||||
pub mod queue;
|
||||
pub mod search;
|
||||
pub mod settings;
|
||||
pub mod statistics;
|
||||
pub mod tags;
|
||||
pub mod tasks;
|
||||
|
||||
use ratatui::Frame;
|
||||
use ratatui::layout::{Constraint, Direction, Layout, Rect};
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::{Block, Borders, Paragraph, Tabs};
|
||||
|
||||
use crate::app::{AppState, View};
|
||||
|
||||
/// Format a file size in bytes into a human-readable string.
|
||||
pub fn format_size(bytes: u64) -> String {
|
||||
if bytes < 1024 {
|
||||
format!("{bytes} B")
|
||||
} else if bytes < 1024 * 1024 {
|
||||
format!("{:.1} KB", bytes as f64 / 1024.0)
|
||||
} else if bytes < 1024 * 1024 * 1024 {
|
||||
format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0))
|
||||
} else {
|
||||
format!("{:.2} GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
|
||||
}
|
||||
}
|
||||
|
||||
/// Format duration in seconds into hh:mm:ss format.
|
||||
pub fn format_duration(secs: f64) -> String {
|
||||
let total = secs as u64;
|
||||
let h = total / 3600;
|
||||
let m = (total % 3600) / 60;
|
||||
let s = total % 60;
|
||||
if h > 0 {
|
||||
format!("{h:02}:{m:02}:{s:02}")
|
||||
} else {
|
||||
format!("{m:02}:{s:02}")
|
||||
}
|
||||
}
|
||||
|
||||
/// Trim a timestamp string to just the date portion (YYYY-MM-DD).
|
||||
pub fn format_date(timestamp: &str) -> &str {
|
||||
// Timestamps are typically "2024-01-15T10:30:00Z" or similar
|
||||
if timestamp.len() >= 10 {
|
||||
×tamp[..10]
|
||||
} else {
|
||||
timestamp
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a color based on media type string.
|
||||
pub fn media_type_color(media_type: &str) -> Color {
|
||||
match media_type {
|
||||
t if t.starts_with("audio") => Color::Green,
|
||||
t if t.starts_with("video") => Color::Magenta,
|
||||
t if t.starts_with("image") => Color::Yellow,
|
||||
t if t.starts_with("application/pdf") => Color::Red,
|
||||
t if t.starts_with("text") => Color::Cyan,
|
||||
_ => Color::White,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(f: &mut Frame, state: &AppState) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(3),
|
||||
Constraint::Min(0),
|
||||
Constraint::Length(1),
|
||||
])
|
||||
.split(f.area());
|
||||
|
||||
render_tabs(f, state, chunks[0]);
|
||||
|
||||
match state.current_view {
|
||||
View::Library => library::render(f, state, chunks[1]),
|
||||
View::Search => search::render(f, state, chunks[1]),
|
||||
View::Detail => detail::render(f, state, chunks[1]),
|
||||
View::Tags => tags::render(f, state, chunks[1]),
|
||||
View::Collections => collections::render(f, state, chunks[1]),
|
||||
View::Audit => audit::render(f, state, chunks[1]),
|
||||
View::Import => import::render(f, state, chunks[1]),
|
||||
View::Settings => settings::render(f, state, chunks[1]),
|
||||
View::Duplicates => duplicates::render(f, state, chunks[1]),
|
||||
View::Database => database::render(f, state, chunks[1]),
|
||||
View::MetadataEdit => metadata_edit::render(f, state, chunks[1]),
|
||||
View::Queue => queue::render(f, state, chunks[1]),
|
||||
View::Statistics => statistics::render(f, state, chunks[1]),
|
||||
View::Tasks => tasks::render(f, state, chunks[1]),
|
||||
}
|
||||
|
||||
render_status_bar(f, state, chunks[2]);
|
||||
}
|
||||
|
||||
fn render_tabs(f: &mut Frame, state: &AppState, area: Rect) {
|
||||
let titles: Vec<Line> = vec![
|
||||
"Library",
|
||||
"Search",
|
||||
"Tags",
|
||||
"Collections",
|
||||
"Audit",
|
||||
"Queue",
|
||||
"Stats",
|
||||
"Tasks",
|
||||
]
|
||||
.into_iter()
|
||||
.map(|t| Line::from(Span::styled(t, Style::default().fg(Color::White))))
|
||||
.collect();
|
||||
|
||||
let selected = match state.current_view {
|
||||
View::Library | View::Detail | View::Import | View::Settings | View::MetadataEdit => 0,
|
||||
View::Search => 1,
|
||||
View::Tags => 2,
|
||||
View::Collections => 3,
|
||||
View::Audit | View::Duplicates | View::Database => 4,
|
||||
View::Queue => 5,
|
||||
View::Statistics => 6,
|
||||
View::Tasks => 7,
|
||||
};
|
||||
|
||||
let tabs = Tabs::new(titles)
|
||||
.block(Block::default().borders(Borders::ALL).title(" Pinakes "))
|
||||
.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_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 paragraph = Paragraph::new(Line::from(Span::styled(
|
||||
status,
|
||||
Style::default().fg(Color::DarkGray),
|
||||
)));
|
||||
f.render_widget(paragraph, area);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue