use super::*;
use crate::{command::construct_artist_actions, utils::filtered_items_from_query};
use anyhow::Context;
pub fn handle_key_sequence_for_popup(
key_sequence: &KeySequence,
client_pub: &flume::Sender<ClientRequest>,
state: &SharedState,
ui: &mut UIStateGuard,
) -> Result<bool> {
match ui.popup.as_ref().context("empty popup")? {
PopupState::Search { .. } => {
return handle_key_sequence_for_search_popup(key_sequence, client_pub, state, ui);
}
PopupState::PlaylistCreate { .. } => {
return handle_key_sequence_for_create_playlist_popup(key_sequence, client_pub, ui);
}
PopupState::ActionList(item, ..) => {
return handle_key_sequence_for_action_list_popup(
item.n_actions(),
key_sequence,
client_pub,
state,
ui,
);
}
PopupState::UserPlaylistList(..) => {
if handle_key_sequence_for_playlist_search_popup(key_sequence, ui) {
return Ok(true);
}
}
_ => {}
}
let Some(command) = config::get_config()
.keymap_config
.find_command_from_key_sequence(key_sequence)
else {
return Ok(false);
};
match ui.popup.as_ref().context("empty popup")? {
PopupState::Search { .. } => anyhow::bail!("search popup should be handled before"),
PopupState::PlaylistCreate { .. } => {
anyhow::bail!("create playlist popup should be handled before")
}
PopupState::ActionList(..) => {
anyhow::bail!("action list popup should be handled before")
}
PopupState::ArtistList(_, artists, _) => {
let n_items = artists.len();
handle_command_for_list_popup(
command,
ui,
n_items,
|_, _| {},
|ui: &mut UIStateGuard, id: usize| -> Result<()> {
let Some(PopupState::ArtistList(action, artists, _)) = &ui.popup else {
return Ok(());
};
match action {
ArtistPopupAction::Browse => {
let context_id = ContextId::Artist(artists[id].id.clone());
ui.new_page(PageState::Context {
id: None,
context_page_type: ContextPageType::Browsing(context_id),
state: None,
});
}
ArtistPopupAction::ShowActions => {
let actions = {
let data = state.data.read();
construct_artist_actions(&artists[id], &data)
};
ui.popup = Some(PopupState::ActionList(
Box::new(ActionListItem::Artist(artists[id].clone(), actions)),
ListState::default(),
));
}
}
Ok(())
},
|ui: &mut UIStateGuard| {
ui.popup = None;
},
)
}
PopupState::UserPlaylistList(action, _) => match action {
PlaylistPopupAction::Browse {
folder_id,
search_query,
} => {
let search_query = search_query.clone();
let data = state.data.read();
let items = data.user_data.folder_playlists_items(*folder_id);
let filtered_items = filtered_items_from_query(&search_query, &items);
handle_command_for_list_popup(
command,
ui,
filtered_items.len(),
|_, _| {},
|ui: &mut UIStateGuard, id: usize| -> Result<()> {
match filtered_items.get(id).expect("invalid index") {
PlaylistFolderItem::Folder(f) => {
ui.popup = Some(PopupState::UserPlaylistList(
PlaylistPopupAction::Browse {
folder_id: f.target_id,
search_query: search_query.clone(),
},
ListState::default(),
));
}
PlaylistFolderItem::Playlist(p) => {
let context_id = ContextId::Playlist(
PlaylistId::from_uri(&crate::utils::parse_uri(&p.id.uri()))?
.into_static(),
);
ui.new_page(PageState::Context {
id: None,
context_page_type: ContextPageType::Browsing(context_id),
state: None,
});
}
}
Ok(())
},
|ui: &mut UIStateGuard| {
ui.popup = None;
},
)
}
PlaylistPopupAction::AddTrack {
folder_id,
track_id,
search_query,
} => {
let search_query = search_query.clone();
let track_id = track_id.clone();
let data = state.data.read();
let items = data.user_data.modifiable_playlist_items(Some(*folder_id));
let filtered_items = filtered_items_from_query(&search_query, &items);
handle_command_for_list_popup(
command,
ui,
filtered_items.len(),
|_, _| {},
|ui: &mut UIStateGuard, id: usize| -> Result<()> {
ui.popup = match filtered_items.get(id).expect("invalid index") {
PlaylistFolderItem::Folder(f) => Some(PopupState::UserPlaylistList(
PlaylistPopupAction::AddTrack {
folder_id: f.target_id,
track_id,
search_query: search_query.clone(),
},
ListState::default(),
)),
PlaylistFolderItem::Playlist(p) => {
client_pub.send(ClientRequest::AddPlayableToPlaylist(
p.id.clone(),
track_id.into(),
))?;
None
}
};
Ok(())
},
|ui: &mut UIStateGuard| {
ui.popup = None;
},
)
}
PlaylistPopupAction::AddEpisode {
folder_id,
episode_id,
search_query,
} => {
let search_query = search_query.clone();
let episode_id = episode_id.clone();
let data = state.data.read();
let items = data.user_data.modifiable_playlist_items(Some(*folder_id));
let filtered_items = filtered_items_from_query(&search_query, &items);
handle_command_for_list_popup(
command,
ui,
filtered_items.len(),
|_, _| {},
|ui: &mut UIStateGuard, id: usize| -> Result<()> {
ui.popup = match filtered_items.get(id).expect("invalid index") {
PlaylistFolderItem::Folder(f) => Some(PopupState::UserPlaylistList(
PlaylistPopupAction::AddEpisode {
folder_id: f.target_id,
episode_id,
search_query: search_query.clone(),
},
ListState::default(),
)),
PlaylistFolderItem::Playlist(p) => {
client_pub.send(ClientRequest::AddPlayableToPlaylist(
p.id.clone(),
episode_id.into(),
))?;
None
}
};
Ok(())
},
|ui: &mut UIStateGuard| {
ui.popup = None;
},
)
}
},
PopupState::UserFollowedArtistList(_) => {
let artist_uris = state
.data
.read()
.user_data
.followed_artists
.iter()
.map(|a| a.id.uri())
.collect::<Vec<_>>();
handle_command_for_context_browsing_list_popup(
command,
ui,
&artist_uris,
&rspotify::model::Type::Artist,
)
}
PopupState::UserSavedAlbumList(_) => {
let album_uris = state
.data
.read()
.user_data
.saved_albums
.iter()
.map(|a| a.id.uri())
.collect::<Vec<_>>();
handle_command_for_context_browsing_list_popup(
command,
ui,
&album_uris,
&rspotify::model::Type::Album,
)
}
PopupState::ThemeList(themes, _) => {
let n_items = themes.len();
handle_command_for_list_popup(
command,
ui,
n_items,
|ui: &mut UIStateGuard, id: usize| {
ui.theme = match ui.popup {
Some(PopupState::ThemeList(ref themes, _)) => themes[id].clone(),
_ => return,
};
},
|ui: &mut UIStateGuard, _| -> Result<()> {
ui.popup = None;
Ok(())
},
|ui: &mut UIStateGuard| {
ui.theme = match ui.popup {
Some(PopupState::ThemeList(ref themes, _)) => themes[0].clone(),
_ => return,
};
ui.popup = None;
},
)
}
PopupState::DeviceList(_) => {
let player = state.player.read();
handle_command_for_list_popup(
command,
ui,
player.devices.len(),
|_, _| {},
|ui: &mut UIStateGuard, id: usize| -> Result<()> {
let is_playing = player.playback.as_ref().is_some_and(|p| p.is_playing);
client_pub.send(ClientRequest::Player(PlayerRequest::TransferPlayback(
player.devices[id].id.clone(),
is_playing,
)))?;
ui.popup = None;
Ok(())
},
|ui: &mut UIStateGuard| {
ui.popup = None;
},
)
}
}
}
fn handle_key_sequence_for_create_playlist_popup(
key_sequence: &KeySequence,
client_pub: &flume::Sender<ClientRequest>,
ui: &mut UIStateGuard,
) -> Result<bool> {
let Some(PopupState::PlaylistCreate {
name,
desc,
current_field,
}) = &mut ui.popup
else {
return Ok(false);
};
if key_sequence.keys.len() == 1 {
match &key_sequence.keys[0] {
Key::None(crossterm::event::KeyCode::Enter) => {
client_pub.send(ClientRequest::CreatePlaylist {
playlist_name: name.get_text(),
public: false,
collab: false,
desc: desc.get_text(),
})?;
ui.popup = None;
return Ok(true);
}
Key::None(crossterm::event::KeyCode::Tab | crossterm::event::KeyCode::BackTab) => {
*current_field = match ¤t_field {
PlaylistCreateCurrentField::Name => PlaylistCreateCurrentField::Desc,
PlaylistCreateCurrentField::Desc => PlaylistCreateCurrentField::Name,
};
return Ok(true);
}
k => {
let line_input = match current_field {
PlaylistCreateCurrentField::Name => name,
PlaylistCreateCurrentField::Desc => desc,
};
if line_input.input(k).is_some() {
return Ok(true);
}
}
}
}
Ok(false)
}
fn handle_key_sequence_for_search_popup(
key_sequence: &KeySequence,
client_pub: &flume::Sender<ClientRequest>,
state: &SharedState,
ui: &mut UIStateGuard,
) -> Result<bool> {
let Some(PopupState::Search { ref mut query }) = &mut ui.popup else {
return Ok(false);
};
if key_sequence.keys.len() == 1 {
if let Key::None(c) = key_sequence.keys[0] {
match c {
crossterm::event::KeyCode::Char(c) => {
query.push(c);
ui.current_page_mut().select(0);
return Ok(true);
}
crossterm::event::KeyCode::Backspace => {
if query.is_empty() {
ui.popup = None;
} else {
query.pop().unwrap();
ui.current_page_mut().select(0);
}
return Ok(true);
}
_ => {}
}
}
}
page::handle_key_sequence_for_page(key_sequence, client_pub, state, ui)
}
fn handle_command_for_context_browsing_list_popup(
command: Command,
ui: &mut UIStateGuard,
uris: &[String],
context_type: &rspotify::model::Type,
) -> Result<bool> {
handle_command_for_list_popup(
command,
ui,
uris.len(),
|_, _| {},
|ui: &mut UIStateGuard, id: usize| -> Result<()> {
let uri = crate::utils::parse_uri(&uris[id]);
let context_id = match context_type {
rspotify::model::Type::Playlist => {
ContextId::Playlist(PlaylistId::from_uri(&uri)?.into_static())
}
rspotify::model::Type::Artist => {
ContextId::Artist(ArtistId::from_uri(&uri)?.into_static())
}
rspotify::model::Type::Album => {
ContextId::Album(AlbumId::from_uri(&uri)?.into_static())
}
_ => {
return Ok(());
}
};
ui.new_page(PageState::Context {
id: None,
context_page_type: ContextPageType::Browsing(context_id),
state: None,
});
Ok(())
},
|ui: &mut UIStateGuard| {
ui.popup = None;
},
)
}
fn handle_command_for_list_popup(
command: Command,
ui: &mut UIStateGuard,
n_items: usize,
on_select_func: impl FnOnce(&mut UIStateGuard, usize),
on_choose_func: impl FnOnce(&mut UIStateGuard, usize) -> anyhow::Result<()>,
on_close_func: impl FnOnce(&mut UIStateGuard),
) -> anyhow::Result<bool> {
let popup = ui.popup.as_mut().with_context(|| "expect a popup")?;
let current_id = popup.list_selected().unwrap_or_default();
match command {
Command::SelectPreviousOrScrollUp => {
if current_id > 0 {
popup.list_select(Some(current_id - 1));
on_select_func(ui, current_id - 1);
}
}
Command::SelectNextOrScrollDown => {
if current_id + 1 < n_items {
popup.list_select(Some(current_id + 1));
on_select_func(ui, current_id + 1);
}
}
Command::ChooseSelected => {
if current_id < n_items {
on_choose_func(ui, current_id)?;
}
}
Command::ClosePopup => {
on_close_func(ui);
}
_ => return Ok(false),
}
Ok(true)
}
fn handle_key_sequence_for_action_list_popup(
n_actions: usize,
key_sequence: &KeySequence,
client_pub: &flume::Sender<ClientRequest>,
state: &SharedState,
ui: &mut UIStateGuard,
) -> Result<bool> {
if let Some(Key::None(crossterm::event::KeyCode::Char(c))) = key_sequence.keys.first() {
if let Some(id) = c.to_digit(10) {
let id = id as usize;
if id < n_actions {
handle_item_action(id, client_pub, state, ui)?;
return Ok(true);
}
}
}
let Some(command) = config::get_config()
.keymap_config
.find_command_from_key_sequence(key_sequence)
else {
return Ok(false);
};
handle_command_for_list_popup(
command,
ui,
n_actions,
|_, _| {},
|ui: &mut UIStateGuard, id: usize| -> Result<()> {
handle_item_action(id, client_pub, state, ui)?;
Ok(())
},
|ui: &mut UIStateGuard| {
ui.popup = None;
},
)
}
pub fn handle_item_action(
n: usize,
client_pub: &flume::Sender<ClientRequest>,
state: &SharedState,
ui: &mut UIStateGuard,
) -> Result<bool> {
let item = match ui.popup {
Some(PopupState::ActionList(ref item, ..)) => *item.clone(),
_ => return Ok(false),
};
let data = state.data.read();
match item {
ActionListItem::Track(track, actions) => {
handle_action_in_context(actions[n], track.into(), client_pub, &data, ui)
}
ActionListItem::Album(album, actions) => {
handle_action_in_context(actions[n], album.into(), client_pub, &data, ui)
}
ActionListItem::Artist(artist, actions) => {
handle_action_in_context(actions[n], artist.into(), client_pub, &data, ui)
}
ActionListItem::Playlist(playlist, actions) => {
handle_action_in_context(actions[n], playlist.into(), client_pub, &data, ui)
}
ActionListItem::Show(show, actions) => {
handle_action_in_context(actions[n], show.into(), client_pub, &data, ui)
}
ActionListItem::Episode(episode, actions) => {
handle_action_in_context(actions[n], episode.into(), client_pub, &data, ui)
}
}
}
fn handle_key_sequence_for_playlist_search_popup(
key_sequence: &KeySequence,
ui: &mut UIStateGuard,
) -> bool {
let Some(PopupState::UserPlaylistList(ref mut action, _)) = &mut ui.popup else {
return false;
};
let search_query = match action {
PlaylistPopupAction::AddTrack { search_query, .. }
| PlaylistPopupAction::AddEpisode { search_query, .. }
| PlaylistPopupAction::Browse { search_query, .. } => search_query,
};
if key_sequence.keys.len() == 1 {
if let Key::None(c) = key_sequence.keys[0] {
match c {
crossterm::event::KeyCode::Char(c) => {
search_query.push(c);
if let Some(popup) = &mut ui.popup {
popup.list_select(Some(0));
}
return true;
}
crossterm::event::KeyCode::Backspace => {
if search_query.is_empty() {
ui.popup = None;
} else {
search_query.pop();
if let Some(popup) = &mut ui.popup {
popup.list_select(Some(0));
}
}
return true;
}
_ => {}
}
}
}
false
}