diff --git a/src/playlists.rs b/src/playlists.rs index 282eb04..6eb7253 100644 --- a/src/playlists.rs +++ b/src/playlists.rs @@ -108,7 +108,7 @@ impl Playlists { } } - fn process_playlist(list: &SimplifiedPlaylist, spotify: &Spotify) -> Playlist { + pub fn process_playlist(list: &SimplifiedPlaylist, spotify: &Spotify) -> Playlist { debug!("got list: {}", list.name); let id = list.id.clone(); diff --git a/src/spotify.rs b/src/spotify.rs index 78e1744..032fb5a 100644 --- a/src/spotify.rs +++ b/src/spotify.rs @@ -15,7 +15,7 @@ use rspotify::spotify::client::ApiError; 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::search::{SearchTracks, SearchPlaylists}; use failure::Error; @@ -428,10 +428,14 @@ impl Spotify { result.map(|r| r.id) } - pub fn search(&self, query: &str, limit: u32, offset: u32) -> Option { + pub fn search_track(&self, query: &str, limit: u32, offset: u32) -> Option { self.api_with_retry(|api| api.search_track(query, limit, offset, None)) } + pub fn search_playlist(&self, query: &str, limit: u32, offset: u32) -> Option { + self.api_with_retry(|api| api.search_playlist(query, limit, offset, None)) + } + pub fn current_user_playlist( &self, limit: u32, diff --git a/src/ui/mod.rs b/src/ui/mod.rs index bf4a8dc..61c0b77 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -5,3 +5,4 @@ pub mod playlists; pub mod queue; pub mod search; pub mod statusbar; +pub mod tabview; diff --git a/src/ui/search.rs b/src/ui/search.rs index 25714d0..e9a1a10 100644 --- a/src/ui/search.rs +++ b/src/ui/search.rs @@ -10,16 +10,19 @@ use std::cell::RefCell; use std::sync::{Arc, Mutex, RwLock}; use commands::CommandResult; +use playlists::{Playlist, Playlists}; use queue::Queue; use spotify::Spotify; use track::Track; use traits::ViewExt; use ui::listview::ListView; +use ui::tabview::TabView; pub struct SearchView { - results: Arc>>, + results_tracks: Arc>>, + results_playlists: Arc>>, edit: IdView, - list: IdView>, + list: IdView, edit_focused: bool, spotify: Arc, } @@ -28,7 +31,8 @@ pub const LIST_ID: &str = "search_list"; pub const EDIT_ID: &str = "search_edit"; impl SearchView { pub fn new(spotify: Arc, queue: Arc) -> SearchView { - let results = Arc::new(RwLock::new(Vec::new())); + let results_tracks = Arc::new(RwLock::new(Vec::new())); + let results_playlists = Arc::new(RwLock::new(Vec::new())); let searchfield = EditView::new() .on_submit(move |s, input| { @@ -40,14 +44,18 @@ impl SearchView { } }) .with_id(EDIT_ID); - let list = ListView::new(results.clone(), queue).with_id(LIST_ID); + + let tabs = TabView::new() + .tab("tracks", "Tracks", ListView::new(results_tracks.clone(), queue.clone())) + .tab("playlists", "Playlists", ListView::new(results_playlists.clone(), queue.clone())); SearchView { - results, + results_tracks, + results_playlists, edit: searchfield, - list, + list: tabs.with_id(LIST_ID), edit_focused: true, - spotify, + spotify } } @@ -66,17 +74,29 @@ impl SearchView { v.set_content(q); }); - if let Some(results) = self.spotify.search(&query, 50, 0) { + if let Some(results) = self.spotify.search_track(&query, 50, 0) { let tracks = results .tracks .items .iter() .map(|ft| Track::new(ft)) .collect(); - let mut r = self.results.write().unwrap(); + let mut r = self.results_tracks.write().unwrap(); *r = tracks; self.edit_focused = false; } + + if let Some(results) = self.spotify.search_playlist(&query, 50, 0) { + let pls = results + .playlists + .items + .iter() + .map(|sp| Playlists::process_playlist(sp, &&self.spotify)) + .collect(); + let mut r = self.results_playlists.write().unwrap(); + *r = pls; + self.edit_focused = false; + } } } diff --git a/src/ui/tabview.rs b/src/ui/tabview.rs new file mode 100644 index 0000000..5eb2cab --- /dev/null +++ b/src/ui/tabview.rs @@ -0,0 +1,139 @@ +use std::cmp::{max, min}; +use std::collections::HashMap; + +use cursive::align::HAlign; +use cursive::theme::{ColorStyle, ColorType, PaletteColor}; +use cursive::traits::View; +use cursive::{Cursive, Printer, Vec2}; +use unicode_width::UnicodeWidthStr; + +use commands::CommandResult; +use traits::{ViewExt, IntoBoxedViewExt}; + +pub struct Tab { + title: String, + view: Box, +} + +pub struct TabView { + tabs: Vec, + ids: HashMap, + selected: usize +} + +impl TabView { + pub fn new() -> Self { + Self { + tabs: Vec::new(), + ids: HashMap::new(), + selected: 0 + } + } + + pub fn add_tab, V: IntoBoxedViewExt>(&mut self, id: S, title: S, view: V) { + let tab = Tab { + title: title.into(), + view: view.as_boxed_view_ext() + }; + self.tabs.push(tab); + self.ids.insert(id.into(), self.tabs.len() - 1); + } + + pub fn tab, V: IntoBoxedViewExt>(mut self, id: S, title: S, view: V) -> Self { + self.add_tab(id, title, view); + self + } + + pub fn move_focus_to(&mut self, target: usize) { + let len = self.tabs.len().saturating_sub(1); + self.selected = min(target, len); + } + + pub fn move_focus(&mut self, delta: i32) { + let new = self.selected as i32 + delta; + self.move_focus_to(max(new, 0) as usize); + } +} + +impl View for TabView { + fn draw(&self, printer: &Printer<'_, '_>) { + if self.tabs.len() == 0 { + return; + } + + let tabwidth = printer.size.x / self.tabs.len(); + for (i, tab) in self.tabs.iter().enumerate() { + let style = if self.selected == i { + ColorStyle::new( + ColorType::Palette(PaletteColor::Tertiary), + ColorType::Palette(PaletteColor::Highlight), + ) + } else { + ColorStyle::primary() + }; + + let mut width = tabwidth; + if i == self.tabs.len() { + width += printer.size.x % self.tabs.len(); + } + + let offset = HAlign::Center.get_offset(tab.title.width(), width); + + printer.with_color(style, |printer| { + printer.print_hline((i * tabwidth, 0), width, " "); + printer.print((i * tabwidth + offset, 0), &tab.title); + }); + } + + if let Some(tab) = self.tabs.get(self.selected) { + let printer = printer + .offset((0, 1)) + .cropped((printer.size.x, printer.size.y - 1)); + + tab.view.draw(&printer); + } + } + + fn layout(&mut self, size: Vec2) { + if let Some(tab) = self.tabs.get_mut(self.selected) { + tab.view.layout(Vec2::new(size.x, size.y - 1)); + } + } +} + +impl ViewExt for TabView { + fn on_command( + &mut self, + s: &mut Cursive, + cmd: &str, + args: &[String], + ) -> Result { + if cmd == "move" { + if let Some(dir) = args.get(0) { + let amount: i32 = args + .get(1) + .unwrap_or(&"1".to_string()) + .parse() + .map_err(|e| format!("{:?}", e))?; + + let len = self.tabs.len(); + + if dir == "left" && self.selected > 0 { + self.move_focus(-amount); + return Ok(CommandResult::Consumed(None)); + } + + if dir == "right" && self.selected < len - 1 { + self.move_focus(amount); + return Ok(CommandResult::Consumed(None)); + } + } + } + + if let Some(tab) = self.tabs.get_mut(self.selected) { + tab.view.on_command(s, cmd, args) + } else { + Ok(CommandResult::Ignored) + } + } +}