treewide: fix various UI bugs; optimize crypto dependencies & format

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: If8fe8b38c1d9c4fecd40ff71f88d2ae06a6a6964
This commit is contained in:
raf 2026-02-10 12:56:05 +03:00
commit 3ccddce7fd
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
178 changed files with 58285 additions and 54241 deletions

View file

@ -13,178 +13,188 @@ 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 ratatui::{
Frame,
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
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))
}
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}")
}
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 {
&timestamp[..10]
} else {
timestamp
}
// Timestamps are typically "2024-01-15T10:30:00Z" or similar
if timestamp.len() >= 10 {
&timestamp[..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,
}
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());
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]);
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]),
}
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]);
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 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 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),
);
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);
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 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);
let paragraph = Paragraph::new(Line::from(Span::styled(
status,
Style::default().fg(Color::DarkGray),
)));
f.render_widget(paragraph, area);
}