Big UI Overhaul

This commit is contained in:
2026-02-14 23:51:45 -06:00
parent 5839ca1c9d
commit c54c4e148d

View File

@@ -190,21 +190,21 @@ impl Default for App {
let (view_stack, config_status) = match &loaded { let (view_stack, config_status) = match &loaded {
Some(cfg) if cfg.access_token.is_some() => ( Some(cfg) if cfg.access_token.is_some() => (
vec![View::Categories], vec![View::Categories],
format!("auth: token present ({})", cfg.base_url), format!("Connected: ({})", cfg.base_url),
), ),
Some(cfg) => ( Some(cfg) => (
vec![View::Login], vec![View::Login],
format!("auth: needs login ({})", cfg.base_url), format!("Not Connected: ({})", cfg.base_url),
), ),
None => ( None => (
vec![View::Login], vec![View::Login],
"config: missing/invalid (need base_url)".to_string(), "Missing Configuration".to_string(),
), ),
}; };
Self { Self {
should_exit: false, should_exit: false,
status: "Press q to quit".to_string(), status: String::new(),
config_status, config_status,
config: loaded, config: loaded,
@@ -486,7 +486,7 @@ fn handle_key(app: &mut App, code: KeyCode) {
if !matches!(app.current_view(), View::Categories) { if !matches!(app.current_view(), View::Categories) {
app.view_stack = vec![View::Categories]; app.view_stack = vec![View::Categories];
app.selected = 0; app.selected = 0;
app.status = "Home".into(); app.status = "$".into();
return; return;
} else { } else {
app.should_exit = true; app.should_exit = true;
@@ -702,7 +702,7 @@ fn handle_key(app: &mut App, code: KeyCode) {
page_size: PAGE_SIZE, page_size: PAGE_SIZE,
loading: false, loading: false,
}); });
app.status = "Loaded".into(); app.status = "$".into();
} }
Err(e) => app.status = format!("Fetch failed: {e}"), Err(e) => app.status = format!("Fetch failed: {e}"),
} }
@@ -782,7 +782,7 @@ fn handle_key(app: &mut App, code: KeyCode) {
page_size: PAGE_SIZE, page_size: PAGE_SIZE,
loading: false, loading: false,
}); });
app.status = "Loaded".into(); app.status = "$".into();
} }
Err(e) => app.status = format!("Playlist load failed: {e}"), Err(e) => app.status = format!("Playlist load failed: {e}"),
} }
@@ -791,7 +791,7 @@ fn handle_key(app: &mut App, code: KeyCode) {
// Playables -> mpv // Playables -> mpv
// Playables: do nothing on Enter (use 'p' to play) // Playables: do nothing on Enter (use 'p' to play)
if ty == "Episode" || ty == "Audio" || ty == "Movie" { if ty == "Episode" || ty == "Audio" || ty == "Movie" {
app.status = "Press 'p' to play".into(); app.status = "Press 'p' to play, or 'P' for autoplay.".into();
return; return;
} }
@@ -806,7 +806,7 @@ fn handle_key(app: &mut App, code: KeyCode) {
KeyCode::Esc | KeyCode::Backspace => { KeyCode::Esc | KeyCode::Backspace => {
if app.view_stack.len() > 1 { if app.view_stack.len() > 1 {
app.view_stack.pop(); app.view_stack.pop();
app.status = "Went back".to_string(); app.status = "$".to_string();
app.selected = 0; app.selected = 0;
} }
} }
@@ -816,7 +816,8 @@ fn handle_key(app: &mut App, code: KeyCode) {
if app.items.is_empty() { return; } if app.items.is_empty() { return; }
if app.selected == 0 { app.selected = app.items.len() - 1; } if app.selected == 0 { app.selected = app.items.len() - 1; }
else { app.selected -= 1; } else { app.selected -= 1; }
app.status = format!("Selected: {}", app.items[app.selected]); //app.status = format!("Selected: {}", app.items[app.selected]);
app.status = "$".into();
} }
View::Library { items, selected, total, .. } => { View::Library { items, selected, total, .. } => {
if items.is_empty() { return; } if items.is_empty() { return; }
@@ -846,7 +847,8 @@ fn handle_key(app: &mut App, code: KeyCode) {
View::Categories => { View::Categories => {
if app.items.is_empty() { return; } if app.items.is_empty() { return; }
app.selected = (app.selected + 1) % app.items.len(); app.selected = (app.selected + 1) % app.items.len();
app.status = format!("Selected: {}", app.items[app.selected]); //app.status = format!("Selected: {}", app.items[app.selected]);
app.status = "$".into();
} }
View::Library { items, selected, total, .. } => { View::Library { items, selected, total, .. } => {
if items.is_empty() { return; } if items.is_empty() { return; }
@@ -1125,9 +1127,29 @@ fn ui(frame: &mut Frame, app: &App) {
let panes = layout(area); let panes = layout(area);
// Top // Top
//frame.render_widget(
// Paragraph::new("cj-jftui").block(Block::bordered()),
// panes.top,
//);
let top_block = Block::bordered();
frame.render_widget(top_block.clone(), panes.top);
let top_inner = top_block.inner(panes.top);
let [top_l, top_r] = Layout::horizontal([Constraint::Fill(1), Constraint::Length(40)])
.areas(top_inner);
frame.render_widget(Paragraph::new("cj-jftui"), top_l);
let server = app
.config
.as_ref()
.map(|c| c.base_url.clone())
.unwrap_or_else(|| "-".into());
frame.render_widget( frame.render_widget(
Paragraph::new("cj-jftui").block(Block::bordered()), Paragraph::new(server)
panes.top, .alignment(ratatui::layout::Alignment::Right),
top_r,
); );
// Main // Main
@@ -1163,44 +1185,60 @@ fn ui(frame: &mut Frame, app: &App) {
} }
// Bottom Left // Bottom Left
let status_text = if let Some(s) = &app.search { // ---- Bottom Left: prompt / mpv ----
let left_text = if let Some(s) = &app.search {
// search takes over temporarily
let scope = match &s.scope { let scope = match &s.scope {
SearchScope::All => "All".to_string(), SearchScope::All => "All".to_string(),
SearchScope::Library { title, .. } => title.clone(), SearchScope::Library { title, .. } => title.clone(),
}; };
let mode = s.mode.label(); let mode = s.mode.label();
if s.input.is_empty() { if s.input.is_empty() {
format!("/ ({scope}) [{mode}]") format!("/ ({scope}) [{mode}]")
} else { } else {
format!("/ ({scope}) [{mode}] {}", s.input) format!("/ ({scope}) [{mode}] {}", s.input)
} }
} else if app.mpv.is_some() {
// mpv persists while browsing
if app.debug_mpv {
fmt_mpv_state(&app.mpv_state)
} else if let Some(p) = app.mpv_state.percent {
if p.is_finite() { format!("mpv: {p:5.1}%") } else { "mpv: playing".into() }
} else {
"mpv: playing".into()
}
} else { } else {
app.status.clone() "$".into()
}; };
frame.render_widget( frame.render_widget(
Paragraph::new(status_text) Paragraph::new(left_text)
.block(Block::bordered().title("Status").merge_borders(MergeStrategy::Exact)), // no title
.block(Block::bordered().merge_borders(MergeStrategy::Exact)),
panes.left_output, panes.left_output,
); );
// Bottom Right // ---- Bottom Right: always-on status/help ----
let info = match app.current_view() { let connected = if app.config.as_ref().and_then(|c| c.access_token.as_ref()).is_some() {
View::Library { items, total, loading, .. } => { "Connected ✅"
if *loading { } else {
format!("loading… {}/{}", items.len(), total) "Disconnected ❌"
} else {
format!("{}/{}", items.len(), total)
}
}
_ => app.config_status.clone(),
}; };
let server = app
.config
.as_ref()
.map(|c| c.base_url.as_str())
.unwrap_or("-");
let right_text = format!(
"{connected}\n\np/P: play/autoplay\nd: download\nq: quit\n/help: more help"
);
frame.render_widget( frame.render_widget(
Paragraph::new(info) Paragraph::new(right_text)
.block(Block::bordered().title("Info").merge_borders(MergeStrategy::Exact)), .wrap(Wrap { trim: false })
.block(Block::bordered().merge_borders(MergeStrategy::Exact)),
panes.right_output, panes.right_output,
); );
} }
@@ -1247,15 +1285,9 @@ fn render_categories(frame: &mut Frame, app: &App, left: Rect, right: Rect) {
frame.render_stateful_widget(list, left, &mut state); frame.render_stateful_widget(list, left, &mut state);
// Right pane details (use the real selected item) // Right pane details (use the real selected item)
let detail = if len > 0 {
format!("Details for:\n\n{}", app.items[selected])
} else {
"No items".to_string()
};
frame.render_widget( frame.render_widget(
Paragraph::new(detail) Paragraph::new("")
.block(Block::bordered().title("Details").merge_borders(MergeStrategy::Exact)), .block(Block::bordered().merge_borders(MergeStrategy::Exact)),
right, right,
); );
} }
@@ -1399,7 +1431,7 @@ fn open_children(app: &mut App, title: String, query: core::jellyfin::LibraryQue
page_size: PAGE_SIZE, page_size: PAGE_SIZE,
loading: false, loading: false,
}); });
app.status = "Loaded".into(); app.status = "$".into();
} }
Err(e) => app.status = format!("Fetch failed: {e}"), Err(e) => app.status = format!("Fetch failed: {e}"),
} }
@@ -1694,7 +1726,7 @@ fn autoplay_tick(app: &mut App) {
Some(i) => i, Some(i) => i,
None => { None => {
app.autoplay = false; app.autoplay = false;
app.status = "autoplay finished".into(); app.status = "$".into();
return; return;
} }
}; };
@@ -1704,7 +1736,7 @@ fn autoplay_tick(app: &mut App) {
_ => { _ => {
app.autoplay = false; app.autoplay = false;
app.autoplay_next_index = None; app.autoplay_next_index = None;
app.status = "autoplay stopped".into(); app.status = "$".into();
return; return;
} }
}; };
@@ -1741,7 +1773,7 @@ fn autoplay_tick(app: &mut App) {
app.autoplay_prev_time_pos = None; app.autoplay_prev_time_pos = None;
if !app.debug_mpv { if !app.debug_mpv {
app.status = "autoplay next".into(); app.status = "$".into();
} }
} }