From 7a24eca80962841b0841a6fabeeb8a1b9907431c Mon Sep 17 00:00:00 2001 From: KoffeinFlummi Date: Tue, 5 Mar 2019 19:16:35 +0100 Subject: [PATCH] Add main layout view and status bar Fix #4 --- src/events.rs | 7 ++- src/main.rs | 38 ++++++++++----- src/spotify.rs | 112 +++++++++++++++++++++++++++++++++++--------- src/ui/layout.rs | 91 +++++++++++++++++++++++++++++++++++ src/ui/mod.rs | 2 + src/ui/queue.rs | 4 +- src/ui/search.rs | 6 +-- src/ui/statusbar.rs | 78 ++++++++++++++++++++++++++++++ 8 files changed, 295 insertions(+), 43 deletions(-) create mode 100644 src/ui/layout.rs create mode 100644 src/ui/statusbar.rs diff --git a/src/events.rs b/src/events.rs index 0198d61..b2deea2 100644 --- a/src/events.rs +++ b/src/events.rs @@ -1,13 +1,16 @@ use crossbeam_channel::{unbounded, Receiver, Sender, TryIter}; use cursive::{CbFunc, Cursive}; +use rspotify::spotify::model::track::FullTrack; +use spotify::PlayerStatus; + use queue::QueueChange; -use spotify::PlayerState; use ui::playlist::PlaylistEvent; pub enum Event { Queue(QueueChange), - Player(PlayerState), + PlayerStatus(PlayerStatus), + PlayerTrack(Option), Playlist(PlaylistEvent), } diff --git a/src/main.rs b/src/main.rs index 80b6c33..7f4e525 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,6 +27,7 @@ use std::sync::Mutex; use cursive::event::Key; use cursive::view::ScrollStrategy; use cursive::views::*; +use cursive::traits::Identifiable; use cursive::Cursive; mod config; @@ -129,43 +130,53 @@ fn main() { }); } - let searchscreen = cursive.active_screen(); 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()); - 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()); - 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 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| { - s.set_screen(logscreen); + s.call_on_id("main", |v: &mut ui::layout::Layout| { + v.set_view("log"); + }); }); { let ev = event_manager.clone(); 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)); }); } 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(); 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)); }); } @@ -177,7 +188,8 @@ fn main() { trace!("event received"); match event { 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), } } diff --git a/src/spotify.rs b/src/spotify.rs index 8a1a090..c79a3aa 100644 --- a/src/spotify.rs +++ b/src/spotify.rs @@ -14,6 +14,7 @@ use rspotify::spotify::client::Spotify as SpotifyAPI; use rspotify::spotify::model::page::Page; use rspotify::spotify::model::playlist::{PlaylistTrack, SimplifiedPlaylist}; use rspotify::spotify::model::search::SearchTracks; +use rspotify::spotify::model::track::FullTrack; use failure::Error; @@ -28,27 +29,32 @@ use tokio_core::reactor::Core; use std::sync::Arc; use std::sync::Mutex; use std::sync::RwLock; +use std::time::{Duration, SystemTime}; use std::thread; use events::{Event, EventManager}; use queue::Queue; enum WorkerCommand { - Load(SpotifyId), + Load(FullTrack), Play, Pause, Stop, } -pub enum PlayerState { +#[derive(Clone)] +pub enum PlayerStatus { Playing, Paused, Stopped, } pub struct Spotify { - pub state: RwLock, + status: RwLock, + track: RwLock>, pub api: SpotifyAPI, + elapsed: RwLock>, + since: RwLock>, channel: mpsc::UnboundedSender, events: EventManager, user: String, @@ -93,20 +99,23 @@ impl futures::Future for Worker { debug!("message received!"); match cmd { 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.."); + self.events.send(Event::PlayerTrack(Some(track))); } WorkerCommand::Play => { self.player.play(); - self.events.send(Event::Player(PlayerState::Playing)); + self.events.send(Event::PlayerStatus(PlayerStatus::Playing)); } WorkerCommand::Pause => { self.player.pause(); - self.events.send(Event::Player(PlayerState::Paused)); + self.events.send(Event::PlayerStatus(PlayerStatus::Paused)); } WorkerCommand::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.player.play(); - self.events.send(Event::Player(PlayerState::Playing)); + self.events.send(Event::PlayerTrack(Some(track))); + self.events.send(Event::PlayerStatus(PlayerStatus::Playing)); } else { - self.events.send(Event::Player(PlayerState::Stopped)); + self.events.send(Event::PlayerTrack(None)); + self.events.send(Event::PlayerStatus(PlayerStatus::Stopped)); } } Ok(Async::NotReady) => (), @@ -183,8 +194,11 @@ impl Spotify { let api = SpotifyAPI::default().access_token(&token.access_token); Spotify { - state: RwLock::new(PlayerState::Stopped), + status: RwLock::new(PlayerStatus::Stopped), + track: RwLock::new(None), api: api, + elapsed: RwLock::new(None), + since: RwLock::new(None), channel: tx, events: events, user: user, @@ -223,6 +237,40 @@ impl Spotify { 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 { + 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) { + let mut elapsed = self.elapsed.write().expect("could not acquire write lock on elapsed time"); + *elapsed = new_elapsed; + } + + fn get_elapsed(&self) -> Option { + let elapsed = self.elapsed.read().expect("could not acquire read lock on elapsed time"); + (*elapsed).clone() + } + + fn set_since(&self, new_since: Option) { + let mut since = self.since.write().expect("could not acquire write lock on since time"); + *since = new_since; + } + + fn get_since(&self) -> Option { + 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 { 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) } - pub fn load(&self, track: SpotifyId) { + pub fn load(&self, track: FullTrack) { info!("loading track: {:?}", track); self.channel .unbounded_send(WorkerCommand::Load(track)) .unwrap(); } - pub fn updatestate(&self, newstate: PlayerState) { - let mut state = self - .state + pub fn update_status(&self, new_status: PlayerStatus) { + match new_status { + 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() - .expect("could not acquire write lock on player state"); - *state = newstate; + .expect("could not acquire write lock on player status"); + *status = new_status; + } + + pub fn update_track(&self, new_track: Option) { + 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) { @@ -261,13 +331,13 @@ impl Spotify { } pub fn toggleplayback(&self) { - let state = self - .state + let status = self + .status .read() .expect("could not acquire read lock on player state"); - match *state { - PlayerState::Playing => self.pause(), - PlayerState::Paused => self.play(), + match *status { + PlayerStatus::Playing => self.pause(), + PlayerStatus::Paused => self.play(), _ => (), } } diff --git a/src/ui/layout.rs b/src/ui/layout.rs new file mode 100644 index 0000000..2be888c --- /dev/null +++ b/src/ui/layout.rs @@ -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>, + statusbar: Box, + focus: Option +} + +impl Layout { + pub fn new(status: T) -> Layout { + Layout { + views: HashMap::new(), + statusbar: status.as_boxed_view(), + focus: None + } + } + + pub fn add_view, 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, T: IntoBoxedView>(mut self, id: S, view: T) -> Self { + (&mut self).add_view(id, view); + self + } + + pub fn set_view>(&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 + } + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index c457e3d..bdbef3d 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -2,3 +2,5 @@ pub mod playlist; pub mod queue; pub mod search; pub mod trackbutton; +pub mod statusbar; +pub mod layout; diff --git a/src/ui/queue.rs b/src/ui/queue.rs index 670423c..82bb708 100644 --- a/src/ui/queue.rs +++ b/src/ui/queue.rs @@ -8,7 +8,6 @@ use cursive::Cursive; use std::sync::Arc; use std::sync::Mutex; -use librespot::core::spotify_id::SpotifyId; use rspotify::spotify::model::track::FullTrack; use queue::{Queue, QueueChange}; @@ -47,8 +46,7 @@ impl QueueView { if let Some(queuelist) = view_ref { let index = queuelist.get_focus_index(); let track = queue.remove(index).expect("could not dequeue track"); - let trackid = SpotifyId::from_base62(&track.id).expect("could not load track"); - spotify.load(trackid); + spotify.load(track); spotify.play(); } } diff --git a/src/ui/search.rs b/src/ui/search.rs index 76a806c..43d1d1d 100644 --- a/src/ui/search.rs +++ b/src/ui/search.rs @@ -7,8 +7,6 @@ use cursive::Cursive; use std::sync::Arc; use std::sync::Mutex; -use librespot::core::spotify_id::SpotifyId; - use queue::Queue; use spotify::Spotify; use ui::trackbutton::TrackButton; @@ -33,12 +31,12 @@ impl SearchView { if let Ok(tracks) = tracks { for track in tracks.tracks.items { let s = spotify.clone(); - let trackid = SpotifyId::from_base62(&track.id).expect("could not load track"); let mut button = TrackButton::new(&track); // plays the selected track + let t = track.clone(); button.add_callback(Key::Enter, move |_cursive| { - s.load(trackid); + s.load(t.clone()); s.play(); }); diff --git a/src/ui/statusbar.rs b/src/ui/statusbar.rs new file mode 100644 index 0000000..1e15d75 --- /dev/null +++ b/src/ui/statusbar.rs @@ -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 +} + +impl StatusBar { + pub fn new(spotify: Arc) -> 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::()); + printer.with_color(style, |printer| { + printer.print((0, 1), &vec![' '; printer.size.x].into_iter().collect::()); + }); + + 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::>().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) + } +}