use std::sync::Arc; use cursive::view::{Margins, ViewWrapper}; use cursive::views::{Dialog, NamedView, ScrollView, SelectView}; use cursive::Cursive; use crate::commands::CommandResult; use crate::ext_traits::SelectViewExt; use crate::library::Library; use crate::model::artist::Artist; use crate::model::playable::Playable; use crate::model::playlist::Playlist; use crate::model::track::Track; use crate::queue::Queue; #[cfg(feature = "share_clipboard")] use crate::sharing::write_share; use crate::spotify::PlayerEvent; use crate::traits::{ListItem, ViewExt}; use crate::ui::layout::Layout; use crate::ui::modal::Modal; use crate::{command::Command, spotify::Spotify}; use cursive::traits::{Finder, Nameable}; pub struct ContextMenu { dialog: Modal, } pub struct PlayTrackMenu { dialog: Modal, } pub struct AddToPlaylistMenu { dialog: Modal, } pub struct SelectArtistMenu { dialog: Modal, } pub struct SelectArtistActionMenu { dialog: Modal, } enum ContextMenuAction { ShowItem(Box), SelectArtist(Vec), SelectArtistAction(Artist), #[cfg(feature = "share_clipboard")] ShareUrl(String), AddToPlaylist(Box), ShowRecommendations(Box), ToggleSavedStatus(Box), Play(Box), PlayNext(Box), TogglePlayback, Queue(Box), } impl ContextMenu { pub fn add_track_dialog( library: Arc, spotify: Spotify, track: Track, ) -> NamedView { let mut list_select: SelectView = SelectView::new(); let current_user_id = library.user_id.as_ref().unwrap(); for list in library.playlists().iter() { if current_user_id == &list.owner_id || list.collaborative { list_select.add_item(list.name.clone(), list.clone()); } } list_select.set_autojump(true); list_select.set_on_submit(move |s, selected| { let track = track.clone(); let mut playlist = selected.clone(); let spotify = spotify.clone(); let library = library.clone(); if playlist.has_track(track.id.as_ref().unwrap_or(&String::new())) { let mut already_added_dialog = Self::track_already_added(); already_added_dialog.add_button("Add anyway", move |c| { let mut playlist = playlist.clone(); let spotify = spotify.clone(); let library = library.clone(); playlist.append_tracks(&[Playable::Track(track.clone())], spotify, library); c.pop_layer(); // Close add_track_dialog too c.pop_layer(); }); let modal = Modal::new(already_added_dialog); s.add_layer(modal); } else { playlist.append_tracks(&[Playable::Track(track)], spotify, library); s.pop_layer(); } }); let dialog = Dialog::new() .title("Add track to playlist") .dismiss_button("Close") .padding(Margins::lrtb(1, 1, 1, 0)) .content(ScrollView::new(list_select.with_name("addplaylist_select"))); AddToPlaylistMenu { dialog: Modal::new_ext(dialog), } .with_name("addtrackmenu") } pub fn select_artist_dialog( library: Arc, queue: Arc, artists: Vec, ) -> NamedView { let mut artist_select = SelectView::::new(); for artist in artists { artist_select.add_item(artist.name.clone(), artist); } artist_select.set_on_submit(move |s, selected_artist| { let dialog = Self::select_artist_action_dialog( library.clone(), queue.clone(), selected_artist.clone(), ); s.pop_layer(); s.add_layer(dialog); }); let dialog = Dialog::new() .title("Select artist") .dismiss_button("Close") .padding(Margins::lrtb(1, 1, 1, 0)) .content(ScrollView::new(artist_select.with_name("artist_select"))); SelectArtistMenu { dialog: Modal::new_ext(dialog), } .with_name("selectartist") } pub fn select_artist_action_dialog( library: Arc, queue: Arc, artist: Artist, ) -> NamedView { let moved_artist = artist.clone(); let mut artist_action_select = SelectView::::new(); artist_action_select.add_item("View Artist", true); artist_action_select.add_item( format!( "{}ollow Artist", if library.is_followed_artist(&artist) { "Unf" } else { "F" } ), false, ); artist_action_select.set_on_submit(move |s, selected_action| { match selected_action { true => { if let Some(view) = moved_artist.clone().open(queue.clone(), library.clone()) { s.call_on_name("main", |v: &mut Layout| v.push_view(view)); } } false => { if library.clone().is_followed_artist(&moved_artist) { moved_artist.clone().unsave(library.clone()); } else { moved_artist.clone().save(library.clone()); } } } s.pop_layer(); }); let dialog = Dialog::new() .title(format!( "Select action for artist: {}", artist.name.as_str() )) .dismiss_button("Close") .padding(Margins::lrtb(1, 1, 1, 0)) .content(ScrollView::new( artist_action_select.with_name("artist_action_select"), )); SelectArtistActionMenu { dialog: Modal::new_ext(dialog), } .with_name("selectartistaction") } fn track_already_added() -> Dialog { Dialog::text("This track is already in your playlist") .title("Track already exists") .padding(Margins::lrtb(1, 1, 1, 0)) .dismiss_button("Close") } pub fn new(item: &dyn ListItem, queue: Arc, library: Arc) -> NamedView { let mut content: SelectView = SelectView::new(); if item.is_playable() { if item.is_playing(queue.clone()) && queue.get_spotify().get_current_status() == PlayerEvent::Paused(queue.get_spotify().get_current_progress()) { // the item is the current track, but paused content.insert_item(0, "Resume", ContextMenuAction::TogglePlayback); } else if !item.is_playing(queue.clone()) { // the item is not the current track content.insert_item(0, "Play", ContextMenuAction::Play(item.as_listitem())); } else { // the item is the current track and playing content.insert_item(0, "Pause", ContextMenuAction::TogglePlayback); } content.insert_item( 1, "Play next", ContextMenuAction::PlayNext(item.as_listitem()), ); content.insert_item(2, "Queue", ContextMenuAction::Queue(item.as_listitem())); } if let Some(artists) = item.artists() { let action = match artists.len() { 0 => None, 1 => Some(ContextMenuAction::SelectArtistAction(artists[0].clone())), _ => Some(ContextMenuAction::SelectArtist(artists.clone())), }; if let Some(a) = action { content.add_item( format!("Artist{}", if artists.len() > 1 { "s" } else { "" }), a, ) } } if let Some(a) = item.album(queue.clone()) { content.add_item("Show album", ContextMenuAction::ShowItem(Box::new(a))); } #[cfg(feature = "share_clipboard")] { if let Some(url) = item.share_url() { content.add_item("Share", ContextMenuAction::ShareUrl(url)); } if let Some(url) = item.album(queue.clone()).and_then(|a| a.share_url()) { content.add_item("Share album", ContextMenuAction::ShareUrl(url)); } } if let Some(t) = item.track() { content.add_item( "Add to playlist", ContextMenuAction::AddToPlaylist(Box::new(t.clone())), ); content.add_item( "Similar tracks", ContextMenuAction::ShowRecommendations(Box::new(t)), ) } // If the item is saveable, its save state will be set if let Some(savestatus) = item.is_saved(library.clone()) { content.add_item( match savestatus { true => "Unsave", false => "Save", }, ContextMenuAction::ToggleSavedStatus(item.as_listitem()), ); } // open detail view of artist/album { let library = library.clone(); content.set_on_submit(move |s: &mut Cursive, action: &ContextMenuAction| { let queue = queue.clone(); let library = library.clone(); s.pop_layer(); match action { ContextMenuAction::ShowItem(item) => { if let Some(view) = item.open(queue, library) { s.call_on_name("main", move |v: &mut Layout| v.push_view(view)); } } #[cfg(feature = "share_clipboard")] ContextMenuAction::ShareUrl(url) => { write_share(url.to_string()); } ContextMenuAction::AddToPlaylist(track) => { let dialog = Self::add_track_dialog(library, queue.get_spotify(), *track.clone()); s.add_layer(dialog); } ContextMenuAction::ShowRecommendations(item) => { if let Some(view) = item.to_owned().open_recommendations(queue, library) { s.call_on_name("main", move |v: &mut Layout| v.push_view(view)); } } ContextMenuAction::SelectArtist(artists) => { let dialog = Self::select_artist_dialog(library, queue, artists.clone()); s.add_layer(dialog); } ContextMenuAction::SelectArtistAction(artist) => { let dialog = Self::select_artist_action_dialog(library, queue, artist.clone()); s.add_layer(dialog); } ContextMenuAction::ToggleSavedStatus(item) => { item.as_listitem().toggle_saved(library) } ContextMenuAction::Play(item) => item.as_listitem().play(queue), ContextMenuAction::PlayNext(item) => item.as_listitem().play_next(queue), ContextMenuAction::TogglePlayback => queue.toggleplayback(), ContextMenuAction::Queue(item) => item.as_listitem().queue(queue), } }); } let dialog = Dialog::new() .title(item.display_left(library)) .dismiss_button("Close") .padding(Margins::lrtb(1, 1, 1, 0)) .content(content.with_name("contextmenu_select")); Self { dialog: Modal::new_ext(dialog), } .with_name("contextmenu") } } impl ViewExt for PlayTrackMenu { fn on_command(&mut self, s: &mut Cursive, cmd: &Command) -> Result { log::info!("playtrack command: {:?}", cmd); handle_move_command::(&mut self.dialog, s, cmd, "playtrack_select") } } impl ViewExt for AddToPlaylistMenu { fn on_command(&mut self, s: &mut Cursive, cmd: &Command) -> Result { log::info!("playlist command: {:?}", cmd); handle_move_command::(&mut self.dialog, s, cmd, "addplaylist_select") } } impl ViewExt for ContextMenu { fn on_command(&mut self, s: &mut Cursive, cmd: &Command) -> Result { handle_move_command::(&mut self.dialog, s, cmd, "contextmenu_select") } } impl ViewExt for SelectArtistMenu { fn on_command(&mut self, s: &mut Cursive, cmd: &Command) -> Result { log::info!("artist move command: {:?}", cmd); handle_move_command::(&mut self.dialog, s, cmd, "artist_select") } } impl ViewExt for SelectArtistActionMenu { fn on_command(&mut self, s: &mut Cursive, cmd: &Command) -> Result { handle_move_command::(&mut self.dialog, s, cmd, "artist_action_select") } } fn handle_move_command( sel: &mut Modal, s: &mut Cursive, cmd: &Command, name: &str, ) -> Result { match cmd { Command::Back => { s.pop_layer(); Ok(CommandResult::Consumed(None)) } Command::Move(_, _) => sel .call_on_name(name, |select: &mut SelectView| { select.handle_command(cmd) }) .unwrap_or(Ok(CommandResult::Consumed(None))), _ => Ok(CommandResult::Consumed(None)), } } impl ViewWrapper for PlayTrackMenu { wrap_impl!(self.dialog: Modal); } impl ViewWrapper for AddToPlaylistMenu { wrap_impl!(self.dialog: Modal); } impl ViewWrapper for ContextMenu { wrap_impl!(self.dialog: Modal); } impl ViewWrapper for SelectArtistMenu { wrap_impl!(self.dialog: Modal); } impl ViewWrapper for SelectArtistActionMenu { wrap_impl!(self.dialog: Modal); }