From f7450321dabf4b2cf39ca157201fca11c87d9ed6 Mon Sep 17 00:00:00 2001 From: Bettehem Date: Sat, 28 May 2022 15:12:04 +0300 Subject: [PATCH] Add `track_format` config option (#800) * Added track_name_first config option to allow choosing if artists' names should be shown before or after the track name. * Added active_fields config option, which allows configuration of which columns are visible in Queue/Library view. This also removes the need for a separate track_name_first and album_column option. * Fixed README * Made custom tracklist formatting more flexible. Updated readme with new instructions. Reformatted impl member order to match the definitions in traits.rs. * Added track_name_first config option to allow choosing if artists' names should be shown before or after the track name. * Added active_fields config option, which allows configuration of which columns are visible in Queue/Library view. This also removes the need for a separate track_name_first and album_column option. * Fixed README * Made custom tracklist formatting more flexible. Updated readme with new instructions. Reformatted impl member order to match the definitions in traits.rs. * Fetch formatting config from library config Instead of the lazy static mutex * Moved custom format function to Playable impl as it's a better location to handle both Tracks and Episodes * Rename from `tracklist_formatting` to `track_format` Also shorten `format_{left|center|right}` to `{left|center|right}` Co-authored-by: Henrik Friedrichsen --- README.md | 61 +++++++++++++++++++++++++++++++ src/config.rs | 19 +++++++++- src/model/album.rs | 26 ++++++------- src/model/artist.rs | 26 ++++++------- src/model/episode.rs | 2 +- src/model/playable.rs | 53 ++++++++++++++++++++++++++- src/model/playlist.rs | 26 ++++++------- src/model/show.rs | 2 +- src/model/track.rs | 85 ++++++++++++++++++++++++++++++------------- src/traits.rs | 4 +- src/ui/contextmenu.rs | 75 ++++++++++++++++++++------------------ src/ui/listview.rs | 4 +- 12 files changed, 273 insertions(+), 110 deletions(-) diff --git a/README.md b/README.md index f8c2c45..da74453 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ as the \*BSDs. - [Custom Keybindings](#custom-keybindings) - [Proxy](#proxy) - [Theming](#theming) + - [Track Formatting](#track-formatting) - [Cover Drawing](#cover-drawing) - [Authentication](#authentication) @@ -328,6 +329,7 @@ Possible configuration values are: | `repeat` | Set default repeat mode | `off`, `track`, `playlist` | `off` | | `playback_state` | Set default playback state | `"Stopped"`, `"Paused"`, `"Playing"`, `"Default"` | `"Paused"` | | `library_tabs` | Tabs to show in library screen | Array of `tracks`, `albums`, `artists`, `playlists`, `podcasts` | All tabs | +| `[track_format]` | Set active fields shown in Library/Queue views | See [track formatting](#track-formatting) | | | `[theme]` | Custom theme | See [custom theme](#theming) | | | `[keybindings]` | Custom keybindings | See [custom keybindings](#custom-keybindings) | | @@ -385,6 +387,65 @@ search_match = "light red" More examples can be found in [this pull request](https://github.com/hrkfdn/ncspot/pull/40). +### Track Formatting +It's possible to customize which fields are shown in Queue/Library views. +If you don't define `center` for example, the default value will be used. +Available options for tracks: +`%artists`, `%title`, `%album`, `%saved`, `%duration` +Default configuration: + +```toml +[track_format] +left = "%artists - %title" +center = "%album" +right = "%saved %duration" +``` + +
Examples: (Click to show/hide) + + +Example 1 - Show only album name and track name after it: + +```toml +[track_format] +left = "%album" +center = "%title" +right = "" +``` + +Example 2 - Show track title before artists, and don't show album at all: + +```toml +[track_format] +left = "%title - %artists" +center = "" +``` + +Example 3 - Show everything as default, but hide saved status and track length: + +```toml +[track_format] +right = "" +``` + +Example 4 - Show everything as default, except show title before artists: + +```toml +[track_format] +left = "%title - %artists" +``` + +Example 5 - Show saved status and duration first, followed by track title and artists, with the album last: + +```toml +[track_format] +left = "|%saved| %duration | %title - %artists" +center = "" +right = "%album" +``` + +
+ ## Cover Drawing When compiled with the `cover` feature, `ncspot` can draw the album art of the diff --git a/src/config.rs b/src/config.rs index 100701f..0d28707 100644 --- a/src/config.rs +++ b/src/config.rs @@ -33,6 +33,23 @@ pub enum LibraryTab { Podcasts, } +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +pub struct TrackFormat { + pub left: Option, + pub center: Option, + pub right: Option, +} + +impl TrackFormat { + pub fn default() -> Self { + TrackFormat { + left: Some(String::from("%artists - %title")), + center: Some(String::from("%album")), + right: Some(String::from("%saved %duration")), + } + } +} + #[derive(Clone, Serialize, Deserialize, Debug, Default)] pub struct ConfigValues { pub command_key: Option, @@ -50,12 +67,12 @@ pub struct ConfigValues { pub volnorm_pregain: Option, pub notify: Option, pub bitrate: Option, - pub album_column: Option, pub gapless: Option, pub shuffle: Option, pub repeat: Option, pub cover_max_scale: Option, pub playback_state: Option, + pub track_format: Option, pub library_tabs: Option>, } diff --git a/src/model/album.rs b/src/model/album.rs index 4bf3636..37e5fc2 100644 --- a/src/model/album.rs +++ b/src/model/album.rs @@ -165,11 +165,7 @@ impl ListItem for Album { } } - fn as_listitem(&self) -> Box { - Box::new(self.clone()) - } - - fn display_left(&self) -> String { + fn display_left(&self, _library: Arc) -> String { format!("{}", self) } @@ -219,14 +215,6 @@ impl ListItem for Album { } } - fn save(&mut self, library: Arc) { - library.save_album(self); - } - - fn unsave(&mut self, library: Arc) { - library.unsave_album(self); - } - fn toggle_saved(&mut self, library: Arc) { if library.is_saved_album(self) { library.unsave_album(self); @@ -235,6 +223,14 @@ impl ListItem for Album { } } + fn save(&mut self, library: Arc) { + library.save_album(self); + } + + fn unsave(&mut self, library: Arc) { + library.unsave_album(self); + } + fn open(&self, queue: Arc, library: Arc) -> Option> { Some(AlbumView::new(queue, library, self).into_boxed_view_ext()) } @@ -301,4 +297,8 @@ impl ListItem for Album { .collect(), ) } + + fn as_listitem(&self) -> Box { + Box::new(self.clone()) + } } diff --git a/src/model/artist.rs b/src/model/artist.rs index 4ac4c45..8028e64 100644 --- a/src/model/artist.rs +++ b/src/model/artist.rs @@ -94,11 +94,7 @@ impl ListItem for Artist { } } - fn as_listitem(&self) -> Box { - Box::new(self.clone()) - } - - fn display_left(&self) -> String { + fn display_left(&self, _library: Arc) -> String { format!("{}", self) } @@ -155,14 +151,6 @@ impl ListItem for Artist { } } - fn save(&mut self, library: Arc) { - library.follow_artist(self); - } - - fn unsave(&mut self, library: Arc) { - library.unfollow_artist(self); - } - fn toggle_saved(&mut self, library: Arc) { if library.is_followed_artist(self) { library.unfollow_artist(self); @@ -171,6 +159,14 @@ impl ListItem for Artist { } } + fn save(&mut self, library: Arc) { + library.follow_artist(self); + } + + fn unsave(&mut self, library: Arc) { + library.unfollow_artist(self); + } + fn open(&self, queue: Arc, library: Arc) -> Option> { Some(ArtistView::new(queue, library, self).into_boxed_view_ext()) } @@ -205,4 +201,8 @@ impl ListItem for Artist { .clone() .map(|id| format!("https://open.spotify.com/artist/{}", id)) } + + fn as_listitem(&self) -> Box { + Box::new(self.clone()) + } } diff --git a/src/model/episode.rs b/src/model/episode.rs index 3bdab08..90208a2 100644 --- a/src/model/episode.rs +++ b/src/model/episode.rs @@ -75,7 +75,7 @@ impl ListItem for Episode { .unwrap_or(false) } - fn display_left(&self) -> String { + fn display_left(&self, _library: Arc) -> String { self.name.clone() } diff --git a/src/model/playable.rs b/src/model/playable.rs index b887f20..1977cff 100644 --- a/src/model/playable.rs +++ b/src/model/playable.rs @@ -19,6 +19,55 @@ pub enum Playable { } impl Playable { + pub fn format(playable: Playable, formatting: String, library: Arc) -> String { + formatting + .replace( + "%artists", + if let Some(artists) = playable.artists() { + artists + .iter() + .map(|artist| artist.clone().name) + .collect::>() + .join(", ") + } else { + String::new() + } + .as_str(), + ) + .replace( + "%title", + match playable.clone() { + Playable::Episode(episode) => episode.name, + Playable::Track(track) => track.title, + } + .as_str(), + ) + .replace( + "%album", + match playable.clone() { + Playable::Track(track) => track.album.unwrap_or_default(), + _ => String::new(), + } + .as_str(), + ) + .replace( + "%saved", + if library.is_saved_track(&match playable.clone() { + Playable::Episode(episode) => Playable::Episode(episode), + Playable::Track(track) => Playable::Track(track), + }) { + if library.cfg.values().use_nerdfont.unwrap_or_default() { + "\u{f62b}" + } else { + "✓" + } + } else { + "" + }, + ) + .replace("%duration", playable.duration_str().as_str()) + } + pub fn id(&self) -> Option { match self { Playable::Track(track) => track.id.clone(), @@ -106,8 +155,8 @@ impl ListItem for Playable { self.as_listitem().is_playing(queue) } - fn display_left(&self) -> String { - self.as_listitem().display_left() + fn display_left(&self, library: Arc) -> String { + self.as_listitem().display_left(library) } fn display_center(&self, library: Arc) -> String { diff --git a/src/model/playlist.rs b/src/model/playlist.rs index 4804a66..cdf44a6 100644 --- a/src/model/playlist.rs +++ b/src/model/playlist.rs @@ -191,11 +191,7 @@ impl ListItem for Playlist { } } - fn as_listitem(&self) -> Box { - Box::new(self.clone()) - } - - fn display_left(&self) -> String { + fn display_left(&self, _library: Arc) -> String { match self.owner_name.as_ref() { Some(owner) => format!("{} • {}", self.name, owner), None => self.name.clone(), @@ -251,14 +247,6 @@ impl ListItem for Playlist { } } - fn save(&mut self, library: Arc) { - library.follow_playlist(self); - } - - fn unsave(&mut self, library: Arc) { - library.delete_playlist(&self.id); - } - fn toggle_saved(&mut self, library: Arc) { // Don't allow users to unsave their own playlists with one keypress if !library.is_followed_playlist(self) { @@ -272,6 +260,14 @@ impl ListItem for Playlist { } } + fn save(&mut self, library: Arc) { + library.follow_playlist(self); + } + + fn unsave(&mut self, library: Arc) { + library.delete_playlist(&self.id); + } + fn open(&self, queue: Arc, library: Arc) -> Option> { Some(PlaylistView::new(queue, library, self).into_boxed_view_ext()) } @@ -326,4 +322,8 @@ impl ListItem for Playlist { self.owner_id, self.id )) } + + fn as_listitem(&self) -> Box { + Box::new(self.clone()) + } } diff --git a/src/model/show.rs b/src/model/show.rs index 94240f5..9dcabd7 100644 --- a/src/model/show.rs +++ b/src/model/show.rs @@ -76,7 +76,7 @@ impl ListItem for Show { false } - fn display_left(&self) -> String { + fn display_left(&self, _library: Arc) -> String { format!("{}", self) } diff --git a/src/model/track.rs b/src/model/track.rs index 3e32ff9..13ba762 100644 --- a/src/model/track.rs +++ b/src/model/track.rs @@ -1,6 +1,7 @@ use std::fmt; use std::sync::{Arc, RwLock}; +use crate::config; use chrono::{DateTime, Utc}; use rspotify::model::album::FullAlbum; use rspotify::model::track::{FullTrack, SavedTrack, SimplifiedTrack}; @@ -181,33 +182,61 @@ impl ListItem for Track { current.map(|t| t.id() == self.id).unwrap_or(false) } - fn as_listitem(&self) -> Box { - Box::new(self.clone()) - } - - fn display_left(&self) -> String { - format!("{}", self) + fn display_left(&self, library: Arc) -> String { + let formatting = library + .cfg + .values() + .track_format + .clone() + .unwrap_or_default(); + let default = config::TrackFormat::default().left.unwrap(); + let left = formatting.left.unwrap_or_else(|| default.clone()); + if left != default { + Playable::format(Playable::Track(self.clone()), left, library) + } else { + format!("{}", self) + } } fn display_center(&self, library: Arc) -> String { - if library.cfg.values().album_column.unwrap_or(true) { - self.album.clone().unwrap_or_default() + let formatting = library + .cfg + .values() + .track_format + .clone() + .unwrap_or_default(); + let default = config::TrackFormat::default().center.unwrap(); + let center = formatting.center.unwrap_or_else(|| default.clone()); + if center != default { + Playable::format(Playable::Track(self.clone()), center, library) } else { - "".to_string() + self.album.clone().unwrap_or_default() } } fn display_right(&self, library: Arc) -> String { - let saved = if library.is_saved_track(&Playable::Track(self.clone())) { - if library.cfg.values().use_nerdfont.unwrap_or(false) { - "\u{f62b} " - } else { - "✓ " - } + let formatting = library + .cfg + .values() + .track_format + .clone() + .unwrap_or_default(); + let default = config::TrackFormat::default().right.unwrap(); + let right = formatting.right.unwrap_or_else(|| default.clone()); + if right != default { + Playable::format(Playable::Track(self.clone()), right, library) } else { - "" - }; - format!("{}{}", saved, self.duration_str()) + let saved = if library.is_saved_track(&Playable::Track(self.clone())) { + if library.cfg.values().use_nerdfont.unwrap_or(false) { + "\u{f62b}" + } else { + "✓" + } + } else { + "" + }; + format!("{} {}", saved, self.duration_str()) + } } fn play(&mut self, queue: Arc) { @@ -223,14 +252,6 @@ impl ListItem for Track { queue.append(Playable::Track(self.clone())); } - fn save(&mut self, library: Arc) { - library.save_tracks(vec![self], true); - } - - fn unsave(&mut self, library: Arc) { - library.unsave_tracks(vec![self], true); - } - fn toggle_saved(&mut self, library: Arc) { if library.is_saved_track(&Playable::Track(self.clone())) { library.unsave_tracks(vec![self], true); @@ -239,6 +260,14 @@ impl ListItem for Track { } } + fn save(&mut self, library: Arc) { + library.save_tracks(vec![self], true); + } + + fn unsave(&mut self, library: Arc) { + library.unsave_tracks(vec![self], true); + } + fn open(&self, _queue: Arc, _library: Arc) -> Option> { None } @@ -303,4 +332,8 @@ impl ListItem for Track { fn track(&self) -> Option { Some(self.clone()) } + + fn as_listitem(&self) -> Box { + Box::new(self.clone()) + } } diff --git a/src/traits.rs b/src/traits.rs index b1139ae..426a0a2 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -14,14 +14,14 @@ use crate::queue::Queue; pub trait ListItem: Sync + Send + 'static { fn is_playing(&self, queue: Arc) -> bool; - fn display_left(&self) -> String; + fn display_left(&self, library: Arc) -> String; fn display_center(&self, _library: Arc) -> String { "".to_string() } fn display_right(&self, library: Arc) -> String; fn play(&mut self, queue: Arc); - fn queue(&mut self, queue: Arc); fn play_next(&mut self, queue: Arc); + fn queue(&mut self, queue: Arc); fn toggle_saved(&mut self, library: Arc); fn save(&mut self, library: Arc); fn unsave(&mut self, library: Arc); diff --git a/src/ui/contextmenu.rs b/src/ui/contextmenu.rs index 1307d28..16bb575 100644 --- a/src/ui/contextmenu.rs +++ b/src/ui/contextmenu.rs @@ -215,48 +215,51 @@ impl ContextMenu { } // 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(); + content.set_on_submit(move |s: &mut Cursive, action: &ContextMenuAction| { + let queue = queue.clone(); + let library = library.clone(); + s.pop_layer(); - match action { - ContextMenuAction::PlayTrack(track) => { - let dialog = Self::play_track_dialog(queue, *track.clone()); - s.add_layer(dialog); - } - ContextMenuAction::ShowItem(item) => { - if let Some(view) = item.open(queue, library) { - s.call_on_name("main", move |v: &mut Layout| v.push_view(view)); + match action { + ContextMenuAction::PlayTrack(track) => { + let dialog = Self::play_track_dialog(queue, *track.clone()); + s.add_layer(dialog); + } + 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); } } - 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()) + .title(item.display_left(library)) .dismiss_button("Cancel") .padding(Margins::lrtb(1, 1, 1, 0)) .content(content.with_name("contextmenu_select")); diff --git a/src/ui/listview.rs b/src/ui/listview.rs index 1f5a3e6..6b5e3f7 100644 --- a/src/ui/listview.rs +++ b/src/ui/listview.rs @@ -108,7 +108,7 @@ impl ListView { .iter() .enumerate() .filter(|(_, i)| { - i.display_left() + i.display_left(self.library.clone()) .to_lowercase() .contains(&query[..].to_lowercase()) }) @@ -189,7 +189,7 @@ impl View for ListView { ColorStyle::primary() }; - let left = item.display_left(); + let left = item.display_left(self.library.clone()); let center = item.display_center(self.library.clone()); let right = item.display_right(self.library.clone()); let draw_center = !center.is_empty();