implement custom track button view (closes #2)
This commit is contained in:
@@ -15,6 +15,7 @@ serde = "1.0"
|
|||||||
serde_derive = "1.0"
|
serde_derive = "1.0"
|
||||||
toml = "0.4"
|
toml = "0.4"
|
||||||
tokio-core = "0.1"
|
tokio-core = "0.1"
|
||||||
|
unicode-width = "0.1.5"
|
||||||
|
|
||||||
[dependencies.librespot]
|
[dependencies.librespot]
|
||||||
git = "https://github.com/librespot-org/librespot.git"
|
git = "https://github.com/librespot-org/librespot.git"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ extern crate futures;
|
|||||||
extern crate librespot;
|
extern crate librespot;
|
||||||
extern crate rspotify;
|
extern crate rspotify;
|
||||||
extern crate tokio_core;
|
extern crate tokio_core;
|
||||||
|
extern crate unicode_width;
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate serde_derive;
|
extern crate serde_derive;
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ impl Queue {
|
|||||||
pub fn remove(&mut self, index: usize) -> Option<FullTrack> {
|
pub fn remove(&mut self, index: usize) -> Option<FullTrack> {
|
||||||
match self.queue.remove(index) {
|
match self.queue.remove(index) {
|
||||||
Some(track) => {
|
Some(track) => {
|
||||||
|
debug!("Removed from queue: {}", &track.name);
|
||||||
self.send_event();
|
self.send_event();
|
||||||
Some(track)
|
Some(track)
|
||||||
}
|
}
|
||||||
@@ -30,12 +31,14 @@ impl Queue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn enqueue(&mut self, track: FullTrack) {
|
pub fn enqueue(&mut self, track: FullTrack) {
|
||||||
|
debug!("Queued: {}", &track.name);
|
||||||
self.queue.push_back(track);
|
self.queue.push_back(track);
|
||||||
self.send_event();
|
self.send_event();
|
||||||
}
|
}
|
||||||
pub fn dequeue(&mut self) -> Option<FullTrack> {
|
pub fn dequeue(&mut self) -> Option<FullTrack> {
|
||||||
match self.queue.pop_front() {
|
match self.queue.pop_front() {
|
||||||
Some(track) => {
|
Some(track) => {
|
||||||
|
debug!("Dequeued : {}", track.name);
|
||||||
self.send_event();
|
self.send_event();
|
||||||
Some(track)
|
Some(track)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
pub mod queue;
|
pub mod queue;
|
||||||
pub mod search;
|
pub mod search;
|
||||||
|
pub mod trackbutton;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use cursive::direction::Orientation;
|
use cursive::direction::Orientation;
|
||||||
|
use cursive::event::Key;
|
||||||
use cursive::traits::Boxable;
|
use cursive::traits::Boxable;
|
||||||
use cursive::traits::Identifiable;
|
use cursive::traits::Identifiable;
|
||||||
use cursive::views::*;
|
use cursive::views::*;
|
||||||
@@ -11,6 +12,7 @@ use librespot::core::spotify_id::SpotifyId;
|
|||||||
|
|
||||||
use queue::Queue;
|
use queue::Queue;
|
||||||
use spotify::Spotify;
|
use spotify::Spotify;
|
||||||
|
use ui::trackbutton::TrackButton;
|
||||||
|
|
||||||
pub struct QueueView {
|
pub struct QueueView {
|
||||||
pub view: Option<Panel<LinearLayout>>,
|
pub view: Option<Panel<LinearLayout>>,
|
||||||
@@ -40,23 +42,18 @@ impl QueueView {
|
|||||||
let queue_ref = self.queue.clone();
|
let queue_ref = self.queue.clone();
|
||||||
let queue = self.queue.lock().unwrap();
|
let queue = self.queue.lock().unwrap();
|
||||||
for (index, track) in queue.iter().enumerate() {
|
for (index, track) in queue.iter().enumerate() {
|
||||||
let artists = track
|
let mut button = TrackButton::new(&track);
|
||||||
.artists
|
let spotify = self.spotify.clone();
|
||||||
.iter()
|
|
||||||
.map(|ref artist| artist.name.clone())
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join(", ");
|
|
||||||
let formatted = format!("{} - {}", artists, track.name);
|
|
||||||
|
|
||||||
let trackid = SpotifyId::from_base62(&track.id).expect("could not load track");
|
|
||||||
let s = self.spotify.clone();
|
|
||||||
|
|
||||||
|
// <enter> dequeues the selected track
|
||||||
let queue_ref = queue_ref.clone();
|
let queue_ref = queue_ref.clone();
|
||||||
let button = Button::new_raw(formatted, move |_cursive| {
|
button.add_callback(Key::Enter, move |_cursive| {
|
||||||
s.load(trackid);
|
let track = queue_ref.lock().unwrap().remove(index).expect("could not dequeue track");
|
||||||
s.play();
|
let trackid = SpotifyId::from_base62(&track.id).expect("could not load track");
|
||||||
queue_ref.lock().unwrap().remove(index);
|
spotify.load(trackid);
|
||||||
|
spotify.play();
|
||||||
});
|
});
|
||||||
|
|
||||||
queuelist.add_child("", button);
|
queuelist.add_child("", button);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use cursive::direction::Orientation;
|
use cursive::direction::Orientation;
|
||||||
|
use cursive::event::Key;
|
||||||
use cursive::traits::Boxable;
|
use cursive::traits::Boxable;
|
||||||
use cursive::traits::Identifiable;
|
use cursive::traits::Identifiable;
|
||||||
use cursive::views::*;
|
use cursive::views::*;
|
||||||
@@ -10,6 +11,7 @@ use librespot::core::spotify_id::SpotifyId;
|
|||||||
|
|
||||||
use queue::Queue;
|
use queue::Queue;
|
||||||
use spotify::Spotify;
|
use spotify::Spotify;
|
||||||
|
use ui::trackbutton::TrackButton;
|
||||||
|
|
||||||
pub struct SearchView {
|
pub struct SearchView {
|
||||||
pub view: Panel<LinearLayout>,
|
pub view: Panel<LinearLayout>,
|
||||||
@@ -32,24 +34,22 @@ impl SearchView {
|
|||||||
for track in tracks.tracks.items {
|
for track in tracks.tracks.items {
|
||||||
let s = spotify.clone();
|
let s = spotify.clone();
|
||||||
let trackid = SpotifyId::from_base62(&track.id).expect("could not load track");
|
let trackid = SpotifyId::from_base62(&track.id).expect("could not load track");
|
||||||
let artists = track
|
let mut button = TrackButton::new(&track);
|
||||||
.artists
|
|
||||||
.iter()
|
// <enter> plays the selected track
|
||||||
.map(|ref artist| artist.name.clone())
|
button.add_callback(Key::Enter, move |_cursive| {
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join(", ");
|
|
||||||
let formatted = format!("{} - {}", artists, track.name);
|
|
||||||
let button = Button::new_raw(formatted, move |_cursive| {
|
|
||||||
s.load(trackid);
|
s.load(trackid);
|
||||||
s.play();
|
s.play();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// <space> queues the selected track
|
||||||
let queue = queue.clone();
|
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();
|
let mut queue = queue.lock().unwrap();
|
||||||
queue.enqueue(track.clone());
|
queue.enqueue(track.clone());
|
||||||
debug!("Added to queue: {}", track.name);
|
|
||||||
});
|
});
|
||||||
results.add_child("", button_queue);
|
|
||||||
|
results.add_child("", button);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
121
src/ui/trackbutton.rs
Normal file
121
src/ui/trackbutton.rs
Normal file
@@ -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::<Vec<String>>()
|
||||||
|
.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<F, E>(&mut self, trigger: E, cb: F)
|
||||||
|
where
|
||||||
|
E: Into<EventTrigger>,
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user