diff --git a/.gitignore b/.gitignore index ac72d99..aaaf735 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Generated by Cargo # will have compiled files and executables /target/ +/.idea/ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html diff --git a/Cargo.toml b/Cargo.toml index 6750d87..b5d53b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ unicode-width = "0.1.5" dbus = { version = "0.6.4", optional = true } rand = "0.6.5" webbrowser = "0.5" +clipboard = "0.5.0" [dependencies.rspotify] git = "https://github.com/samrayleung/rspotify" diff --git a/README.md b/README.md index aa617f7..e969fae 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,8 @@ have them configurable. * `r` to toggle repeat mode * `z` to toggle shuffle playback * `q` quits ncspot +* `x` copies a sharable URL to the song to the system clipboard +* `Shift-x` copies a sharable URL to the currently selected item to the system clipboard You can also open a Vim style commandprompt using `:`, the following commands are supported: @@ -65,6 +67,7 @@ are supported: * `stop`: Stop playback * `previous`/`next`: Play previous/next track * `clear`: Clear playlist +* `share [current | selected]`: Copies a sharable URL of either the selected item or the currernt song to the system clipboard The screens can be opened with `queue`, `search`, `playlists` and `log`, whereas `search` can be supplied with a search term that will be entered after opening diff --git a/src/album.rs b/src/album.rs index 9d5699b..93016d4 100644 --- a/src/album.rs +++ b/src/album.rs @@ -199,6 +199,12 @@ impl ListItem for Album { Some(AlbumView::new(queue, library, self).as_boxed_view_ext()) } + fn share_url(&self) -> Option { + self.id + .clone() + .map(|id| format!("https://open.spotify.com/album/{}", id)) + } + fn artist(&self) -> Option { Some(Artist::new( self.artist_ids[0].clone(), diff --git a/src/artist.rs b/src/artist.rs index 698bf2d..603ab24 100644 --- a/src/artist.rs +++ b/src/artist.rs @@ -193,4 +193,10 @@ impl ListItem for Artist { fn open(&self, queue: Arc, library: Arc) -> Option> { Some(ArtistView::new(queue, library, self).as_boxed_view_ext()) } + + fn share_url(&self) -> Option { + self.id + .clone() + .map(|id| format!("https://open.spotify.com/artist/{}", id)) + } } diff --git a/src/commands.rs b/src/commands.rs index 819fc4f..b0662b4 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -284,9 +284,9 @@ impl CommandManager { }); } - pub fn register_keybindings<'a>( + pub fn register_keybindings( this: Arc, - cursive: &'a mut Cursive, + cursive: &mut Cursive, keybindings: Option>, ) { let mut kb = Self::default_keybindings(); @@ -321,6 +321,8 @@ impl CommandManager { kb.insert(",".into(), "seek -500".into()); kb.insert("r".into(), "repeat".into()); kb.insert("z".into(), "shuffle".into()); + kb.insert("x".into(), "share current".into()); + kb.insert("Shift+x".into(), "share selected".into()); kb.insert("F1".into(), "focus queue".into()); kb.insert("F2".into(), "focus search".into()); diff --git a/src/main.rs b/src/main.rs index 78c35f4..e9ff401 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ extern crate clap; extern crate crossbeam_channel; #[macro_use] extern crate cursive; +extern crate clipboard; extern crate directories; extern crate failure; extern crate futures; diff --git a/src/playlist.rs b/src/playlist.rs index bd32498..5682763 100644 --- a/src/playlist.rs +++ b/src/playlist.rs @@ -166,4 +166,11 @@ impl ListItem for Playlist { fn open(&self, queue: Arc, library: Arc) -> Option> { Some(PlaylistView::new(queue, library, self).as_boxed_view_ext()) } + + fn share_url(&self) -> Option { + Some(format!( + "https://open.spotify.com/user/{}/playlist/{}", + self.owner_id, self.id + )) + } } diff --git a/src/track.rs b/src/track.rs index 1267e3b..ecff57b 100644 --- a/src/track.rs +++ b/src/track.rs @@ -189,6 +189,12 @@ impl ListItem for Track { None } + fn share_url(&self) -> Option { + self.id + .clone() + .map(|id| format!("https://open.spotify.com/track/{}", id)) + } + fn album(&self, queue: Arc) -> Option { let spotify = queue.get_spotify(); diff --git a/src/traits.rs b/src/traits.rs index 62bd0a4..aae2c70 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -18,6 +18,7 @@ pub trait ListItem: Sync + Send + 'static { fn queue(&mut self, queue: Arc); fn toggle_saved(&mut self, library: Arc); fn open(&self, queue: Arc, library: Arc) -> Option>; + fn share_url(&self) -> Option; fn album(&self, _queue: Arc) -> Option { None diff --git a/src/ui/listview.rs b/src/ui/listview.rs index 50da0f3..f60c221 100644 --- a/src/ui/listview.rs +++ b/src/ui/listview.rs @@ -9,6 +9,7 @@ use cursive::view::ScrollBase; use cursive::{Cursive, Printer, Rect, Vec2}; use unicode_width::UnicodeWidthStr; +use clipboard::{ClipboardContext, ClipboardProvider}; use commands::CommandResult; use library::Library; use queue::Queue; @@ -215,6 +216,10 @@ impl View for ListView { self.content.read().unwrap().len() != self.last_content_len } + fn required_size(&mut self, constraint: Vec2) -> Vec2 { + Vec2::new(constraint.x, self.content.read().unwrap().len()) + } + fn on_event(&mut self, e: Event) -> EventResult { match e { Event::Mouse { @@ -262,10 +267,6 @@ impl View for ListView { EventResult::Consumed(None) } - fn required_size(&mut self, constraint: Vec2) -> Vec2 { - Vec2::new(constraint.x, self.content.read().unwrap().len()) - } - fn important_area(&self, view_size: Vec2) -> Rect { if self.content.read().unwrap().len() > 0 { Rect::from((view_size.x, self.selected)) @@ -314,6 +315,26 @@ impl ViewExt for ListView { } } + if cmd == "share" { + let source = args.get(0); + let url = + source.and_then(|source| match source.as_str() { + "selected" => self.content.read().ok().and_then(|content| { + content.get(self.selected).and_then(ListItem::share_url) + }), + "current" => self.queue.get_current().and_then(|t| t.share_url()), + _ => None, + }); + + if let Some(url) = url { + ClipboardProvider::new() + .and_then(|mut ctx: ClipboardContext| ctx.set_contents(url)) + .ok(); + }; + + return Ok(CommandResult::Consumed(None)); + } + if cmd == "move" { if let Some(dir) = args.get(0) { let amount: usize = args