diff --git a/Cargo.toml b/Cargo.toml index ba11892..1469b86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ serde = "1.0" serde_derive = "1.0" toml = "0.4" tokio-core = "0.1" +unicode-width = "0.1.5" [dependencies.librespot] git = "https://github.com/librespot-org/librespot.git" diff --git a/src/main.rs b/src/main.rs index 8203d0d..a97c39f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ extern crate futures; extern crate librespot; extern crate rspotify; extern crate tokio_core; +extern crate unicode_width; #[macro_use] extern crate serde_derive; diff --git a/src/queue.rs b/src/queue.rs index 52b7e39..1694ae1 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -23,6 +23,7 @@ impl Queue { pub fn remove(&mut self, index: usize) -> Option { match self.queue.remove(index) { Some(track) => { + debug!("Removed from queue: {}", &track.name); self.send_event(); Some(track) } @@ -30,12 +31,14 @@ impl Queue { } } pub fn enqueue(&mut self, track: FullTrack) { + debug!("Queued: {}", &track.name); self.queue.push_back(track); self.send_event(); } pub fn dequeue(&mut self) -> Option { match self.queue.pop_front() { Some(track) => { + debug!("Dequeued : {}", track.name); self.send_event(); Some(track) } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 843100f..2b157e5 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,2 +1,3 @@ pub mod queue; pub mod search; +pub mod trackbutton; diff --git a/src/ui/queue.rs b/src/ui/queue.rs index 596f70b..ec05b31 100644 --- a/src/ui/queue.rs +++ b/src/ui/queue.rs @@ -1,4 +1,5 @@ use cursive::direction::Orientation; +use cursive::event::Key; use cursive::traits::Boxable; use cursive::traits::Identifiable; use cursive::views::*; @@ -11,6 +12,7 @@ use librespot::core::spotify_id::SpotifyId; use queue::Queue; use spotify::Spotify; +use ui::trackbutton::TrackButton; pub struct QueueView { pub view: Option>, @@ -40,23 +42,18 @@ impl QueueView { let queue_ref = self.queue.clone(); let queue = self.queue.lock().unwrap(); for (index, track) in queue.iter().enumerate() { - let artists = track - .artists - .iter() - .map(|ref artist| artist.name.clone()) - .collect::>() - .join(", "); - let formatted = format!("{} - {}", artists, track.name); - - let trackid = SpotifyId::from_base62(&track.id).expect("could not load track"); - let s = self.spotify.clone(); + let mut button = TrackButton::new(&track); + let spotify = self.spotify.clone(); + // dequeues the selected track let queue_ref = queue_ref.clone(); - let button = Button::new_raw(formatted, move |_cursive| { - s.load(trackid); - s.play(); - queue_ref.lock().unwrap().remove(index); + button.add_callback(Key::Enter, move |_cursive| { + let track = queue_ref.lock().unwrap().remove(index).expect("could not dequeue track"); + let trackid = SpotifyId::from_base62(&track.id).expect("could not load track"); + spotify.load(trackid); + spotify.play(); }); + queuelist.add_child("", button); } } diff --git a/src/ui/search.rs b/src/ui/search.rs index f15e141..76a806c 100644 --- a/src/ui/search.rs +++ b/src/ui/search.rs @@ -1,4 +1,5 @@ use cursive::direction::Orientation; +use cursive::event::Key; use cursive::traits::Boxable; use cursive::traits::Identifiable; use cursive::views::*; @@ -10,6 +11,7 @@ use librespot::core::spotify_id::SpotifyId; use queue::Queue; use spotify::Spotify; +use ui::trackbutton::TrackButton; pub struct SearchView { pub view: Panel, @@ -32,24 +34,22 @@ impl SearchView { for track in tracks.tracks.items { let s = spotify.clone(); let trackid = SpotifyId::from_base62(&track.id).expect("could not load track"); - let artists = track - .artists - .iter() - .map(|ref artist| artist.name.clone()) - .collect::>() - .join(", "); - let formatted = format!("{} - {}", artists, track.name); - let button = Button::new_raw(formatted, move |_cursive| { + let mut button = TrackButton::new(&track); + + // plays the selected track + button.add_callback(Key::Enter, move |_cursive| { s.load(trackid); s.play(); }); + + // queues the selected track let queue = queue.clone(); - let button_queue = OnEventView::new(button).on_event(' ', move |_cursive| { + button.add_callback(' ', move |_cursive| { let mut queue = queue.lock().unwrap(); queue.enqueue(track.clone()); - debug!("Added to queue: {}", track.name); }); - results.add_child("", button_queue); + + results.add_child("", button); } } } diff --git a/src/ui/trackbutton.rs b/src/ui/trackbutton.rs new file mode 100644 index 0000000..db13c80 --- /dev/null +++ b/src/ui/trackbutton.rs @@ -0,0 +1,121 @@ +use cursive::align::HAlign; +use cursive::Cursive; +use cursive::direction::Direction; +use cursive::event::{Callback, Event, EventResult, EventTrigger}; +use cursive::vec::Vec2; +use cursive::Printer; +use cursive::traits::View; +use cursive::theme::ColorStyle; +use rspotify::spotify::model::track::FullTrack; +use unicode_width::UnicodeWidthStr; + +pub struct TrackButton { + callbacks: Vec<(EventTrigger, Callback)>, + + track: FullTrack, + title: String, + duration: String, + + enabled: bool, + last_size: Vec2, + invalidated: bool, +} + +impl TrackButton { + pub fn new(track: &FullTrack) -> TrackButton { + let artists = track + .artists + .iter() + .map(|ref artist| artist.name.clone()) + .collect::>() + .join(", "); + let formatted_title = format!("{} - {}", artists, track.name); + + let minutes = track.duration_ms / 60000; + let seconds = (track.duration_ms % 60000)/1000; + let formatted_duration = format!("{:02}:{:02}", minutes, seconds); + + TrackButton { + callbacks: Vec::new(), + track: track.clone(), + title: formatted_title, + duration: formatted_duration, + enabled: true, + last_size: Vec2::zero(), + invalidated: true, + } + } + + pub fn add_callback(&mut self, trigger: E, cb: F) + where + E: Into, + F: 'static + Fn(&mut Cursive), + { + self.callbacks.push((trigger.into(), Callback::from_fn(cb))); + } +} + +// This is heavily based on Cursive's Button implementation with minor +// modifications to print the track's duration at the right screen border +impl View for TrackButton { + fn draw(&self, printer: &Printer<'_, '_>) { + if printer.size.x == 0 { + return; + } + + let style = if !(self.enabled && printer.enabled) { + ColorStyle::secondary() + } else if !printer.focused { + ColorStyle::primary() + } else { + ColorStyle::highlight() + }; + + // shorten titles that are too long and append ".." to indicate this + let mut title_shortened = self.title.clone(); + title_shortened.truncate(printer.size.x - self.duration.width() - 1); + if title_shortened.width() < self.title.width() { + let offset = title_shortened.width()-2; + title_shortened.replace_range(offset.., ".."); + } + + printer.with_color(style, |printer| { + printer.print((0, 0), &title_shortened); + }); + + // track duration goes to the end of the line + let offset = + HAlign::Right.get_offset(self.duration.width(), printer.size.x); + + printer.with_color(style, |printer| { + printer.print((offset, 0), &self.duration); + }); + } + + fn on_event(&mut self, event: Event) -> EventResult { + for (trigger, callback) in self.callbacks.iter() { + if trigger.apply(&event) { + return EventResult::Consumed(Some(callback.clone())) + } + } + EventResult::Ignored + } + + fn layout(&mut self, size: Vec2) { + self.last_size = size; + self.invalidated = false; + } + + fn required_size(&mut self, constraint: Vec2) -> Vec2 { + // we always want the full width + Vec2::new(constraint.x, 1) + } + + fn take_focus(&mut self, _: Direction) -> bool { + self.enabled + } + + fn needs_relayout(&self) -> bool { + self.invalidated + } +}