Big UI Overhaul
This commit is contained in:
120
src/main.rs
120
src/main.rs
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user