Add tabview, add playlists to search
This commit is contained in:
@@ -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);
|
debug!("got list: {}", list.name);
|
||||||
let id = list.id.clone();
|
let id = list.id.clone();
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use rspotify::spotify::client::ApiError;
|
|||||||
use rspotify::spotify::client::Spotify as SpotifyAPI;
|
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, SearchPlaylists};
|
||||||
|
|
||||||
use failure::Error;
|
use failure::Error;
|
||||||
|
|
||||||
@@ -428,10 +428,14 @@ impl Spotify {
|
|||||||
result.map(|r| r.id)
|
result.map(|r| r.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn search(&self, query: &str, limit: u32, offset: u32) -> Option<SearchTracks> {
|
pub fn search_track(&self, query: &str, limit: u32, offset: u32) -> Option<SearchTracks> {
|
||||||
self.api_with_retry(|api| api.search_track(query, limit, offset, None))
|
self.api_with_retry(|api| api.search_track(query, limit, offset, None))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn search_playlist(&self, query: &str, limit: u32, offset: u32) -> Option<SearchPlaylists> {
|
||||||
|
self.api_with_retry(|api| api.search_playlist(query, limit, offset, None))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn current_user_playlist(
|
pub fn current_user_playlist(
|
||||||
&self,
|
&self,
|
||||||
limit: u32,
|
limit: u32,
|
||||||
|
|||||||
@@ -5,3 +5,4 @@ pub mod playlists;
|
|||||||
pub mod queue;
|
pub mod queue;
|
||||||
pub mod search;
|
pub mod search;
|
||||||
pub mod statusbar;
|
pub mod statusbar;
|
||||||
|
pub mod tabview;
|
||||||
|
|||||||
@@ -10,16 +10,19 @@ use std::cell::RefCell;
|
|||||||
use std::sync::{Arc, Mutex, RwLock};
|
use std::sync::{Arc, Mutex, RwLock};
|
||||||
|
|
||||||
use commands::CommandResult;
|
use commands::CommandResult;
|
||||||
|
use playlists::{Playlist, Playlists};
|
||||||
use queue::Queue;
|
use queue::Queue;
|
||||||
use spotify::Spotify;
|
use spotify::Spotify;
|
||||||
use track::Track;
|
use track::Track;
|
||||||
use traits::ViewExt;
|
use traits::ViewExt;
|
||||||
use ui::listview::ListView;
|
use ui::listview::ListView;
|
||||||
|
use ui::tabview::TabView;
|
||||||
|
|
||||||
pub struct SearchView {
|
pub struct SearchView {
|
||||||
results: Arc<RwLock<Vec<Track>>>,
|
results_tracks: Arc<RwLock<Vec<Track>>>,
|
||||||
|
results_playlists: Arc<RwLock<Vec<Playlist>>>,
|
||||||
edit: IdView<EditView>,
|
edit: IdView<EditView>,
|
||||||
list: IdView<ListView<Track>>,
|
list: IdView<TabView>,
|
||||||
edit_focused: bool,
|
edit_focused: bool,
|
||||||
spotify: Arc<Spotify>,
|
spotify: Arc<Spotify>,
|
||||||
}
|
}
|
||||||
@@ -28,7 +31,8 @@ pub const LIST_ID: &str = "search_list";
|
|||||||
pub const EDIT_ID: &str = "search_edit";
|
pub const EDIT_ID: &str = "search_edit";
|
||||||
impl SearchView {
|
impl SearchView {
|
||||||
pub fn new(spotify: Arc<Spotify>, queue: Arc<Queue>) -> SearchView {
|
pub fn new(spotify: Arc<Spotify>, queue: Arc<Queue>) -> 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()
|
let searchfield = EditView::new()
|
||||||
.on_submit(move |s, input| {
|
.on_submit(move |s, input| {
|
||||||
@@ -40,14 +44,18 @@ impl SearchView {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.with_id(EDIT_ID);
|
.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 {
|
SearchView {
|
||||||
results,
|
results_tracks,
|
||||||
|
results_playlists,
|
||||||
edit: searchfield,
|
edit: searchfield,
|
||||||
list,
|
list: tabs.with_id(LIST_ID),
|
||||||
edit_focused: true,
|
edit_focused: true,
|
||||||
spotify,
|
spotify
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,17 +74,29 @@ impl SearchView {
|
|||||||
v.set_content(q);
|
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
|
let tracks = results
|
||||||
.tracks
|
.tracks
|
||||||
.items
|
.items
|
||||||
.iter()
|
.iter()
|
||||||
.map(|ft| Track::new(ft))
|
.map(|ft| Track::new(ft))
|
||||||
.collect();
|
.collect();
|
||||||
let mut r = self.results.write().unwrap();
|
let mut r = self.results_tracks.write().unwrap();
|
||||||
*r = tracks;
|
*r = tracks;
|
||||||
self.edit_focused = false;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
139
src/ui/tabview.rs
Normal file
139
src/ui/tabview.rs
Normal file
@@ -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<dyn ViewExt>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TabView {
|
||||||
|
tabs: Vec<Tab>,
|
||||||
|
ids: HashMap<String, usize>,
|
||||||
|
selected: usize
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TabView {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
tabs: Vec::new(),
|
||||||
|
ids: HashMap::new(),
|
||||||
|
selected: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_tab<S: Into<String>, 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<S: Into<String>, 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<CommandResult, String> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user