Improved UI.
This commit is contained in:
@@ -93,8 +93,9 @@ Common key bindings:
|
|||||||
| `x` | Like / unlike |
|
| `x` | Like / unlike |
|
||||||
| `a` | Add track next |
|
| `a` | Add track next |
|
||||||
| `Shift-A` | Add track to the end of the queue |
|
| `Shift-A` | Add track to the end of the queue |
|
||||||
| `Shift-P` | Add track to a playlist |
|
| `Shift-P` | Add selected/current track(s) to a playlist |
|
||||||
| `Shift-D` | Open device picker |
|
| `i`, `Shift-I` | Track info / current track info |
|
||||||
|
| `Shift-D` | Delete selected item |
|
||||||
| `v` | Toggle tile/table view |
|
| `v` | Toggle tile/table view |
|
||||||
| `/` | Search |
|
| `/` | Search |
|
||||||
| `:` | Open command line |
|
| `:` | Open command line |
|
||||||
|
|||||||
+4
-1
@@ -34,6 +34,7 @@ pub enum Action {
|
|||||||
ToggleLike,
|
ToggleLike,
|
||||||
ToggleTrackSelection,
|
ToggleTrackSelection,
|
||||||
OpenTrackInfo,
|
OpenTrackInfo,
|
||||||
|
OpenCurrentTrackInfo,
|
||||||
QueueAddNext,
|
QueueAddNext,
|
||||||
QueueAddLast,
|
QueueAddLast,
|
||||||
/// Download the selected federated track(s) into the local library.
|
/// Download the selected federated track(s) into the local library.
|
||||||
@@ -105,7 +106,8 @@ impl Action {
|
|||||||
| Action::NewPlaylist
|
| Action::NewPlaylist
|
||||||
| Action::ToggleLike
|
| Action::ToggleLike
|
||||||
| Action::ToggleTrackSelection
|
| Action::ToggleTrackSelection
|
||||||
| Action::OpenTrackInfo => Category::Queue,
|
| Action::OpenTrackInfo
|
||||||
|
| Action::OpenCurrentTrackInfo => Category::Queue,
|
||||||
Action::MoveUp
|
Action::MoveUp
|
||||||
| Action::MoveDown
|
| Action::MoveDown
|
||||||
| Action::MoveLeft
|
| Action::MoveLeft
|
||||||
@@ -174,6 +176,7 @@ impl Action {
|
|||||||
Action::ToggleLike => "Like / unlike".into(),
|
Action::ToggleLike => "Like / unlike".into(),
|
||||||
Action::ToggleTrackSelection => "Track line selection".into(),
|
Action::ToggleTrackSelection => "Track line selection".into(),
|
||||||
Action::OpenTrackInfo => "Track info".into(),
|
Action::OpenTrackInfo => "Track info".into(),
|
||||||
|
Action::OpenCurrentTrackInfo => "Current track info".into(),
|
||||||
Action::QueueAddNext => "Queue: add next".into(),
|
Action::QueueAddNext => "Queue: add next".into(),
|
||||||
Action::QueueAddLast => "Queue: add to end".into(),
|
Action::QueueAddLast => "Queue: add to end".into(),
|
||||||
Action::DownloadSelected => "Federation: download to library".into(),
|
Action::DownloadSelected => "Federation: download to library".into(),
|
||||||
|
|||||||
+15
-6
@@ -12,7 +12,8 @@ use crossterm::event::{KeyCode, KeyEvent};
|
|||||||
use crate::app::Runtime;
|
use crate::app::Runtime;
|
||||||
use crate::app::event::AppEvent;
|
use crate::app::event::AppEvent;
|
||||||
use crate::app::state::{
|
use crate::app::state::{
|
||||||
AppState, DeleteTarget, EditField, EditTarget, FedInputField, Popup, addable_playlists,
|
AppState, DeleteTarget, EditField, EditTarget, FedInputField, Loadable, Popup,
|
||||||
|
addable_playlists,
|
||||||
};
|
};
|
||||||
use crate::library::models::{ReleaseEdit, TrackEdit, TrackItem};
|
use crate::library::models::{ReleaseEdit, TrackEdit, TrackItem};
|
||||||
|
|
||||||
@@ -536,7 +537,9 @@ fn handle_picker(
|
|||||||
cursor: usize,
|
cursor: usize,
|
||||||
key: KeyEvent,
|
key: KeyEvent,
|
||||||
) {
|
) {
|
||||||
|
let waiting = matches!(&state.playlists.list, None | Some(Loadable::Loading));
|
||||||
let options = addable_playlists(state);
|
let options = addable_playlists(state);
|
||||||
|
let new_index = options.len();
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Esc => {}
|
KeyCode::Esc => {}
|
||||||
KeyCode::Up | KeyCode::Char('k') => {
|
KeyCode::Up | KeyCode::Char('k') => {
|
||||||
@@ -548,18 +551,23 @@ fn handle_picker(
|
|||||||
KeyCode::Down | KeyCode::Char('j') => {
|
KeyCode::Down | KeyCode::Char('j') => {
|
||||||
state.popup = Some(Popup::AddToPlaylist {
|
state.popup = Some(Popup::AddToPlaylist {
|
||||||
target,
|
target,
|
||||||
cursor: (cursor + 1).min(options.len()),
|
cursor: (cursor + 1).min(new_index),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
KeyCode::Enter => {
|
KeyCode::Enter => {
|
||||||
if cursor == 0 {
|
if waiting {
|
||||||
|
state.status_message = Some("loading playlists…".into());
|
||||||
|
state.popup = Some(Popup::AddToPlaylist { target, cursor });
|
||||||
|
} else if cursor < options.len() {
|
||||||
|
if let Some((id, title)) = options.get(cursor).cloned() {
|
||||||
|
spawn_add_target(runtime, id, title, target);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
state.popup = Some(Popup::NewPlaylist {
|
state.popup = Some(Popup::NewPlaylist {
|
||||||
for_target: Some(target),
|
for_target: Some(target),
|
||||||
input: crate::app::input::LineEdit::default(),
|
input: crate::app::input::LineEdit::default(),
|
||||||
busy: false,
|
busy: false,
|
||||||
});
|
});
|
||||||
} else if let Some((id, title)) = options.get(cursor - 1).cloned() {
|
|
||||||
spawn_add_target(runtime, id, title, target);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => state.popup = Some(Popup::AddToPlaylist { target, cursor }),
|
_ => state.popup = Some(Popup::AddToPlaylist { target, cursor }),
|
||||||
@@ -586,7 +594,8 @@ fn handle_name_entry(
|
|||||||
KeyCode::Esc => {
|
KeyCode::Esc => {
|
||||||
// Reached from the picker → step back to it; otherwise close.
|
// Reached from the picker → step back to it; otherwise close.
|
||||||
if let Some(target) = for_target {
|
if let Some(target) = for_target {
|
||||||
state.popup = Some(Popup::AddToPlaylist { target, cursor: 0 });
|
let cursor = addable_playlists(state).len();
|
||||||
|
state.popup = Some(Popup::AddToPlaylist { target, cursor });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Enter => {
|
KeyCode::Enter => {
|
||||||
|
|||||||
+1
-1
@@ -480,7 +480,7 @@ impl PlaylistAddTarget {
|
|||||||
/// Modal dialog over the main screen.
|
/// Modal dialog over the main screen.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Popup {
|
pub enum Popup {
|
||||||
/// Pick one of the playlists (row 0 = "create new"); the target is
|
/// Pick one of the playlists (last row = "create new"); the target is
|
||||||
/// added on Enter (federated tracks are downloaded first).
|
/// added on Enter (federated tracks are downloaded first).
|
||||||
AddToPlaylist {
|
AddToPlaylist {
|
||||||
target: PlaylistAddTarget,
|
target: PlaylistAddTarget,
|
||||||
|
|||||||
+142
-35
@@ -283,27 +283,11 @@ pub fn update(state: &mut AppState, action: Action) -> Option<Effect> {
|
|||||||
}
|
}
|
||||||
Action::OpenTrackInfo => {
|
Action::OpenTrackInfo => {
|
||||||
let tracks = selected_tracks(state);
|
let tracks = selected_tracks(state);
|
||||||
if tracks.is_empty() {
|
open_track_info(state, tracks, "no track selected")
|
||||||
state.status_message = Some("no track selected".into());
|
}
|
||||||
None
|
Action::OpenCurrentTrackInfo => {
|
||||||
} else {
|
let tracks = state.player.current.clone().into_iter().collect();
|
||||||
let fed_tracks = tracks
|
open_track_info(state, tracks, "nothing playing")
|
||||||
.iter()
|
|
||||||
.filter(|track| track_info_needs_fed_metadata(track))
|
|
||||||
.filter_map(|track| track.fed.as_ref().map(|fed| (track.id, fed.clone())))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
state.popup = Some(super::state::Popup::TrackInfo {
|
|
||||||
tracks,
|
|
||||||
cursor: 0,
|
|
||||||
scroll: 0,
|
|
||||||
});
|
|
||||||
if fed_tracks.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
state.status_message = Some("federation: fetching track metadata…".to_string());
|
|
||||||
Some(Effect::FedFetchTrackInfo { tracks: fed_tracks })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Action::RemoveFromQueue => remove_selected_from_queue(state),
|
Action::RemoveFromQueue => remove_selected_from_queue(state),
|
||||||
Action::QueueAddNext => queue_add(state, true),
|
Action::QueueAddNext => queue_add(state, true),
|
||||||
@@ -317,19 +301,7 @@ pub fn update(state: &mut AppState, action: Action) -> Option<Effect> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
Action::AddToPlaylist => {
|
Action::AddToPlaylist => {
|
||||||
let fed = selected_fed_tracks(state);
|
let target = selected_playlist_target(state, true);
|
||||||
let target = if !fed.is_empty() {
|
|
||||||
Some(super::state::PlaylistAddTarget::Fed(fed))
|
|
||||||
} else {
|
|
||||||
let local = selected_tracks(state);
|
|
||||||
if !local.is_empty() {
|
|
||||||
Some(super::state::PlaylistAddTarget::Local(local))
|
|
||||||
} else {
|
|
||||||
selected_track(state)
|
|
||||||
.or_else(|| state.player.current.clone())
|
|
||||||
.map(|track| super::state::PlaylistAddTarget::Local(vec![track]))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
match target {
|
match target {
|
||||||
Some(target) => {
|
Some(target) => {
|
||||||
state.popup = Some(super::state::Popup::AddToPlaylist { target, cursor: 0 });
|
state.popup = Some(super::state::Popup::AddToPlaylist { target, cursor: 0 });
|
||||||
@@ -355,11 +327,13 @@ pub fn update(state: &mut AppState, action: Action) -> Option<Effect> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Action::NewPlaylist => {
|
Action::NewPlaylist => {
|
||||||
|
let for_target = selected_playlist_target(state, false);
|
||||||
state.popup = Some(super::state::Popup::NewPlaylist {
|
state.popup = Some(super::state::Popup::NewPlaylist {
|
||||||
for_target: None,
|
for_target,
|
||||||
input: crate::app::input::LineEdit::default(),
|
input: crate::app::input::LineEdit::default(),
|
||||||
busy: false,
|
busy: false,
|
||||||
});
|
});
|
||||||
|
state.track_selection.clear();
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
Action::ClearQueue => {
|
Action::ClearQueue => {
|
||||||
@@ -399,6 +373,54 @@ fn track_info_needs_fed_metadata(track: &TrackItem) -> bool {
|
|||||||
|| track.file_path.is_empty())
|
|| track.file_path.is_empty())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn open_track_info(
|
||||||
|
state: &mut AppState,
|
||||||
|
tracks: Vec<TrackItem>,
|
||||||
|
empty_message: &'static str,
|
||||||
|
) -> Option<Effect> {
|
||||||
|
if tracks.is_empty() {
|
||||||
|
state.status_message = Some(empty_message.into());
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let fed_tracks = tracks
|
||||||
|
.iter()
|
||||||
|
.filter(|track| track_info_needs_fed_metadata(track))
|
||||||
|
.filter_map(|track| track.fed.as_ref().map(|fed| (track.id, fed.clone())))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
state.popup = Some(super::state::Popup::TrackInfo {
|
||||||
|
tracks,
|
||||||
|
cursor: 0,
|
||||||
|
scroll: 0,
|
||||||
|
});
|
||||||
|
if fed_tracks.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
state.status_message = Some("federation: fetching track metadata…".to_string());
|
||||||
|
Some(Effect::FedFetchTrackInfo { tracks: fed_tracks })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected_playlist_target(
|
||||||
|
state: &AppState,
|
||||||
|
include_current: bool,
|
||||||
|
) -> Option<super::state::PlaylistAddTarget> {
|
||||||
|
let fed = selected_fed_tracks(state);
|
||||||
|
if !fed.is_empty() {
|
||||||
|
return Some(super::state::PlaylistAddTarget::Fed(fed));
|
||||||
|
}
|
||||||
|
|
||||||
|
let local = selected_tracks(state);
|
||||||
|
if !local.is_empty() {
|
||||||
|
return Some(super::state::PlaylistAddTarget::Local(local));
|
||||||
|
}
|
||||||
|
|
||||||
|
include_current
|
||||||
|
.then(|| state.player.current.clone())
|
||||||
|
.flatten()
|
||||||
|
.map(|track| super::state::PlaylistAddTarget::Local(vec![track]))
|
||||||
|
}
|
||||||
|
|
||||||
/// `e`: open the metadata edit form for whatever is under the cursor —
|
/// `e`: open the metadata edit form for whatever is under the cursor —
|
||||||
/// an artist tile, a release, a track or a playlist.
|
/// an artist tile, a release, a track or a playlist.
|
||||||
fn open_edit_popup(state: &mut AppState) {
|
fn open_edit_popup(state: &mut AppState) {
|
||||||
@@ -2748,6 +2770,91 @@ mod tests {
|
|||||||
assert!(!state.player.playing);
|
assert!(!state.player.playing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn current_track_info_uses_now_playing_track() {
|
||||||
|
let mut state = AppState {
|
||||||
|
active_tab: Tab::Queue,
|
||||||
|
..AppState::default()
|
||||||
|
};
|
||||||
|
state.player.queue = vec![test_track(1), test_track(2)];
|
||||||
|
state.queue_tab.cursor = 0;
|
||||||
|
state.player.current = Some(test_track(2));
|
||||||
|
|
||||||
|
assert_eq!(update(&mut state, Action::OpenCurrentTrackInfo), None);
|
||||||
|
match &state.popup {
|
||||||
|
Some(crate::app::state::Popup::TrackInfo { tracks, .. }) => {
|
||||||
|
assert_eq!(
|
||||||
|
tracks.iter().map(|track| track.id).collect::<Vec<_>>(),
|
||||||
|
vec![2]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
other => panic!("expected track info popup, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add_to_playlist_from_release_carries_selected_track() {
|
||||||
|
use crate::app::state::{PlaylistAddTarget, Popup};
|
||||||
|
use crate::library::models::ReleaseDetail;
|
||||||
|
|
||||||
|
let mut state = AppState::default();
|
||||||
|
state
|
||||||
|
.global
|
||||||
|
.stack
|
||||||
|
.push(GlobalView::Release { id: 1, cursor: 1 });
|
||||||
|
state.release_views.insert(
|
||||||
|
1,
|
||||||
|
Loadable::Ready(ReleaseDetail {
|
||||||
|
id: 1,
|
||||||
|
title: "r".into(),
|
||||||
|
release_type: "album".into(),
|
||||||
|
year: None,
|
||||||
|
cover_path: None,
|
||||||
|
artists: vec![],
|
||||||
|
tracks: vec![test_track(1), test_track(2)],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(update(&mut state, Action::AddToPlaylist), None);
|
||||||
|
match &state.popup {
|
||||||
|
Some(Popup::AddToPlaylist {
|
||||||
|
target: PlaylistAddTarget::Local(tracks),
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
assert_eq!(
|
||||||
|
tracks.iter().map(|track| track.id).collect::<Vec<_>>(),
|
||||||
|
vec![2]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
other => panic!("expected add-to-playlist popup with selected track, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add_to_playlist_from_non_track_view_uses_current_track() {
|
||||||
|
use crate::app::state::{PlaylistAddTarget, Popup};
|
||||||
|
|
||||||
|
let mut state = AppState {
|
||||||
|
active_tab: Tab::Logs,
|
||||||
|
..AppState::default()
|
||||||
|
};
|
||||||
|
state.player.current = Some(test_track(7));
|
||||||
|
|
||||||
|
assert_eq!(update(&mut state, Action::AddToPlaylist), None);
|
||||||
|
match &state.popup {
|
||||||
|
Some(Popup::AddToPlaylist {
|
||||||
|
target: PlaylistAddTarget::Local(tracks),
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
assert_eq!(
|
||||||
|
tracks.iter().map(|track| track.id).collect::<Vec<_>>(),
|
||||||
|
vec![7]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
other => panic!("expected add-to-playlist popup with current track, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn visual_selection_removes_queue_range() {
|
fn visual_selection_removes_queue_range() {
|
||||||
let mut state = AppState {
|
let mut state = AppState {
|
||||||
|
|||||||
@@ -86,11 +86,6 @@ command = "GoToRelease"
|
|||||||
key_sequence = "shift-p"
|
key_sequence = "shift-p"
|
||||||
command = "AddToPlaylist"
|
command = "AddToPlaylist"
|
||||||
|
|
||||||
[[keymaps]]
|
|
||||||
key_sequence = "n"
|
|
||||||
command = "NewPlaylist"
|
|
||||||
context = "playlists"
|
|
||||||
|
|
||||||
[[keymaps]]
|
[[keymaps]]
|
||||||
key_sequence = "j"
|
key_sequence = "j"
|
||||||
command = "MoveDown"
|
command = "MoveDown"
|
||||||
@@ -211,6 +206,10 @@ command = "ToggleTrackSelection"
|
|||||||
key_sequence = "i"
|
key_sequence = "i"
|
||||||
command = "OpenTrackInfo"
|
command = "OpenTrackInfo"
|
||||||
|
|
||||||
|
[[keymaps]]
|
||||||
|
key_sequence = "shift-i"
|
||||||
|
command = "OpenCurrentTrackInfo"
|
||||||
|
|
||||||
[[keymaps]]
|
[[keymaps]]
|
||||||
key_sequence = "e"
|
key_sequence = "e"
|
||||||
command = "EditSelected"
|
command = "EditSelected"
|
||||||
|
|||||||
@@ -416,6 +416,49 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_playlist_context_keeps_next_track_on_n() {
|
||||||
|
let mut km = keymap_from(DEFAULT_KEYMAP);
|
||||||
|
assert_eq!(
|
||||||
|
km.resolve(key!(n), KeyContext::Playlists),
|
||||||
|
KeyResolution::Action(Action::NextTrack)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_shift_n_is_unbound() {
|
||||||
|
let mut km = keymap_from(DEFAULT_KEYMAP);
|
||||||
|
let shift_n = KeyCombination::new(KeyCode::Char('N'), KeyModifiers::SHIFT);
|
||||||
|
assert_eq!(
|
||||||
|
km.resolve(shift_n, KeyContext::Library),
|
||||||
|
KeyResolution::Unmatched
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
km.resolve(shift_n, KeyContext::Playlists),
|
||||||
|
KeyResolution::Unmatched
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_add_to_playlist_key_is_global() {
|
||||||
|
let mut km = keymap_from(DEFAULT_KEYMAP);
|
||||||
|
let shift_p = KeyCombination::new(KeyCode::Char('P'), KeyModifiers::SHIFT);
|
||||||
|
assert_eq!(
|
||||||
|
km.resolve(shift_p, KeyContext::Library),
|
||||||
|
KeyResolution::Action(Action::AddToPlaylist)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_current_track_info_key_resolves() {
|
||||||
|
let mut km = keymap_from(DEFAULT_KEYMAP);
|
||||||
|
let shift_i = KeyCombination::new(KeyCode::Char('I'), KeyModifiers::SHIFT);
|
||||||
|
assert_eq!(
|
||||||
|
km.resolve(shift_i, KeyContext::Library),
|
||||||
|
KeyResolution::Action(Action::OpenCurrentTrackInfo)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn user_binding_overrides_default() {
|
fn user_binding_overrides_default() {
|
||||||
let mut bindings = parse_bindings(DEFAULT_KEYMAP).unwrap();
|
let mut bindings = parse_bindings(DEFAULT_KEYMAP).unwrap();
|
||||||
|
|||||||
+29
-10
@@ -441,8 +441,14 @@ fn file_size(value: Option<i64>) -> String {
|
|||||||
|
|
||||||
fn draw_picker(frame: &mut Frame, state: &AppState, track_title: &str, cursor: usize) {
|
fn draw_picker(frame: &mut Frame, state: &AppState, track_title: &str, cursor: usize) {
|
||||||
let options = addable_playlists(state);
|
let options = addable_playlists(state);
|
||||||
let loading = !matches!(&state.playlists.list, Some(Loadable::Ready(_)));
|
let loading = matches!(&state.playlists.list, None | Some(Loadable::Loading));
|
||||||
let rows = options.len() + 1;
|
let rows = if loading {
|
||||||
|
1
|
||||||
|
} else if options.is_empty() {
|
||||||
|
2
|
||||||
|
} else {
|
||||||
|
options.len() + 1
|
||||||
|
};
|
||||||
let height = (rows as u16 + 4)
|
let height = (rows as u16 + 4)
|
||||||
.min(frame.area().height.saturating_sub(2))
|
.min(frame.area().height.saturating_sub(2))
|
||||||
.max(6);
|
.max(6);
|
||||||
@@ -463,18 +469,30 @@ fn draw_picker(frame: &mut Frame, state: &AppState, track_title: &str, cursor: u
|
|||||||
])
|
])
|
||||||
.areas(inner);
|
.areas(inner);
|
||||||
|
|
||||||
let mut lines: Vec<Line> = vec![Line::styled("+ New playlist…", theme::accent())];
|
let mut lines: Vec<Line> = Vec::new();
|
||||||
if loading {
|
if loading {
|
||||||
lines.push(Line::styled("loading playlists…", theme::dim()));
|
lines.push(Line::styled("loading playlists…", theme::dim()));
|
||||||
|
} else if matches!(&state.playlists.list, Some(Loadable::Failed(_))) {
|
||||||
|
lines.push(Line::styled("playlist list unavailable", theme::dim()));
|
||||||
|
lines.push(Line::styled("+ New playlist…", theme::accent()));
|
||||||
} else if options.is_empty() {
|
} else if options.is_empty() {
|
||||||
lines.push(Line::styled("no playlists yet", theme::dim()));
|
lines.push(Line::styled("no playlists yet", theme::dim()));
|
||||||
|
lines.push(Line::styled("+ New playlist…", theme::accent()));
|
||||||
} else {
|
} else {
|
||||||
for (_, title) in &options {
|
for (_, title) in &options {
|
||||||
lines.push(Line::raw(title.clone()));
|
lines.push(Line::raw(title.clone()));
|
||||||
}
|
}
|
||||||
|
lines.push(Line::styled("+ New playlist…", theme::accent()));
|
||||||
}
|
}
|
||||||
|
let selected_line = if loading {
|
||||||
|
0
|
||||||
|
} else if options.is_empty() {
|
||||||
|
lines.len().saturating_sub(1)
|
||||||
|
} else {
|
||||||
|
cursor.min(options.len())
|
||||||
|
};
|
||||||
let visible = usize::from(list_area.height.max(1));
|
let visible = usize::from(list_area.height.max(1));
|
||||||
let first = cursor
|
let first = selected_line
|
||||||
.saturating_sub(visible / 2)
|
.saturating_sub(visible / 2)
|
||||||
.min(lines.len().saturating_sub(visible));
|
.min(lines.len().saturating_sub(visible));
|
||||||
for (index, line) in lines.into_iter().enumerate().skip(first).take(visible) {
|
for (index, line) in lines.into_iter().enumerate().skip(first).take(visible) {
|
||||||
@@ -485,17 +503,18 @@ fn draw_picker(frame: &mut Frame, state: &AppState, track_title: &str, cursor: u
|
|||||||
height: 1,
|
height: 1,
|
||||||
};
|
};
|
||||||
frame.render_widget(Paragraph::new(line), row);
|
frame.render_widget(Paragraph::new(line), row);
|
||||||
if index == cursor {
|
if index == selected_line {
|
||||||
frame.buffer_mut().set_style(row, theme::tab_active());
|
frame.buffer_mut().set_style(row, theme::tab_active());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let footer_text = if loading {
|
||||||
|
format!("♪ {track_title} · loading playlists · esc close")
|
||||||
|
} else {
|
||||||
|
format!("♪ {track_title} · enter add/create · esc close")
|
||||||
|
};
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
Paragraph::new(Line::styled(
|
Paragraph::new(Line::styled(footer_text, theme::dim())).alignment(Alignment::Center),
|
||||||
format!("♪ {track_title} · enter add · esc close"),
|
|
||||||
theme::dim(),
|
|
||||||
))
|
|
||||||
.alignment(Alignment::Center),
|
|
||||||
footer,
|
footer,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user