use std::sync::Arc; use cursive::view::{Margins, ViewWrapper}; use cursive::views::{Dialog, NamedView, ScrollView, SelectView}; use cursive::Cursive; use crate::library::Library; use crate::playable::Playable; use crate::queue::Queue; #[cfg(feature = "share_clipboard")] use crate::sharing::write_share; use crate::track::Track; use crate::traits::{ListItem, ViewExt}; use crate::ui::layout::Layout; use crate::ui::modal::Modal; use crate::{artist::Artist, commands::CommandResult}; use crate::{ command::{Command, MoveAmount, MoveMode}, playlist::Playlist, spotify::Spotify, }; use cursive::traits::{Finder, Nameable}; pub struct ContextMenu { dialog: Modal, } pub struct AddToPlaylistMenu { dialog: Modal, } pub struct SelectArtistMenu { dialog: Modal, } enum ContextMenuAction { ShowItem(Box), SelectArtist(Vec), ShareUrl(String), AddToPlaylist(Box), ShowRecommendations(Track), ToggleTrackSavedStatus(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_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("Cancel") .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 list_select = SelectView::::new(); for artist in artists { list_select.add_item(artist.name.clone(), artist); } list_select.set_on_submit(move |s, selected| { if let Some(view) = selected.open(queue.clone(), library.clone()) { s.call_on_name("main", move |v: &mut Layout| v.push_view(view)); s.pop_layer(); } }); let dialog = Dialog::new() .title("Select artist") .dismiss_button("Cancel") .padding(Margins::lrtb(1, 1, 1, 0)) .content(ScrollView::new(list_select.with_name("artist_select"))); SelectArtistMenu { dialog: Modal::new_ext(dialog), } .with_name("selectartist") } 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("Cancel") } pub fn new(item: &dyn ListItem, queue: Arc, library: Arc) -> NamedView { let mut content: SelectView = SelectView::new(); if let Some(a) = item.artists() { let action = match a.len() { 0 => None, 1 => Some(ContextMenuAction::ShowItem(Box::new(a[0].clone()))), _ => Some(ContextMenuAction::SelectArtist(a)), }; if let Some(a) = action { content.add_item("Show artist", a) } } if let Some(a) = item.album(queue.clone()) { content.add_item("Show album", ContextMenuAction::ShowItem(Box::new(a))); } if let Some(url) = item.share_url() { #[cfg(feature = "share_clipboard")] content.add_item("Share", ContextMenuAction::ShareUrl(url)); } if let Some(url) = item.album(queue.clone()).and_then(|a| a.share_url()) { #[cfg(feature = "share_clipboard")] 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(t.clone()), ); content.add_item( match library.is_saved_track(&Playable::Track(t.clone())) { true => "Unsave track", false => "Save track", }, ContextMenuAction::ToggleTrackSavedStatus(Box::new(t)), ) } // open detail view of artist/album content.set_on_submit(move |s: &mut Cursive, action: &ContextMenuAction| { s.pop_layer(); let queue = queue.clone(); let library = library.clone(); 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)); } } ContextMenuAction::ShareUrl(url) => { #[cfg(feature = "share_clipboard")] 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::ToggleTrackSavedStatus(track) => { let mut track: Track = *track.clone(); track.toggle_saved(library); } ContextMenuAction::SelectArtist(artists) => { let dialog = Self::select_artist_dialog(library, queue, artists.clone()); s.add_layer(dialog); } } }); let dialog = Dialog::new() .title(item.display_left()) .dismiss_button("Cancel") .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 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") } } 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(mode, amount) => sel .call_on_name(name, |select: &mut SelectView| { let items = select.len(); match mode { MoveMode::Up => { match amount { MoveAmount::Extreme => select.set_selection(0), MoveAmount::Integer(amount) => select.select_up(*amount as usize), }; Ok(CommandResult::Consumed(None)) } MoveMode::Down => { match amount { MoveAmount::Extreme => select.set_selection(items), MoveAmount::Integer(amount) => select.select_down(*amount as usize), }; Ok(CommandResult::Consumed(None)) } _ => Ok(CommandResult::Consumed(None)), } }) .unwrap_or(Ok(CommandResult::Consumed(None))), _ => Ok(CommandResult::Consumed(None)), } } 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); }