@@ -1,13 +1,16 @@
|
|||||||
use crossbeam_channel::{unbounded, Receiver, Sender, TryIter};
|
use crossbeam_channel::{unbounded, Receiver, Sender, TryIter};
|
||||||
use cursive::{CbFunc, Cursive};
|
use cursive::{CbFunc, Cursive};
|
||||||
|
|
||||||
|
use rspotify::spotify::model::track::FullTrack;
|
||||||
|
use spotify::PlayerStatus;
|
||||||
|
|
||||||
use queue::QueueChange;
|
use queue::QueueChange;
|
||||||
use spotify::PlayerState;
|
|
||||||
use ui::playlist::PlaylistEvent;
|
use ui::playlist::PlaylistEvent;
|
||||||
|
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
Queue(QueueChange),
|
Queue(QueueChange),
|
||||||
Player(PlayerState),
|
PlayerStatus(PlayerStatus),
|
||||||
|
PlayerTrack(Option<FullTrack>),
|
||||||
Playlist(PlaylistEvent),
|
Playlist(PlaylistEvent),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
38
src/main.rs
38
src/main.rs
@@ -27,6 +27,7 @@ use std::sync::Mutex;
|
|||||||
use cursive::event::Key;
|
use cursive::event::Key;
|
||||||
use cursive::view::ScrollStrategy;
|
use cursive::view::ScrollStrategy;
|
||||||
use cursive::views::*;
|
use cursive::views::*;
|
||||||
|
use cursive::traits::Identifiable;
|
||||||
use cursive::Cursive;
|
use cursive::Cursive;
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
@@ -129,43 +130,53 @@ fn main() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let searchscreen = cursive.active_screen();
|
|
||||||
let search = ui::search::SearchView::new(spotify.clone(), queue.clone());
|
let search = ui::search::SearchView::new(spotify.clone(), queue.clone());
|
||||||
cursive.add_fullscreen_layer(search.view);
|
|
||||||
|
|
||||||
let playlistscreen = cursive.add_active_screen();
|
|
||||||
let mut playlists = ui::playlist::PlaylistView::new(queue.clone(), spotify.clone());
|
let mut playlists = ui::playlist::PlaylistView::new(queue.clone(), spotify.clone());
|
||||||
cursive.add_fullscreen_layer(playlists.view.take().unwrap());
|
|
||||||
|
|
||||||
let queuescreen = cursive.add_active_screen();
|
|
||||||
let mut queueview = ui::queue::QueueView::new(queue.clone(), spotify.clone());
|
let mut queueview = ui::queue::QueueView::new(queue.clone(), spotify.clone());
|
||||||
cursive.add_fullscreen_layer(queueview.view.take().unwrap());
|
|
||||||
|
|
||||||
let logscreen = cursive.add_active_screen();
|
|
||||||
let logview_scroller = ScrollView::new(logview).scroll_strategy(ScrollStrategy::StickToBottom);
|
let logview_scroller = ScrollView::new(logview).scroll_strategy(ScrollStrategy::StickToBottom);
|
||||||
let logpanel = Panel::new(logview_scroller).title("Log");
|
let logpanel = Panel::new(logview_scroller).title("Log");
|
||||||
cursive.add_fullscreen_layer(logpanel);
|
|
||||||
|
let status = ui::statusbar::StatusBar::new(spotify.clone());
|
||||||
|
|
||||||
|
let layout = ui::layout::Layout::new(status)
|
||||||
|
.view("search", BoxView::with_full_height(search.view))
|
||||||
|
.view("playlists", playlists.view.take().unwrap())
|
||||||
|
.view("queue", queueview.view.take().unwrap())
|
||||||
|
.view("log", logpanel);
|
||||||
|
|
||||||
|
cursive.add_fullscreen_layer(layout.with_id("main"));
|
||||||
|
|
||||||
cursive.add_global_callback(Key::F1, move |s| {
|
cursive.add_global_callback(Key::F1, move |s| {
|
||||||
s.set_screen(logscreen);
|
s.call_on_id("main", |v: &mut ui::layout::Layout| {
|
||||||
|
v.set_view("log");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
{
|
{
|
||||||
let ev = event_manager.clone();
|
let ev = event_manager.clone();
|
||||||
cursive.add_global_callback(Key::F2, move |s| {
|
cursive.add_global_callback(Key::F2, move |s| {
|
||||||
s.set_screen(queuescreen);
|
s.call_on_id("main", |v: &mut ui::layout::Layout| {
|
||||||
|
v.set_view("queue");
|
||||||
|
});
|
||||||
ev.send(Event::Queue(QueueChange::Show));
|
ev.send(Event::Queue(QueueChange::Show));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
cursive.add_global_callback(Key::F3, move |s| {
|
cursive.add_global_callback(Key::F3, move |s| {
|
||||||
s.set_screen(searchscreen);
|
s.call_on_id("main", |v: &mut ui::layout::Layout| {
|
||||||
|
v.set_view("search");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
{
|
{
|
||||||
let ev = event_manager.clone();
|
let ev = event_manager.clone();
|
||||||
cursive.add_global_callback(Key::F4, move |s| {
|
cursive.add_global_callback(Key::F4, move |s| {
|
||||||
s.set_screen(playlistscreen);
|
s.call_on_id("main", |v: &mut ui::layout::Layout| {
|
||||||
|
v.set_view("playlists");
|
||||||
|
});
|
||||||
ev.send(Event::Playlist(PlaylistEvent::Refresh));
|
ev.send(Event::Playlist(PlaylistEvent::Refresh));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -177,7 +188,8 @@ fn main() {
|
|||||||
trace!("event received");
|
trace!("event received");
|
||||||
match event {
|
match event {
|
||||||
Event::Queue(ev) => queueview.handle_ev(&mut cursive, ev),
|
Event::Queue(ev) => queueview.handle_ev(&mut cursive, ev),
|
||||||
Event::Player(state) => spotify.updatestate(state),
|
Event::PlayerStatus(state) => spotify.update_status(state),
|
||||||
|
Event::PlayerTrack(track) => spotify.update_track(track),
|
||||||
Event::Playlist(event) => playlists.handle_ev(&mut cursive, event),
|
Event::Playlist(event) => playlists.handle_ev(&mut cursive, event),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
112
src/spotify.rs
112
src/spotify.rs
@@ -14,6 +14,7 @@ use rspotify::spotify::client::Spotify as SpotifyAPI;
|
|||||||
use rspotify::spotify::model::page::Page;
|
use rspotify::spotify::model::page::Page;
|
||||||
use rspotify::spotify::model::playlist::{PlaylistTrack, SimplifiedPlaylist};
|
use rspotify::spotify::model::playlist::{PlaylistTrack, SimplifiedPlaylist};
|
||||||
use rspotify::spotify::model::search::SearchTracks;
|
use rspotify::spotify::model::search::SearchTracks;
|
||||||
|
use rspotify::spotify::model::track::FullTrack;
|
||||||
|
|
||||||
use failure::Error;
|
use failure::Error;
|
||||||
|
|
||||||
@@ -28,27 +29,32 @@ use tokio_core::reactor::Core;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
|
use std::time::{Duration, SystemTime};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
use events::{Event, EventManager};
|
use events::{Event, EventManager};
|
||||||
use queue::Queue;
|
use queue::Queue;
|
||||||
|
|
||||||
enum WorkerCommand {
|
enum WorkerCommand {
|
||||||
Load(SpotifyId),
|
Load(FullTrack),
|
||||||
Play,
|
Play,
|
||||||
Pause,
|
Pause,
|
||||||
Stop,
|
Stop,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum PlayerState {
|
#[derive(Clone)]
|
||||||
|
pub enum PlayerStatus {
|
||||||
Playing,
|
Playing,
|
||||||
Paused,
|
Paused,
|
||||||
Stopped,
|
Stopped,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Spotify {
|
pub struct Spotify {
|
||||||
pub state: RwLock<PlayerState>,
|
status: RwLock<PlayerStatus>,
|
||||||
|
track: RwLock<Option<FullTrack>>,
|
||||||
pub api: SpotifyAPI,
|
pub api: SpotifyAPI,
|
||||||
|
elapsed: RwLock<Option<Duration>>,
|
||||||
|
since: RwLock<Option<SystemTime>>,
|
||||||
channel: mpsc::UnboundedSender<WorkerCommand>,
|
channel: mpsc::UnboundedSender<WorkerCommand>,
|
||||||
events: EventManager,
|
events: EventManager,
|
||||||
user: String,
|
user: String,
|
||||||
@@ -93,20 +99,23 @@ impl futures::Future for Worker {
|
|||||||
debug!("message received!");
|
debug!("message received!");
|
||||||
match cmd {
|
match cmd {
|
||||||
WorkerCommand::Load(track) => {
|
WorkerCommand::Load(track) => {
|
||||||
self.play_task = Box::new(self.player.load(track, false, 0));
|
let trackid = SpotifyId::from_base62(&track.id).expect("could not load track");
|
||||||
|
self.play_task = Box::new(self.player.load(trackid, false, 0));
|
||||||
info!("player loading track..");
|
info!("player loading track..");
|
||||||
|
self.events.send(Event::PlayerTrack(Some(track)));
|
||||||
}
|
}
|
||||||
WorkerCommand::Play => {
|
WorkerCommand::Play => {
|
||||||
self.player.play();
|
self.player.play();
|
||||||
self.events.send(Event::Player(PlayerState::Playing));
|
self.events.send(Event::PlayerStatus(PlayerStatus::Playing));
|
||||||
}
|
}
|
||||||
WorkerCommand::Pause => {
|
WorkerCommand::Pause => {
|
||||||
self.player.pause();
|
self.player.pause();
|
||||||
self.events.send(Event::Player(PlayerState::Paused));
|
self.events.send(Event::PlayerStatus(PlayerStatus::Paused));
|
||||||
}
|
}
|
||||||
WorkerCommand::Stop => {
|
WorkerCommand::Stop => {
|
||||||
self.player.stop();
|
self.player.stop();
|
||||||
self.events.send(Event::Player(PlayerState::Stopped));
|
self.events.send(Event::PlayerTrack(None));
|
||||||
|
self.events.send(Event::PlayerStatus(PlayerStatus::Stopped));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -123,9 +132,11 @@ impl futures::Future for Worker {
|
|||||||
self.play_task = Box::new(self.player.load(trackid, false, 0));
|
self.play_task = Box::new(self.player.load(trackid, false, 0));
|
||||||
self.player.play();
|
self.player.play();
|
||||||
|
|
||||||
self.events.send(Event::Player(PlayerState::Playing));
|
self.events.send(Event::PlayerTrack(Some(track)));
|
||||||
|
self.events.send(Event::PlayerStatus(PlayerStatus::Playing));
|
||||||
} else {
|
} else {
|
||||||
self.events.send(Event::Player(PlayerState::Stopped));
|
self.events.send(Event::PlayerTrack(None));
|
||||||
|
self.events.send(Event::PlayerStatus(PlayerStatus::Stopped));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(Async::NotReady) => (),
|
Ok(Async::NotReady) => (),
|
||||||
@@ -183,8 +194,11 @@ impl Spotify {
|
|||||||
let api = SpotifyAPI::default().access_token(&token.access_token);
|
let api = SpotifyAPI::default().access_token(&token.access_token);
|
||||||
|
|
||||||
Spotify {
|
Spotify {
|
||||||
state: RwLock::new(PlayerState::Stopped),
|
status: RwLock::new(PlayerStatus::Stopped),
|
||||||
|
track: RwLock::new(None),
|
||||||
api: api,
|
api: api,
|
||||||
|
elapsed: RwLock::new(None),
|
||||||
|
since: RwLock::new(None),
|
||||||
channel: tx,
|
channel: tx,
|
||||||
events: events,
|
events: events,
|
||||||
user: user,
|
user: user,
|
||||||
@@ -223,6 +237,40 @@ impl Spotify {
|
|||||||
debug!("worker thread finished.");
|
debug!("worker thread finished.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_current_status(&self) -> PlayerStatus {
|
||||||
|
let status = self.status.read().expect("could not acquire read lock on playback status");
|
||||||
|
(*status).clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_current_track(&self) -> Option<FullTrack> {
|
||||||
|
let track = self.track.read().expect("could not acquire read lock on current track");
|
||||||
|
(*track).clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_current_progress(&self) -> Duration {
|
||||||
|
self.get_elapsed().unwrap_or(Duration::from_secs(0)) + self.get_since().map(|t| t.elapsed().unwrap()).unwrap_or(Duration::from_secs(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_elapsed(&self, new_elapsed: Option<Duration>) {
|
||||||
|
let mut elapsed = self.elapsed.write().expect("could not acquire write lock on elapsed time");
|
||||||
|
*elapsed = new_elapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_elapsed(&self) -> Option<Duration> {
|
||||||
|
let elapsed = self.elapsed.read().expect("could not acquire read lock on elapsed time");
|
||||||
|
(*elapsed).clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_since(&self, new_since: Option<SystemTime>) {
|
||||||
|
let mut since = self.since.write().expect("could not acquire write lock on since time");
|
||||||
|
*since = new_since;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_since(&self) -> Option<SystemTime> {
|
||||||
|
let since = self.since.read().expect("could not acquire read lock on since time");
|
||||||
|
(*since).clone()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn search(&self, query: &str, limit: u32, offset: u32) -> Result<SearchTracks, Error> {
|
pub fn search(&self, query: &str, limit: u32, offset: u32) -> Result<SearchTracks, Error> {
|
||||||
self.api.search_track(query, limit, offset, None)
|
self.api.search_track(query, limit, offset, None)
|
||||||
}
|
}
|
||||||
@@ -240,19 +288,41 @@ impl Spotify {
|
|||||||
.user_playlist_tracks(&self.user, playlist_id, None, 50, 0, None)
|
.user_playlist_tracks(&self.user, playlist_id, None, 50, 0, None)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load(&self, track: SpotifyId) {
|
pub fn load(&self, track: FullTrack) {
|
||||||
info!("loading track: {:?}", track);
|
info!("loading track: {:?}", track);
|
||||||
self.channel
|
self.channel
|
||||||
.unbounded_send(WorkerCommand::Load(track))
|
.unbounded_send(WorkerCommand::Load(track))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn updatestate(&self, newstate: PlayerState) {
|
pub fn update_status(&self, new_status: PlayerStatus) {
|
||||||
let mut state = self
|
match new_status {
|
||||||
.state
|
PlayerStatus::Paused => {
|
||||||
|
self.set_elapsed(Some(self.get_current_progress()));
|
||||||
|
self.set_since(None);
|
||||||
|
},
|
||||||
|
PlayerStatus::Playing => {
|
||||||
|
self.set_since(Some(SystemTime::now()));
|
||||||
|
},
|
||||||
|
PlayerStatus::Stopped => {
|
||||||
|
self.set_elapsed(None);
|
||||||
|
self.set_since(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut status = self
|
||||||
|
.status
|
||||||
.write()
|
.write()
|
||||||
.expect("could not acquire write lock on player state");
|
.expect("could not acquire write lock on player status");
|
||||||
*state = newstate;
|
*status = new_status;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_track(&self, new_track: Option<FullTrack>) {
|
||||||
|
self.set_elapsed(None);
|
||||||
|
self.set_since(None);
|
||||||
|
|
||||||
|
let mut track = self.track.write().expect("could not acquire write lock on current track");
|
||||||
|
*track = new_track;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn play(&self) {
|
pub fn play(&self) {
|
||||||
@@ -261,13 +331,13 @@ impl Spotify {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn toggleplayback(&self) {
|
pub fn toggleplayback(&self) {
|
||||||
let state = self
|
let status = self
|
||||||
.state
|
.status
|
||||||
.read()
|
.read()
|
||||||
.expect("could not acquire read lock on player state");
|
.expect("could not acquire read lock on player state");
|
||||||
match *state {
|
match *status {
|
||||||
PlayerState::Playing => self.pause(),
|
PlayerStatus::Playing => self.pause(),
|
||||||
PlayerState::Paused => self.play(),
|
PlayerStatus::Paused => self.play(),
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
91
src/ui/layout.rs
Normal file
91
src/ui/layout.rs
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
use std::collections::{HashMap};
|
||||||
|
|
||||||
|
use cursive::direction::Direction;
|
||||||
|
use cursive::event::{Event, EventResult, AnyCb};
|
||||||
|
use cursive::traits::View;
|
||||||
|
use cursive::view::{IntoBoxedView, Selector};
|
||||||
|
use cursive::vec::Vec2;
|
||||||
|
use cursive::Printer;
|
||||||
|
|
||||||
|
pub struct Layout {
|
||||||
|
views: HashMap<String, Box<dyn View>>,
|
||||||
|
statusbar: Box<dyn View>,
|
||||||
|
focus: Option<String>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Layout {
|
||||||
|
pub fn new<T: IntoBoxedView>(status: T) -> Layout {
|
||||||
|
Layout {
|
||||||
|
views: HashMap::new(),
|
||||||
|
statusbar: status.as_boxed_view(),
|
||||||
|
focus: None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_view<S: Into<String>, T: IntoBoxedView>(&mut self, id: S, view: T) {
|
||||||
|
let s = id.into();
|
||||||
|
self.views.insert(s.clone(), view.as_boxed_view());
|
||||||
|
self.focus = Some(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn view<S: Into<String>, T: IntoBoxedView>(mut self, id: S, view: T) -> Self {
|
||||||
|
(&mut self).add_view(id, view);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_view<S: Into<String>>(&mut self, id: S) {
|
||||||
|
let s = id.into();
|
||||||
|
self.focus = Some(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for Layout {
|
||||||
|
fn draw(&self, printer: &Printer<'_, '_>) {
|
||||||
|
if let Some(ref id) = self.focus {
|
||||||
|
let v = self.views.get(id).unwrap();
|
||||||
|
let printer = &printer
|
||||||
|
.offset((0, 0))
|
||||||
|
.cropped((printer.size.x, printer.size.y - 2))
|
||||||
|
.focused(true);
|
||||||
|
v.draw(printer);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.statusbar.draw(&printer.offset((0, printer.size.y - 2)));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn required_size(&mut self, constraint: Vec2) -> Vec2 {
|
||||||
|
Vec2::new(constraint.x, constraint.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_event(&mut self, event: Event) -> EventResult {
|
||||||
|
if let Some(ref id) = self.focus {
|
||||||
|
let v = self.views.get_mut(id).unwrap();
|
||||||
|
v.on_event(event)
|
||||||
|
} else {
|
||||||
|
EventResult::Ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(&mut self, size: Vec2) {
|
||||||
|
if let Some(ref id) = self.focus {
|
||||||
|
let v = self.views.get_mut(id).unwrap();
|
||||||
|
v.layout(Vec2::new(size.x, size.y - 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn call_on_any<'a>(&mut self, s: &Selector, c: AnyCb<'a>) {
|
||||||
|
if let Some(ref id) = self.focus {
|
||||||
|
let v = self.views.get_mut(id).unwrap();
|
||||||
|
v.call_on_any(s, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn take_focus(&mut self, source: Direction) -> bool {
|
||||||
|
if let Some(ref id) = self.focus {
|
||||||
|
let v = self.views.get_mut(id).unwrap();
|
||||||
|
v.take_focus(source)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,3 +2,5 @@ pub mod playlist;
|
|||||||
pub mod queue;
|
pub mod queue;
|
||||||
pub mod search;
|
pub mod search;
|
||||||
pub mod trackbutton;
|
pub mod trackbutton;
|
||||||
|
pub mod statusbar;
|
||||||
|
pub mod layout;
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ use cursive::Cursive;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
use librespot::core::spotify_id::SpotifyId;
|
|
||||||
use rspotify::spotify::model::track::FullTrack;
|
use rspotify::spotify::model::track::FullTrack;
|
||||||
|
|
||||||
use queue::{Queue, QueueChange};
|
use queue::{Queue, QueueChange};
|
||||||
@@ -47,8 +46,7 @@ impl QueueView {
|
|||||||
if let Some(queuelist) = view_ref {
|
if let Some(queuelist) = view_ref {
|
||||||
let index = queuelist.get_focus_index();
|
let index = queuelist.get_focus_index();
|
||||||
let track = queue.remove(index).expect("could not dequeue track");
|
let track = queue.remove(index).expect("could not dequeue track");
|
||||||
let trackid = SpotifyId::from_base62(&track.id).expect("could not load track");
|
spotify.load(track);
|
||||||
spotify.load(trackid);
|
|
||||||
spotify.play();
|
spotify.play();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ use cursive::Cursive;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
use librespot::core::spotify_id::SpotifyId;
|
|
||||||
|
|
||||||
use queue::Queue;
|
use queue::Queue;
|
||||||
use spotify::Spotify;
|
use spotify::Spotify;
|
||||||
use ui::trackbutton::TrackButton;
|
use ui::trackbutton::TrackButton;
|
||||||
@@ -33,12 +31,12 @@ impl SearchView {
|
|||||||
if let Ok(tracks) = tracks {
|
if let Ok(tracks) = tracks {
|
||||||
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 mut button = TrackButton::new(&track);
|
let mut button = TrackButton::new(&track);
|
||||||
|
|
||||||
// <enter> plays the selected track
|
// <enter> plays the selected track
|
||||||
|
let t = track.clone();
|
||||||
button.add_callback(Key::Enter, move |_cursive| {
|
button.add_callback(Key::Enter, move |_cursive| {
|
||||||
s.load(trackid);
|
s.load(t.clone());
|
||||||
s.play();
|
s.play();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
78
src/ui/statusbar.rs
Normal file
78
src/ui/statusbar.rs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use cursive::align::HAlign;
|
||||||
|
use cursive::theme::{ColorStyle, ColorType, Color, BaseColor};
|
||||||
|
use cursive::traits::View;
|
||||||
|
use cursive::vec::Vec2;
|
||||||
|
use cursive::Printer;
|
||||||
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
|
use spotify::{PlayerStatus, Spotify};
|
||||||
|
|
||||||
|
pub struct StatusBar {
|
||||||
|
spotify: Arc<Spotify>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StatusBar {
|
||||||
|
pub fn new(spotify: Arc<Spotify>) -> StatusBar {
|
||||||
|
StatusBar {
|
||||||
|
spotify: spotify
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for StatusBar {
|
||||||
|
fn draw(&self, printer: &Printer<'_, '_>) {
|
||||||
|
if printer.size.x == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let front = ColorType::Color(Color::Dark(BaseColor::Black));
|
||||||
|
let back = ColorType::Color(Color::Dark(BaseColor::Green));
|
||||||
|
let style = ColorStyle::new(front, back);
|
||||||
|
|
||||||
|
printer.print((0, 0), &vec![' '; printer.size.x].into_iter().collect::<String>());
|
||||||
|
printer.with_color(style, |printer| {
|
||||||
|
printer.print((0, 1), &vec![' '; printer.size.x].into_iter().collect::<String>());
|
||||||
|
});
|
||||||
|
|
||||||
|
let state_icon = match self.spotify.get_current_status() {
|
||||||
|
PlayerStatus::Playing => " ▶ ",
|
||||||
|
PlayerStatus::Paused => " ▮▮ ",
|
||||||
|
PlayerStatus::Stopped => " ◼ ",
|
||||||
|
}.to_string();
|
||||||
|
|
||||||
|
printer.with_color(style, |printer| {
|
||||||
|
printer.print((0, 1), &state_icon);
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(ref t) = self.spotify.get_current_track() {
|
||||||
|
let name = format!("{} - {}",
|
||||||
|
t.artists.iter().map(|ref artist| artist.name.clone()).collect::<Vec<String>>().join(", "),
|
||||||
|
t.name).to_string();
|
||||||
|
|
||||||
|
let minutes = t.duration_ms / 60000;
|
||||||
|
let seconds = (t.duration_ms % 60000) / 1000;
|
||||||
|
let formatted_duration = format!("{:02}:{:02}", minutes, seconds);
|
||||||
|
|
||||||
|
let elapsed = self.spotify.get_current_progress();
|
||||||
|
let formatted_elapsed = format!("{:02}:{:02}", elapsed.as_secs() / 60, elapsed.as_secs() % 60);
|
||||||
|
|
||||||
|
let duration = format!("{} / {} ", formatted_elapsed, formatted_duration);
|
||||||
|
let offset = HAlign::Right.get_offset(duration.width(), printer.size.x);
|
||||||
|
|
||||||
|
printer.with_color(style, |printer| {
|
||||||
|
printer.print((4, 1), &name);
|
||||||
|
printer.print((offset, 1), &duration);
|
||||||
|
});
|
||||||
|
|
||||||
|
printer.with_color(ColorStyle::new(back, front), |printer| {
|
||||||
|
printer.print_hline((0, 0), (((printer.size.x as u32) * (elapsed.as_millis() as u32)) / t.duration_ms) as usize, "=")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn required_size(&mut self, constraint: Vec2) -> Vec2 {
|
||||||
|
Vec2::new(constraint.x, 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user