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:
@@ -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())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user