Improve context menus to make the UX/UI more consistent (#923)

* 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
This commit is contained in:
Thomas
2022-09-20 13:09:51 -07:00
committed by GitHub
parent ccbc382144
commit 0eedc38b8a
8 changed files with 125 additions and 55 deletions

View File

@@ -302,6 +302,16 @@ impl ListItem for Album {
) )
} }
#[inline]
fn is_saved(&self, library: Arc<Library>) -> Option<bool> {
Some(library.is_saved_album(self))
}
#[inline]
fn is_playable(&self) -> bool {
true
}
fn as_listitem(&self) -> Box<dyn ListItem> { fn as_listitem(&self) -> Box<dyn ListItem> {
Box::new(self.clone()) Box::new(self.clone())
} }

View File

@@ -202,6 +202,16 @@ impl ListItem for Artist {
.map(|id| format!("https://open.spotify.com/artist/{}", id)) .map(|id| format!("https://open.spotify.com/artist/{}", id))
} }
#[inline]
fn is_saved(&self, library: Arc<Library>) -> Option<bool> {
Some(library.is_followed_artist(self))
}
#[inline]
fn is_playable(&self) -> bool {
true
}
fn as_listitem(&self) -> Box<dyn ListItem> { fn as_listitem(&self) -> Box<dyn ListItem> {
Box::new(self.clone()) Box::new(self.clone())
} }

View File

@@ -110,6 +110,11 @@ impl ListItem for Episode {
Some(format!("https://open.spotify.com/episode/{}", self.id)) Some(format!("https://open.spotify.com/episode/{}", self.id))
} }
#[inline]
fn is_playable(&self) -> bool {
true
}
fn as_listitem(&self) -> Box<dyn ListItem> { fn as_listitem(&self) -> Box<dyn ListItem> {
Box::new(self.clone()) Box::new(self.clone())
} }

View File

@@ -324,6 +324,20 @@ impl ListItem for Playlist {
)) ))
} }
fn is_saved(&self, library: Arc<Library>) -> Option<bool> {
// save status of personal playlists can't be toggled for safety
if !library.is_followed_playlist(self) {
return None;
}
Some(library.is_saved_playlist(self))
}
#[inline]
fn is_playable(&self) -> bool {
true
}
fn as_listitem(&self) -> Box<dyn ListItem> { fn as_listitem(&self) -> Box<dyn ListItem> {
Box::new(self.clone()) Box::new(self.clone())
} }

View File

@@ -150,6 +150,16 @@ impl ListItem for Show {
Some(format!("https://open.spotify.com/show/{}", self.id)) Some(format!("https://open.spotify.com/show/{}", self.id))
} }
#[inline]
fn is_saved(&self, library: Arc<Library>) -> Option<bool> {
Some(library.is_saved_show(self))
}
#[inline]
fn is_playable(&self) -> bool {
true
}
fn as_listitem(&self) -> Box<dyn ListItem> { fn as_listitem(&self) -> Box<dyn ListItem> {
Box::new(self.clone()) Box::new(self.clone())
} }

View File

@@ -333,6 +333,16 @@ impl ListItem for Track {
Some(self.clone()) Some(self.clone())
} }
#[inline]
fn is_saved(&self, library: Arc<Library>) -> Option<bool> {
Some(library.is_saved_track(&Playable::Track(self.clone())))
}
#[inline]
fn is_playable(&self) -> bool {
true
}
fn as_listitem(&self) -> Box<dyn ListItem> { fn as_listitem(&self) -> Box<dyn ListItem> {
Box::new(self.clone()) Box::new(self.clone())
} }

View File

@@ -47,6 +47,17 @@ pub trait ListItem: Sync + Send + 'static {
None None
} }
#[allow(unused_variables)]
#[inline]
fn is_saved(&self, library: Arc<Library>) -> Option<bool> {
None
}
#[inline]
fn is_playable(&self) -> bool {
false
}
fn as_listitem(&self) -> Box<dyn ListItem>; fn as_listitem(&self) -> Box<dyn ListItem>;
} }

View File

@@ -1,4 +1,3 @@
use std::borrow::Borrow;
use std::sync::Arc; use std::sync::Arc;
use cursive::view::{Margins, ViewWrapper}; use cursive::view::{Margins, ViewWrapper};
@@ -15,6 +14,7 @@ use crate::model::track::Track;
use crate::queue::Queue; use crate::queue::Queue;
#[cfg(feature = "share_clipboard")] #[cfg(feature = "share_clipboard")]
use crate::sharing::write_share; use crate::sharing::write_share;
use crate::spotify::PlayerEvent;
use crate::traits::{ListItem, ViewExt}; use crate::traits::{ListItem, ViewExt};
use crate::ui::layout::Layout; use crate::ui::layout::Layout;
use crate::ui::modal::Modal; use crate::ui::modal::Modal;
@@ -42,7 +42,6 @@ pub struct SelectArtistActionMenu {
} }
enum ContextMenuAction { enum ContextMenuAction {
PlayTrack(Box<Track>),
ShowItem(Box<dyn ListItem>), ShowItem(Box<dyn ListItem>),
SelectArtist(Vec<Artist>), SelectArtist(Vec<Artist>),
SelectArtistAction(Artist), SelectArtistAction(Artist),
@@ -50,36 +49,14 @@ enum ContextMenuAction {
ShareUrl(String), ShareUrl(String),
AddToPlaylist(Box<Track>), AddToPlaylist(Box<Track>),
ShowRecommendations(Box<Track>), ShowRecommendations(Box<Track>),
ToggleTrackSavedStatus(Box<Track>), ToggleSavedStatus(Box<dyn ListItem>),
Play(Box<dyn ListItem>),
PlayNext(Box<dyn ListItem>),
TogglePlayback,
Queue(Box<dyn ListItem>),
} }
impl ContextMenu { impl ContextMenu {
pub fn play_track_dialog(queue: Arc<Queue>, track: Track) -> NamedView<PlayTrackMenu> {
let track_title = track.title.clone();
let mut track_action_select = SelectView::<bool>::new();
track_action_select.add_item("Play now", true);
track_action_select.add_item("Add to queue", false);
track_action_select.set_on_submit(move |s, selected| {
match selected {
true => track.borrow().clone().play(queue.clone()),
false => track.borrow().clone().queue(queue.clone()),
}
s.pop_layer();
});
let dialog = Dialog::new()
.title(format!("Play track: {}", track_title))
.dismiss_button("Cancel")
.padding(Margins::lrtb(1, 1, 1, 0))
.content(ScrollView::new(
track_action_select.with_name("playtrack_select"),
));
PlayTrackMenu {
dialog: Modal::new_ext(dialog),
}
.with_name("playtrackmenu")
}
pub fn add_track_dialog( pub fn add_track_dialog(
library: Arc<Library>, library: Arc<Library>,
spotify: Spotify, spotify: Spotify,
@@ -126,7 +103,7 @@ impl ContextMenu {
let dialog = Dialog::new() let dialog = Dialog::new()
.title("Add track to playlist") .title("Add track to playlist")
.dismiss_button("Cancel") .dismiss_button("Close")
.padding(Margins::lrtb(1, 1, 1, 0)) .padding(Margins::lrtb(1, 1, 1, 0))
.content(ScrollView::new(list_select.with_name("addplaylist_select"))); .content(ScrollView::new(list_select.with_name("addplaylist_select")));
@@ -159,7 +136,7 @@ impl ContextMenu {
let dialog = Dialog::new() let dialog = Dialog::new()
.title("Select artist") .title("Select artist")
.dismiss_button("Cancel") .dismiss_button("Close")
.padding(Margins::lrtb(1, 1, 1, 0)) .padding(Margins::lrtb(1, 1, 1, 0))
.content(ScrollView::new(artist_select.with_name("artist_select"))); .content(ScrollView::new(artist_select.with_name("artist_select")));
@@ -210,7 +187,7 @@ impl ContextMenu {
"Select action for artist: {}", "Select action for artist: {}",
artist.name.as_str() artist.name.as_str()
)) ))
.dismiss_button("Cancel") .dismiss_button("Close")
.padding(Margins::lrtb(1, 1, 1, 0)) .padding(Margins::lrtb(1, 1, 1, 0))
.content(ScrollView::new( .content(ScrollView::new(
artist_action_select.with_name("artist_action_select"), artist_action_select.with_name("artist_action_select"),
@@ -225,11 +202,34 @@ impl ContextMenu {
Dialog::text("This track is already in your playlist") Dialog::text("This track is already in your playlist")
.title("Track already exists") .title("Track already exists")
.padding(Margins::lrtb(1, 1, 1, 0)) .padding(Margins::lrtb(1, 1, 1, 0))
.dismiss_button("Cancel") .dismiss_button("Close")
} }
pub fn new(item: &dyn ListItem, queue: Arc<Queue>, library: Arc<Library>) -> NamedView<Self> { pub fn new(item: &dyn ListItem, queue: Arc<Queue>, library: Arc<Library>) -> NamedView<Self> {
let mut content: SelectView<ContextMenuAction> = SelectView::new(); 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() { if let Some(artists) = item.artists() {
let action = match artists.len() { let action = match artists.len() {
0 => None, 0 => None,
@@ -244,9 +244,11 @@ impl ContextMenu {
) )
} }
} }
if let Some(a) = item.album(queue.clone()) { if let Some(a) = item.album(queue.clone()) {
content.add_item("Show album", ContextMenuAction::ShowItem(Box::new(a))); content.add_item("Show album", ContextMenuAction::ShowItem(Box::new(a)));
} }
#[cfg(feature = "share_clipboard")] #[cfg(feature = "share_clipboard")]
{ {
if let Some(url) = item.share_url() { if let Some(url) = item.share_url() {
@@ -256,28 +258,27 @@ impl ContextMenu {
content.add_item("Share album", ContextMenuAction::ShareUrl(url)); content.add_item("Share album", ContextMenuAction::ShareUrl(url));
} }
} }
if let Some(t) = item.track() { if let Some(t) = item.track() {
content.insert_item(
0,
"Play track",
ContextMenuAction::PlayTrack(Box::new(t.clone())),
);
content.add_item( content.add_item(
"Add to playlist", "Add to playlist",
ContextMenuAction::AddToPlaylist(Box::new(t.clone())), ContextMenuAction::AddToPlaylist(Box::new(t.clone())),
); );
content.add_item( content.add_item(
"Similar tracks", "Similar tracks",
ContextMenuAction::ShowRecommendations(Box::new(t.clone())), ContextMenuAction::ShowRecommendations(Box::new(t)),
);
content.add_item(
match library.is_saved_track(&Playable::Track(t.clone())) {
true => "Unsave track",
false => "Save track",
},
ContextMenuAction::ToggleTrackSavedStatus(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 // open detail view of artist/album
{ {
@@ -288,10 +289,6 @@ impl ContextMenu {
s.pop_layer(); s.pop_layer();
match action { match action {
ContextMenuAction::PlayTrack(track) => {
let dialog = Self::play_track_dialog(queue, *track.clone());
s.add_layer(dialog);
}
ContextMenuAction::ShowItem(item) => { ContextMenuAction::ShowItem(item) => {
if let Some(view) = item.open(queue, library) { if let Some(view) = item.open(queue, library) {
s.call_on_name("main", move |v: &mut Layout| v.push_view(view)); s.call_on_name("main", move |v: &mut Layout| v.push_view(view));
@@ -311,10 +308,6 @@ impl ContextMenu {
s.call_on_name("main", move |v: &mut Layout| v.push_view(view)); 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) => { ContextMenuAction::SelectArtist(artists) => {
let dialog = Self::select_artist_dialog(library, queue, artists.clone()); let dialog = Self::select_artist_dialog(library, queue, artists.clone());
s.add_layer(dialog); s.add_layer(dialog);
@@ -324,13 +317,20 @@ impl ContextMenu {
Self::select_artist_action_dialog(library, queue, artist.clone()); Self::select_artist_action_dialog(library, queue, artist.clone());
s.add_layer(dialog); 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() let dialog = Dialog::new()
.title(item.display_left(library)) .title(item.display_left(library))
.dismiss_button("Cancel") .dismiss_button("Close")
.padding(Margins::lrtb(1, 1, 1, 0)) .padding(Margins::lrtb(1, 1, 1, 0))
.content(content.with_name("contextmenu_select")); .content(content.with_name("contextmenu_select"));
Self { Self {