* Add save option to context menu of all possible ListItems * Add play options to context menus * Fix for playlists and tracks * Move playback controls into main menu
415 lines
14 KiB
Rust
415 lines
14 KiB
Rust
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<Dialog>,
|
|
}
|
|
|
|
pub struct PlayTrackMenu {
|
|
dialog: Modal<Dialog>,
|
|
}
|
|
|
|
pub struct AddToPlaylistMenu {
|
|
dialog: Modal<Dialog>,
|
|
}
|
|
|
|
pub struct SelectArtistMenu {
|
|
dialog: Modal<Dialog>,
|
|
}
|
|
|
|
pub struct SelectArtistActionMenu {
|
|
dialog: Modal<Dialog>,
|
|
}
|
|
|
|
enum ContextMenuAction {
|
|
ShowItem(Box<dyn ListItem>),
|
|
SelectArtist(Vec<Artist>),
|
|
SelectArtistAction(Artist),
|
|
#[cfg(feature = "share_clipboard")]
|
|
ShareUrl(String),
|
|
AddToPlaylist(Box<Track>),
|
|
ShowRecommendations(Box<Track>),
|
|
ToggleSavedStatus(Box<dyn ListItem>),
|
|
Play(Box<dyn ListItem>),
|
|
PlayNext(Box<dyn ListItem>),
|
|
TogglePlayback,
|
|
Queue(Box<dyn ListItem>),
|
|
}
|
|
|
|
impl ContextMenu {
|
|
pub fn add_track_dialog(
|
|
library: Arc<Library>,
|
|
spotify: Spotify,
|
|
track: Track,
|
|
) -> NamedView<AddToPlaylistMenu> {
|
|
let mut list_select: SelectView<Playlist> = 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<Library>,
|
|
queue: Arc<Queue>,
|
|
artists: Vec<Artist>,
|
|
) -> NamedView<SelectArtistMenu> {
|
|
let mut artist_select = SelectView::<Artist>::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<Library>,
|
|
queue: Arc<Queue>,
|
|
artist: Artist,
|
|
) -> NamedView<SelectArtistActionMenu> {
|
|
let moved_artist = artist.clone();
|
|
let mut artist_action_select = SelectView::<bool>::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<Queue>, library: Arc<Library>) -> NamedView<Self> {
|
|
let mut content: SelectView<ContextMenuAction> = 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<CommandResult, String> {
|
|
log::info!("playtrack command: {:?}", cmd);
|
|
handle_move_command::<bool>(&mut self.dialog, s, cmd, "playtrack_select")
|
|
}
|
|
}
|
|
|
|
impl ViewExt for AddToPlaylistMenu {
|
|
fn on_command(&mut self, s: &mut Cursive, cmd: &Command) -> Result<CommandResult, String> {
|
|
log::info!("playlist command: {:?}", cmd);
|
|
handle_move_command::<Playlist>(&mut self.dialog, s, cmd, "addplaylist_select")
|
|
}
|
|
}
|
|
|
|
impl ViewExt for ContextMenu {
|
|
fn on_command(&mut self, s: &mut Cursive, cmd: &Command) -> Result<CommandResult, String> {
|
|
handle_move_command::<ContextMenuAction>(&mut self.dialog, s, cmd, "contextmenu_select")
|
|
}
|
|
}
|
|
|
|
impl ViewExt for SelectArtistMenu {
|
|
fn on_command(&mut self, s: &mut Cursive, cmd: &Command) -> Result<CommandResult, String> {
|
|
log::info!("artist move command: {:?}", cmd);
|
|
handle_move_command::<Artist>(&mut self.dialog, s, cmd, "artist_select")
|
|
}
|
|
}
|
|
|
|
impl ViewExt for SelectArtistActionMenu {
|
|
fn on_command(&mut self, s: &mut Cursive, cmd: &Command) -> Result<CommandResult, String> {
|
|
handle_move_command::<bool>(&mut self.dialog, s, cmd, "artist_action_select")
|
|
}
|
|
}
|
|
|
|
fn handle_move_command<T: 'static>(
|
|
sel: &mut Modal<Dialog>,
|
|
s: &mut Cursive,
|
|
cmd: &Command,
|
|
name: &str,
|
|
) -> Result<CommandResult, String> {
|
|
match cmd {
|
|
Command::Back => {
|
|
s.pop_layer();
|
|
Ok(CommandResult::Consumed(None))
|
|
}
|
|
Command::Move(_, _) => sel
|
|
.call_on_name(name, |select: &mut SelectView<T>| {
|
|
select.handle_command(cmd)
|
|
})
|
|
.unwrap_or(Ok(CommandResult::Consumed(None))),
|
|
_ => Ok(CommandResult::Consumed(None)),
|
|
}
|
|
}
|
|
|
|
impl ViewWrapper for PlayTrackMenu {
|
|
wrap_impl!(self.dialog: Modal<Dialog>);
|
|
}
|
|
|
|
impl ViewWrapper for AddToPlaylistMenu {
|
|
wrap_impl!(self.dialog: Modal<Dialog>);
|
|
}
|
|
|
|
impl ViewWrapper for ContextMenu {
|
|
wrap_impl!(self.dialog: Modal<Dialog>);
|
|
}
|
|
|
|
impl ViewWrapper for SelectArtistMenu {
|
|
wrap_impl!(self.dialog: Modal<Dialog>);
|
|
}
|
|
|
|
impl ViewWrapper for SelectArtistActionMenu {
|
|
wrap_impl!(self.dialog: Modal<Dialog>);
|
|
}
|