Add rebindable keys, refactor lists
This commit is contained in:
387
src/commands.rs
387
src/commands.rs
@@ -1,11 +1,22 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use cursive::Cursive;
|
||||
use cursive::event::{Event, Key};
|
||||
|
||||
use queue::Queue;
|
||||
use spotify::Spotify;
|
||||
use playlists::Playlist;
|
||||
use track::Track;
|
||||
use ui::layout::Layout;
|
||||
use ui::listview::ListView;
|
||||
use ui::search::SearchView;
|
||||
|
||||
pub struct CommandManager {
|
||||
commands:
|
||||
HashMap<String, Box<dyn Fn(&mut Cursive, Vec<String>) -> Result<Option<String>, String>>>,
|
||||
aliases: HashMap<String, String>,
|
||||
callbacks: Vec<Box<dyn Fn() -> ()>>
|
||||
}
|
||||
|
||||
impl CommandManager {
|
||||
@@ -13,6 +24,7 @@ impl CommandManager {
|
||||
CommandManager {
|
||||
commands: HashMap::new(),
|
||||
aliases: HashMap::new(),
|
||||
callbacks: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +41,265 @@ impl CommandManager {
|
||||
self.commands.insert(name, cb);
|
||||
}
|
||||
|
||||
pub fn register_all(&mut self, spotify: Arc<Spotify>, queue: Arc<Queue>) {
|
||||
self.register(
|
||||
"quit",
|
||||
vec!["q", "x"],
|
||||
Box::new(move |s, _args| {
|
||||
s.quit();
|
||||
Ok(None)
|
||||
}),
|
||||
);
|
||||
|
||||
{
|
||||
let queue = queue.clone();
|
||||
self.register(
|
||||
"stop",
|
||||
Vec::new(),
|
||||
Box::new(move |_s, _args| {
|
||||
queue.stop();
|
||||
Ok(None)
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let queue = queue.clone();
|
||||
self.register(
|
||||
"previous",
|
||||
Vec::new(),
|
||||
Box::new(move |_s, _args| {
|
||||
queue.previous();
|
||||
Ok(None)
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let queue = queue.clone();
|
||||
self.register(
|
||||
"next",
|
||||
Vec::new(),
|
||||
Box::new(move |_s, _args| {
|
||||
queue.next();
|
||||
Ok(None)
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let queue = queue.clone();
|
||||
self.register(
|
||||
"clear",
|
||||
Vec::new(),
|
||||
Box::new(move |_s, _args| {
|
||||
queue.clear();
|
||||
Ok(None)
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let spotify = spotify.clone();
|
||||
self.register(
|
||||
"search",
|
||||
Vec::new(),
|
||||
Box::new(move |s, args| {
|
||||
s.call_on_id("main", |v: &mut Layout| {
|
||||
v.set_view("search");
|
||||
});
|
||||
s.call_on_id("search", |v: &mut SearchView| {
|
||||
if args.len() >= 1 {
|
||||
v.run_search(args.join(" "), spotify.clone());
|
||||
} else {
|
||||
v.focus_search();
|
||||
}
|
||||
});
|
||||
Ok(None)
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
self.register(
|
||||
"playlists",
|
||||
vec!["lists"],
|
||||
Box::new(move |s, _args| {
|
||||
s.call_on_id("main", |v: &mut Layout| {
|
||||
v.set_view("playlists");
|
||||
});
|
||||
Ok(None)
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
self.register(
|
||||
"log",
|
||||
Vec::new(),
|
||||
Box::new(move |s, _args| {
|
||||
s.call_on_id("main", |v: &mut Layout| {
|
||||
v.set_view("log");
|
||||
});
|
||||
Ok(None)
|
||||
}),
|
||||
);
|
||||
|
||||
self.register(
|
||||
"move",
|
||||
Vec::new(),
|
||||
Box::new(move |s, args| {
|
||||
if args.len() < 1 {
|
||||
return Err("Missing direction (up, down, left, right)".to_string());
|
||||
}
|
||||
|
||||
let dir = args.get(0).unwrap();
|
||||
|
||||
let amount: i32 = args.get(1).unwrap_or(&"1".to_string()).parse().map_err(|e| format!("{:?}", e))?;
|
||||
|
||||
if dir == "up" || dir == "down" {
|
||||
let dir = if dir == "up" { -1 } else { 1 };
|
||||
s.call_on_id("queue_list", |v: &mut ListView<Track>| {
|
||||
v.move_focus(dir * amount);
|
||||
});
|
||||
s.call_on_id("list", |v: &mut ListView<Track>| {
|
||||
v.move_focus(dir * amount);
|
||||
});
|
||||
s.call_on_id("list", |v: &mut ListView<Playlist>| {
|
||||
v.move_focus(dir * amount);
|
||||
});
|
||||
s.on_event(Event::Refresh);
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if dir == "left" || dir == "right" {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Err(format!("Unrecognized direction: {}", dir))
|
||||
})
|
||||
);
|
||||
|
||||
{
|
||||
let queue = queue.clone();
|
||||
self.register(
|
||||
"queue",
|
||||
Vec::new(),
|
||||
Box::new(move |s, args| {
|
||||
if let Some(arg) = args.get(0) {
|
||||
if arg != "selected" {
|
||||
return Err("".into());
|
||||
}
|
||||
} else {
|
||||
s.call_on_id("main", |v: &mut Layout| {
|
||||
v.set_view("queue");
|
||||
});
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
{
|
||||
let queue = queue.clone();
|
||||
s.call_on_id("list", |v: &mut ListView<Track>| {
|
||||
v.with_selected(Box::new(move |t| {
|
||||
queue.append(t);
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let queue = queue.clone();
|
||||
s.call_on_id("list", |v: &mut ListView<Playlist>| {
|
||||
v.with_selected(Box::new(move |pl| {
|
||||
for track in pl.tracks.iter() {
|
||||
queue.append(track);
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let queue = queue.clone();
|
||||
self.register(
|
||||
"play",
|
||||
vec!["pause", "toggle", "toggleplay", "toggleplayback"],
|
||||
Box::new(move |s, args| {
|
||||
if let Some(arg) = args.get(0) {
|
||||
if arg != "selected" {
|
||||
return Err("".into());
|
||||
}
|
||||
} else {
|
||||
queue.toggleplayback();
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
{
|
||||
let queue = queue.clone();
|
||||
s.call_on_id("queue_list", |v: &mut ListView<Track>| {
|
||||
v.get_selected_index().map(|i| queue.play(i));
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let queue = queue.clone();
|
||||
s.call_on_id("list", |v: &mut ListView<Track>| {
|
||||
v.with_selected(Box::new(move |t| {
|
||||
let index = queue.append_next(t);
|
||||
queue.play(index);
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let queue = queue.clone();
|
||||
s.call_on_id("list", |v: &mut ListView<Playlist>| {
|
||||
v.with_selected(Box::new(move |pl| {
|
||||
let indices: Vec<usize> = pl.tracks
|
||||
.iter()
|
||||
.map(|t| queue.append_next(t))
|
||||
.collect();
|
||||
if let Some(i) = indices.get(0) {
|
||||
queue.play(*i)
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let queue = queue.clone();
|
||||
self.register(
|
||||
"delete",
|
||||
Vec::new(),
|
||||
Box::new(move |s, args| {
|
||||
if let Some(arg) = args.get(0) {
|
||||
if arg != "selected" {
|
||||
return Err("".into());
|
||||
}
|
||||
} else {
|
||||
return Err("".into());
|
||||
}
|
||||
|
||||
{
|
||||
let queue = queue.clone();
|
||||
s.call_on_id("queue_list", |v: &mut ListView<Track>| {
|
||||
v.get_selected_index().map(|i| queue.remove(i));
|
||||
});
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_aliases(&self, name: &String) -> String {
|
||||
if let Some(s) = self.aliases.get(name) {
|
||||
self.handle_aliases(s)
|
||||
@@ -37,13 +308,125 @@ impl CommandManager {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle(&self, s: &mut Cursive, cmd: String) -> Result<Option<String>, String> {
|
||||
pub fn handle(&self, s: &mut Cursive, cmd: String) {
|
||||
let components: Vec<String> = cmd.split(' ').map(|s| s.to_string()).collect();
|
||||
|
||||
if let Some(cb) = self.commands.get(&self.handle_aliases(&components[0])) {
|
||||
let result = if let Some(cb) = self.commands.get(&self.handle_aliases(&components[0])) {
|
||||
cb(s, components[1..].to_vec())
|
||||
} else {
|
||||
Err("Unknown command.".to_string())
|
||||
};
|
||||
|
||||
// TODO: handle non-error output as well
|
||||
if let Err(e) = result {
|
||||
s.call_on_id("main", |v: &mut Layout| {
|
||||
v.set_error(e);
|
||||
});
|
||||
}
|
||||
|
||||
for cb in &self.callbacks {
|
||||
cb();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register_callback(&mut self, cb: Box<dyn Fn() -> ()>) {
|
||||
self.callbacks.push(cb);
|
||||
}
|
||||
|
||||
pub fn register_keybinding<'a, E: Into<cursive::event::Event>, S: Into<String>>(
|
||||
this: Arc<Self>,
|
||||
cursive: &'a mut Cursive,
|
||||
event: E,
|
||||
command: S,
|
||||
) {
|
||||
let cmd = command.into();
|
||||
cursive.add_global_callback(event, move |s| {
|
||||
this.handle(s, cmd.clone());
|
||||
});
|
||||
}
|
||||
|
||||
pub fn register_keybindings<'a>(
|
||||
this: Arc<Self>,
|
||||
cursive: &'a mut Cursive,
|
||||
keybindings: Option<HashMap<String, String>>
|
||||
) {
|
||||
let mut kb = Self::default_keybindings();
|
||||
kb.extend(keybindings.unwrap_or(HashMap::new()));
|
||||
|
||||
for (k, v) in kb {
|
||||
Self::register_keybinding(
|
||||
this.clone(),
|
||||
cursive,
|
||||
Self::parse_keybinding(k),
|
||||
v);
|
||||
}
|
||||
}
|
||||
|
||||
fn default_keybindings() -> HashMap<String, String> {
|
||||
let mut kb = HashMap::new();
|
||||
|
||||
kb.insert("q".into(), "quit".into());
|
||||
kb.insert("P".into(), "toggle".into());
|
||||
kb.insert("S".into(), "stop".into());
|
||||
kb.insert("<".into(), "previous".into());
|
||||
kb.insert(">".into(), "next".into());
|
||||
kb.insert("c".into(), "clear".into());
|
||||
kb.insert(" ".into(), "queue selected".into());
|
||||
kb.insert("Enter".into(), "play selected".into());
|
||||
kb.insert("d".into(), "delete selected".into());
|
||||
kb.insert("/".into(), "search".into());
|
||||
|
||||
kb.insert("F1".into(), "queue".into());
|
||||
kb.insert("F2".into(), "search".into());
|
||||
kb.insert("F3".into(), "playlists".into());
|
||||
kb.insert("F9".into(), "log".into());
|
||||
|
||||
kb.insert("Up".into(), "move up".into());
|
||||
kb.insert("Down".into(), "move down".into());
|
||||
kb.insert("Left".into(), "move left".into());
|
||||
kb.insert("Right".into(), "move right".into());
|
||||
kb.insert("k".into(), "move up".into());
|
||||
kb.insert("j".into(), "move down".into());
|
||||
kb.insert("h".into(), "move left".into());
|
||||
kb.insert("l".into(), "move right".into());
|
||||
|
||||
kb
|
||||
}
|
||||
|
||||
fn parse_keybinding(kb: String) -> cursive::event::Event {
|
||||
match kb.as_ref() {
|
||||
"Enter" => Event::Key(Key::Enter),
|
||||
"Tab" => Event::Key(Key::Tab),
|
||||
"Backspace" => Event::Key(Key::Backspace),
|
||||
"Esc" => Event::Key(Key::Esc),
|
||||
"Left" => Event::Key(Key::Left),
|
||||
"Right" => Event::Key(Key::Right),
|
||||
"Up" => Event::Key(Key::Up),
|
||||
"Down" => Event::Key(Key::Down),
|
||||
"Ins" => Event::Key(Key::Ins),
|
||||
"Del" => Event::Key(Key::Del),
|
||||
"Home" => Event::Key(Key::Home),
|
||||
"End" => Event::Key(Key::End),
|
||||
"PageUp" => Event::Key(Key::PageUp),
|
||||
"PageDown" => Event::Key(Key::PageDown),
|
||||
"PauseBreak" => Event::Key(Key::PauseBreak),
|
||||
"NumpadCenter" => Event::Key(Key::NumpadCenter),
|
||||
"F0" => Event::Key(Key::F0),
|
||||
"F1" => Event::Key(Key::F1),
|
||||
"F2" => Event::Key(Key::F2),
|
||||
"F3" => Event::Key(Key::F3),
|
||||
"F4" => Event::Key(Key::F4),
|
||||
"F5" => Event::Key(Key::F5),
|
||||
"F6" => Event::Key(Key::F6),
|
||||
"F7" => Event::Key(Key::F7),
|
||||
"F8" => Event::Key(Key::F8),
|
||||
"F9" => Event::Key(Key::F9),
|
||||
"F10" => Event::Key(Key::F10),
|
||||
"F11" => Event::Key(Key::F11),
|
||||
"F12" => Event::Key(Key::F12),
|
||||
s => {
|
||||
Event::Char(s.chars().next().unwrap())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub const CLIENT_ID: &str = "d420a117a32841c2b3474932e49fb54b";
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Default)]
|
||||
pub struct Config {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub keybindings: Option<HashMap<String, String>>,
|
||||
}
|
||||
|
||||
211
src/main.rs
211
src/main.rs
@@ -1,4 +1,5 @@
|
||||
extern crate crossbeam_channel;
|
||||
#[macro_use]
|
||||
extern crate cursive;
|
||||
extern crate failure;
|
||||
extern crate futures;
|
||||
@@ -28,12 +29,10 @@ use std::io::prelude::*;
|
||||
use std::path::PathBuf;
|
||||
use std::process;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use std::thread;
|
||||
|
||||
use cursive::event::Key;
|
||||
use cursive::traits::{Identifiable, View};
|
||||
use cursive::view::{ScrollStrategy, Selector};
|
||||
use cursive::traits::{Identifiable};
|
||||
use cursive::view::{ScrollStrategy};
|
||||
use cursive::views::*;
|
||||
use cursive::Cursive;
|
||||
|
||||
@@ -46,6 +45,7 @@ mod spotify;
|
||||
mod theme;
|
||||
mod track;
|
||||
mod ui;
|
||||
mod traits;
|
||||
|
||||
#[cfg(feature = "mpris")]
|
||||
mod mpris;
|
||||
@@ -81,19 +81,6 @@ fn init_logger(content: TextContent, write_to_file: bool) {
|
||||
}
|
||||
}
|
||||
|
||||
fn register_keybinding<E: Into<cursive::event::Event>, S: Into<String>>(
|
||||
cursive: &mut Cursive,
|
||||
ev: &EventManager,
|
||||
event: E,
|
||||
command: S,
|
||||
) {
|
||||
let ev = ev.clone();
|
||||
let cmd = command.into();
|
||||
cursive.add_global_callback(event, move |_s| {
|
||||
ev.send(Event::Command(cmd.clone()));
|
||||
});
|
||||
}
|
||||
|
||||
fn main() {
|
||||
std::env::set_var("RUST_LOG", "ncspot=trace");
|
||||
std::env::set_var("RUST_BACKTRACE", "full");
|
||||
@@ -131,7 +118,6 @@ fn main() {
|
||||
cursive.set_theme(theme::default());
|
||||
|
||||
let event_manager = EventManager::new(cursive.cb_sink().clone());
|
||||
let mut cmd_manager = CommandManager::new();
|
||||
|
||||
let spotify = Arc::new(spotify::Spotify::new(
|
||||
event_manager.clone(),
|
||||
@@ -140,13 +126,13 @@ fn main() {
|
||||
config::CLIENT_ID.to_string(),
|
||||
));
|
||||
|
||||
let queue = Arc::new(Mutex::new(queue::Queue::new(
|
||||
let queue = Arc::new(queue::Queue::new(
|
||||
event_manager.clone(),
|
||||
spotify.clone(),
|
||||
)));
|
||||
));
|
||||
|
||||
#[cfg(feature = "mpris")]
|
||||
let mpris_manager = mpris::MprisManager::new(spotify.clone(), queue.clone());
|
||||
let mpris_manager = Arc::new(mpris::MprisManager::new(spotify.clone(), queue.clone()));
|
||||
|
||||
let search = ui::search::SearchView::new(spotify.clone(), queue.clone());
|
||||
|
||||
@@ -167,23 +153,19 @@ fn main() {
|
||||
});
|
||||
}
|
||||
|
||||
let mut playlists_view = ui::playlist::PlaylistView::new(&playlists, queue.clone());
|
||||
let playlistsview = ui::playlist::PlaylistView::new(&playlists, queue.clone());
|
||||
|
||||
let mut queueview = ui::queue::QueueView::new(queue.clone());
|
||||
let queueview = ui::queue::QueueView::new(queue.clone());
|
||||
|
||||
let logview_scroller = ScrollView::new(logview).scroll_strategy(ScrollStrategy::StickToBottom);
|
||||
|
||||
let status = ui::statusbar::StatusBar::new(queue.clone(), spotify.clone());
|
||||
|
||||
let mut layout = ui::layout::Layout::new(status, &event_manager)
|
||||
.view("search", search.view.with_id("search"), "Search")
|
||||
.view("search", search.with_id("search"), "Search")
|
||||
.view("log", logview_scroller, "Log")
|
||||
.view(
|
||||
"playlists",
|
||||
playlists_view.view.take().unwrap(),
|
||||
"Playlists",
|
||||
)
|
||||
.view("queue", queueview.view.take().unwrap(), "Queue");
|
||||
.view("playlists", playlistsview, "Playlists")
|
||||
.view("queue", queueview, "Queue");
|
||||
|
||||
// initial view is queue
|
||||
layout.set_view("queue");
|
||||
@@ -206,151 +188,20 @@ fn main() {
|
||||
|
||||
cursive.add_fullscreen_layer(layout.with_id("main"));
|
||||
|
||||
// Register commands
|
||||
cmd_manager.register(
|
||||
"quit",
|
||||
vec!["q", "x"],
|
||||
Box::new(move |s, _args| {
|
||||
s.quit();
|
||||
Ok(None)
|
||||
}),
|
||||
);
|
||||
let mut cmd_manager = CommandManager::new();
|
||||
cmd_manager.register_all(spotify.clone(), queue.clone());
|
||||
|
||||
#[cfg(feature = "mpris")]
|
||||
{
|
||||
let queue = queue.clone();
|
||||
cmd_manager.register(
|
||||
"toggleplayback",
|
||||
vec!["toggleplay", "toggle", "play", "pause"],
|
||||
Box::new(move |_s, _args| {
|
||||
queue.lock().expect("could not lock queue").toggleplayback();
|
||||
Ok(None)
|
||||
}),
|
||||
);
|
||||
let mpris_manager = mpris_manager.clone();
|
||||
cmd_manager.register_callback(Box::new(move || {
|
||||
mpris_manager.update();
|
||||
}));
|
||||
}
|
||||
|
||||
{
|
||||
let queue = queue.clone();
|
||||
cmd_manager.register(
|
||||
"stop",
|
||||
Vec::new(),
|
||||
Box::new(move |_s, _args| {
|
||||
queue.lock().expect("could not lock queue").stop();
|
||||
Ok(None)
|
||||
}),
|
||||
);
|
||||
}
|
||||
let cmd_manager = Arc::new(cmd_manager);
|
||||
|
||||
{
|
||||
let queue = queue.clone();
|
||||
cmd_manager.register(
|
||||
"previous",
|
||||
Vec::new(),
|
||||
Box::new(move |_s, _args| {
|
||||
queue.lock().expect("could not lock queue").previous();
|
||||
Ok(None)
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let queue = queue.clone();
|
||||
cmd_manager.register(
|
||||
"next",
|
||||
Vec::new(),
|
||||
Box::new(move |_s, _args| {
|
||||
queue.lock().expect("could not lock queue").next();
|
||||
Ok(None)
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let queue = queue.clone();
|
||||
cmd_manager.register(
|
||||
"clear",
|
||||
Vec::new(),
|
||||
Box::new(move |_s, _args| {
|
||||
queue.lock().expect("could not lock queue").clear();
|
||||
Ok(None)
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
cmd_manager.register(
|
||||
"queue",
|
||||
Vec::new(),
|
||||
Box::new(move |s, _args| {
|
||||
s.call_on_id("main", |v: &mut ui::layout::Layout| {
|
||||
v.set_view("queue");
|
||||
});
|
||||
Ok(None)
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
cursive.add_global_callback(Key::F1, move |s| {
|
||||
s.call_on_id("main", |v: &mut ui::layout::Layout| {
|
||||
v.set_view("queue");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
cmd_manager.register(
|
||||
"search",
|
||||
Vec::new(),
|
||||
Box::new(move |s, args| {
|
||||
s.call_on_id("main", |v: &mut ui::layout::Layout| {
|
||||
v.set_view("search");
|
||||
});
|
||||
s.call_on_id("search", |v: &mut LinearLayout| {
|
||||
v.focus_view(&Selector::Id("search_edit")).unwrap();
|
||||
});
|
||||
if args.len() >= 1 {
|
||||
s.call_on_id("search_edit", |v: &mut EditView| {
|
||||
v.set_content(args.join(" "));
|
||||
});
|
||||
}
|
||||
Ok(None)
|
||||
}),
|
||||
);
|
||||
|
||||
{
|
||||
cmd_manager.register(
|
||||
"playlists",
|
||||
vec!["lists"],
|
||||
Box::new(move |s, _args| {
|
||||
s.call_on_id("main", |v: &mut ui::layout::Layout| {
|
||||
v.set_view("playlists");
|
||||
});
|
||||
Ok(None)
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
cmd_manager.register(
|
||||
"log",
|
||||
Vec::new(),
|
||||
Box::new(move |s, _args| {
|
||||
s.call_on_id("main", |v: &mut ui::layout::Layout| {
|
||||
v.set_view("log");
|
||||
});
|
||||
Ok(None)
|
||||
}),
|
||||
);
|
||||
|
||||
register_keybinding(&mut cursive, &event_manager, 'q', "quit");
|
||||
register_keybinding(&mut cursive, &event_manager, 'P', "toggle");
|
||||
register_keybinding(&mut cursive, &event_manager, 'S', "stop");
|
||||
register_keybinding(&mut cursive, &event_manager, '<', "previous");
|
||||
register_keybinding(&mut cursive, &event_manager, '>', "next");
|
||||
register_keybinding(&mut cursive, &event_manager, 'c', "clear");
|
||||
|
||||
register_keybinding(&mut cursive, &event_manager, Key::F1, "queue");
|
||||
register_keybinding(&mut cursive, &event_manager, Key::F2, "search");
|
||||
register_keybinding(&mut cursive, &event_manager, Key::F3, "playlists");
|
||||
register_keybinding(&mut cursive, &event_manager, Key::F9, "log");
|
||||
CommandManager::register_keybindings(cmd_manager.clone(), &mut cursive, cfg.keybindings);
|
||||
|
||||
// cursive event loop
|
||||
while cursive.is_running() {
|
||||
@@ -360,30 +211,18 @@ fn main() {
|
||||
match event {
|
||||
Event::Player(state) => {
|
||||
if state == PlayerEvent::FinishedTrack {
|
||||
queue.lock().expect("could not lock queue").next();
|
||||
queue.next();
|
||||
}
|
||||
spotify.update_status(state);
|
||||
|
||||
#[cfg(feature = "mpris")]
|
||||
mpris_manager.update();
|
||||
}
|
||||
Event::Playlist(event) => playlists_view.handle_ev(&mut cursive, event),
|
||||
Event::Playlist(_event) => (),
|
||||
Event::Command(cmd) => {
|
||||
// TODO: handle non-error output as well
|
||||
if let Err(e) = cmd_manager.handle(&mut cursive, cmd) {
|
||||
cursive.call_on_id("main", |v: &mut ui::layout::Layout| {
|
||||
v.set_error(e);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(feature = "mpris")]
|
||||
mpris_manager.update();
|
||||
cmd_manager.handle(&mut cursive, cmd);
|
||||
}
|
||||
Event::ScreenChange(name) => match name.as_ref() {
|
||||
"playlists" => playlists_view.repopulate(&mut cursive),
|
||||
"queue" => queueview.repopulate(&mut cursive),
|
||||
_ => (),
|
||||
},
|
||||
Event::ScreenChange(_name) => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
17
src/mpris.rs
17
src/mpris.rs
@@ -1,6 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
use std::sync::{mpsc, Arc, Mutex};
|
||||
use std::sync::{mpsc, Arc};
|
||||
|
||||
use dbus::arg::{RefArg, Variant};
|
||||
use dbus::stdintf::org_freedesktop_dbus::PropertiesPropertiesChanged;
|
||||
@@ -19,11 +19,11 @@ fn get_playbackstatus(spotify: Arc<Spotify>) -> String {
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn get_metadata(queue: Arc<Mutex<Queue>>) -> HashMap<String, Variant<Box<RefArg>>> {
|
||||
fn get_metadata(queue: Arc<Queue>) -> HashMap<String, Variant<Box<RefArg>>> {
|
||||
let mut hm: HashMap<String, Variant<Box<RefArg>>> = HashMap::new();
|
||||
|
||||
let queue = queue.lock().expect("could not lock queue");
|
||||
let track = queue.get_current();
|
||||
let t = queue.get_current();
|
||||
let track = t.as_ref(); // TODO
|
||||
|
||||
hm.insert(
|
||||
"mpris:trackid".to_string(),
|
||||
@@ -86,7 +86,7 @@ fn get_metadata(queue: Arc<Mutex<Queue>>) -> HashMap<String, Variant<Box<RefArg>
|
||||
hm
|
||||
}
|
||||
|
||||
fn run_dbus_server(spotify: Arc<Spotify>, queue: Arc<Mutex<Queue>>, rx: mpsc::Receiver<()>) {
|
||||
fn run_dbus_server(spotify: Arc<Spotify>, queue: Arc<Queue>, rx: mpsc::Receiver<()>) {
|
||||
let conn = Rc::new(
|
||||
dbus::Connection::get_private(dbus::BusType::Session).expect("Failed to connect to dbus"),
|
||||
);
|
||||
@@ -322,7 +322,7 @@ fn run_dbus_server(spotify: Arc<Spotify>, queue: Arc<Mutex<Queue>>, rx: mpsc::Re
|
||||
let method_next = {
|
||||
let queue = queue.clone();
|
||||
f.method("Next", (), move |m| {
|
||||
queue.lock().expect("failed to lock queue").next();
|
||||
queue.next();
|
||||
Ok(vec![m.msg.method_return()])
|
||||
})
|
||||
};
|
||||
@@ -330,7 +330,7 @@ fn run_dbus_server(spotify: Arc<Spotify>, queue: Arc<Mutex<Queue>>, rx: mpsc::Re
|
||||
let method_previous = {
|
||||
let queue = queue.clone();
|
||||
f.method("Previous", (), move |m| {
|
||||
queue.lock().expect("failed to lock queue").previous();
|
||||
queue.previous();
|
||||
Ok(vec![m.msg.method_return()])
|
||||
})
|
||||
};
|
||||
@@ -397,12 +397,13 @@ fn run_dbus_server(spotify: Arc<Spotify>, queue: Arc<Mutex<Queue>>, rx: mpsc::Re
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MprisManager {
|
||||
tx: mpsc::Sender<()>,
|
||||
}
|
||||
|
||||
impl MprisManager {
|
||||
pub fn new(spotify: Arc<Spotify>, queue: Arc<Mutex<Queue>>) -> Self {
|
||||
pub fn new(spotify: Arc<Spotify>, queue: Arc<Queue>) -> Self {
|
||||
let (tx, rx) = mpsc::channel::<()>();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
|
||||
@@ -5,8 +5,10 @@ use std::sync::{Arc, RwLock};
|
||||
use rspotify::spotify::model::playlist::SimplifiedPlaylist;
|
||||
|
||||
use events::{Event, EventManager};
|
||||
use queue::Queue;
|
||||
use spotify::Spotify;
|
||||
use track::Track;
|
||||
use traits::ListItem;
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
pub struct Playlist {
|
||||
@@ -25,15 +27,36 @@ pub struct PlaylistStore {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Playlists {
|
||||
pub store: Arc<RwLock<PlaylistStore>>,
|
||||
pub store: Arc<RwLock<Vec<Playlist>>>,
|
||||
ev: EventManager,
|
||||
spotify: Arc<Spotify>,
|
||||
}
|
||||
|
||||
impl ListItem for Playlist {
|
||||
fn is_playing(&self, queue: Arc<Queue>) -> bool {
|
||||
let playing: Vec<String> = queue.queue
|
||||
.read()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|t| t.id.clone())
|
||||
.collect();
|
||||
let ids: Vec<String> = self.tracks.iter().map(|t| t.id.clone()).collect();
|
||||
ids.len() > 0 && playing == ids
|
||||
}
|
||||
|
||||
fn display_left(&self) -> String {
|
||||
self.meta.name.clone()
|
||||
}
|
||||
|
||||
fn display_right(&self) -> String {
|
||||
format!("{} tracks", self.tracks.len())
|
||||
}
|
||||
}
|
||||
|
||||
impl Playlists {
|
||||
pub fn new(ev: &EventManager, spotify: &Arc<Spotify>) -> Playlists {
|
||||
Playlists {
|
||||
store: Arc::new(RwLock::new(PlaylistStore::default())),
|
||||
store: Arc::new(RwLock::new(Vec::new())),
|
||||
ev: ev.clone(),
|
||||
spotify: spotify.clone(),
|
||||
}
|
||||
@@ -51,8 +74,8 @@ impl Playlists {
|
||||
if let Ok(cache) = parsed {
|
||||
debug!("playlist cache loaded ({} lists)", cache.playlists.len());
|
||||
let mut store = self.store.write().expect("can't writelock playlist store");
|
||||
store.playlists.clear();
|
||||
store.playlists.extend(cache.playlists);
|
||||
store.clear();
|
||||
store.extend(cache.playlists);
|
||||
|
||||
// force refresh of UI (if visible)
|
||||
self.ev.send(Event::ScreenChange("playlists".to_owned()));
|
||||
@@ -104,11 +127,11 @@ impl Playlists {
|
||||
}
|
||||
|
||||
fn needs_download(&self, remote: &SimplifiedPlaylist) -> bool {
|
||||
for local in &self
|
||||
for local in self
|
||||
.store
|
||||
.read()
|
||||
.expect("can't readlock playlists")
|
||||
.playlists
|
||||
.iter()
|
||||
{
|
||||
if local.meta.id == remote.id {
|
||||
return local.meta.snapshot_id != remote.snapshot_id;
|
||||
@@ -119,14 +142,14 @@ impl Playlists {
|
||||
|
||||
fn append_or_update(&self, updated: &Playlist) -> usize {
|
||||
let mut store = self.store.write().expect("can't writelock playlists");
|
||||
for (index, mut local) in store.playlists.iter_mut().enumerate() {
|
||||
for (index, mut local) in store.iter_mut().enumerate() {
|
||||
if local.meta.id == updated.meta.id {
|
||||
*local = updated.clone();
|
||||
return index;
|
||||
}
|
||||
}
|
||||
store.playlists.push(updated.clone());
|
||||
store.playlists.len() - 1
|
||||
store.push(updated.clone());
|
||||
store.len() - 1
|
||||
}
|
||||
|
||||
pub fn fetch_playlists(&self) {
|
||||
|
||||
84
src/queue.rs
84
src/queue.rs
@@ -1,14 +1,12 @@
|
||||
use std::slice::Iter;
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use events::{Event, EventManager};
|
||||
use spotify::Spotify;
|
||||
use track::Track;
|
||||
|
||||
pub struct Queue {
|
||||
// TODO: put this in an RwLock instead of locking the whole Queue struct
|
||||
queue: Vec<Track>,
|
||||
current_track: Option<usize>,
|
||||
pub queue: Arc<RwLock<Vec<Track>>>,
|
||||
current_track: RwLock<Option<usize>>,
|
||||
spotify: Arc<Spotify>,
|
||||
ev: EventManager,
|
||||
}
|
||||
@@ -16,18 +14,18 @@ pub struct Queue {
|
||||
impl Queue {
|
||||
pub fn new(ev: EventManager, spotify: Arc<Spotify>) -> Queue {
|
||||
Queue {
|
||||
queue: Vec::new(),
|
||||
current_track: None,
|
||||
queue: Arc::new(RwLock::new(Vec::new())),
|
||||
current_track: RwLock::new(None),
|
||||
spotify: spotify,
|
||||
ev: ev,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_index(&self) -> Option<usize> {
|
||||
match self.current_track {
|
||||
match *self.current_track.read().unwrap() {
|
||||
Some(index) => {
|
||||
let next_index = index + 1;
|
||||
if next_index < self.queue.len() {
|
||||
if next_index < self.queue.read().unwrap().len() {
|
||||
Some(next_index)
|
||||
} else {
|
||||
None
|
||||
@@ -38,7 +36,7 @@ impl Queue {
|
||||
}
|
||||
|
||||
pub fn previous_index(&self) -> Option<usize> {
|
||||
match self.current_track {
|
||||
match *self.current_track.read().unwrap() {
|
||||
Some(index) => {
|
||||
if index > 0 {
|
||||
Some(index - 1)
|
||||
@@ -50,33 +48,41 @@ impl Queue {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_current(&self) -> Option<&Track> {
|
||||
match self.current_track {
|
||||
Some(index) => Some(&self.queue[index]),
|
||||
pub fn get_current(&self) -> Option<Track> {
|
||||
match *self.current_track.read().unwrap() {
|
||||
Some(index) => Some(self.queue.read().unwrap()[index].clone()),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn append(&mut self, track: &Track) {
|
||||
self.queue.push(track.clone());
|
||||
pub fn append(&self, track: &Track) {
|
||||
let mut q = self.queue.write().unwrap();
|
||||
q.push(track.clone());
|
||||
}
|
||||
|
||||
pub fn append_next(&mut self, track: &Track) -> usize {
|
||||
if let Some(next_index) = self.next_index() {
|
||||
self.queue.insert(next_index, track.clone());
|
||||
pub fn append_next(&self, track: &Track) -> usize {
|
||||
let next = self.next_index();
|
||||
let mut q = self.queue.write().unwrap();
|
||||
|
||||
if let Some(next_index) = next {
|
||||
q.insert(next_index, track.clone());
|
||||
next_index
|
||||
} else {
|
||||
self.queue.push(track.clone());
|
||||
self.queue.len() - 1
|
||||
q.push(track.clone());
|
||||
q.len() - 1
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, index: usize) {
|
||||
self.queue.remove(index);
|
||||
pub fn remove(&self, index: usize) {
|
||||
{
|
||||
let mut q = self.queue.write().unwrap();
|
||||
q.remove(index);
|
||||
}
|
||||
|
||||
// if the queue is empty or we are at the end of the queue, stop
|
||||
// playback
|
||||
if self.queue.len() == 0 || index == self.queue.len() {
|
||||
let len = self.queue.read().unwrap().len();
|
||||
if len == 0 || index == len {
|
||||
self.stop();
|
||||
return;
|
||||
}
|
||||
@@ -84,27 +90,32 @@ impl Queue {
|
||||
// if we are deleting the currently playing track, play the track with
|
||||
// the same index again, because the next track is now at the position
|
||||
// of the one we deleted
|
||||
if let Some(current_track) = self.current_track {
|
||||
let current = *self.current_track.read().unwrap();
|
||||
if let Some(current_track) = current {
|
||||
if index == current_track {
|
||||
self.play(index);
|
||||
} else if index < current_track {
|
||||
self.current_track = Some(current_track - 1);
|
||||
let mut current = self.current_track.write().unwrap();
|
||||
current.replace(current_track - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
pub fn clear(&self) {
|
||||
self.stop();
|
||||
self.queue.clear();
|
||||
|
||||
let mut q = self.queue.write().unwrap();
|
||||
q.clear();
|
||||
|
||||
// redraw queue if open
|
||||
self.ev.send(Event::ScreenChange("queue".to_owned()));
|
||||
}
|
||||
|
||||
pub fn play(&mut self, index: usize) {
|
||||
let track = &self.queue[index];
|
||||
pub fn play(&self, index: usize) {
|
||||
let track = &self.queue.read().unwrap()[index];
|
||||
self.spotify.load(&track);
|
||||
self.current_track = Some(index);
|
||||
let mut current = self.current_track.write().unwrap();
|
||||
current.replace(index);
|
||||
self.spotify.play();
|
||||
self.spotify.update_track();
|
||||
}
|
||||
@@ -113,12 +124,13 @@ impl Queue {
|
||||
self.spotify.toggleplayback();
|
||||
}
|
||||
|
||||
pub fn stop(&mut self) {
|
||||
self.current_track = None;
|
||||
pub fn stop(&self) {
|
||||
let mut current = self.current_track.write().unwrap();
|
||||
*current = None;
|
||||
self.spotify.stop();
|
||||
}
|
||||
|
||||
pub fn next(&mut self) {
|
||||
pub fn next(&self) {
|
||||
if let Some(index) = self.next_index() {
|
||||
self.play(index);
|
||||
} else {
|
||||
@@ -126,15 +138,11 @@ impl Queue {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn previous(&mut self) {
|
||||
pub fn previous(&self) {
|
||||
if let Some(index) = self.previous_index() {
|
||||
self.play(index);
|
||||
} else {
|
||||
self.spotify.stop();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> Iter<Track> {
|
||||
self.queue.iter()
|
||||
}
|
||||
}
|
||||
|
||||
19
src/track.rs
19
src/track.rs
@@ -1,7 +1,11 @@
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use rspotify::spotify::model::track::FullTrack;
|
||||
|
||||
use queue::Queue;
|
||||
use traits::ListItem;
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
pub struct Track {
|
||||
pub id: String,
|
||||
@@ -73,3 +77,18 @@ impl fmt::Debug for Track {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ListItem for Track {
|
||||
fn is_playing(&self, queue: Arc<Queue>) -> bool {
|
||||
let current = queue.get_current();
|
||||
current.map(|t| t.id == self.id).unwrap_or(false)
|
||||
}
|
||||
|
||||
fn display_left(&self) -> String {
|
||||
format!("{}", self)
|
||||
}
|
||||
|
||||
fn display_right(&self) -> String {
|
||||
self.duration_str()
|
||||
}
|
||||
}
|
||||
|
||||
9
src/traits.rs
Normal file
9
src/traits.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use queue::Queue;
|
||||
|
||||
pub trait ListItem {
|
||||
fn is_playing(&self, queue: Arc<Queue>) -> bool;
|
||||
fn display_left(&self) -> String;
|
||||
fn display_right(&self) -> String;
|
||||
}
|
||||
97
src/ui/listview.rs
Normal file
97
src/ui/listview.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
use std::cmp::{min, max};
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use cursive::align::HAlign;
|
||||
use cursive::theme::ColorStyle;
|
||||
use cursive::traits::View;
|
||||
use cursive::{Printer, Vec2};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use queue::Queue;
|
||||
use traits::ListItem;
|
||||
|
||||
pub struct ListView<I: 'static + ListItem> {
|
||||
content: Arc<RwLock<Vec<I>>>,
|
||||
selected: Option<usize>,
|
||||
queue: Arc<Queue>,
|
||||
}
|
||||
|
||||
impl<I: ListItem> ListView<I> {
|
||||
pub fn new(content: Arc<RwLock<Vec<I>>>, queue: Arc<Queue>) -> Self {
|
||||
Self {
|
||||
content: content,
|
||||
selected: None,
|
||||
queue: queue,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_selected(&self, cb: Box<Fn(&I) -> ()>) {
|
||||
if let Some(i) = self.selected {
|
||||
if let Some(x) = self.content.read().unwrap().get(i) {
|
||||
cb(x);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_selected_index(&self) -> Option<usize> {
|
||||
self.selected
|
||||
}
|
||||
|
||||
pub fn move_focus(&mut self, delta: i32) {
|
||||
let len = self.content.read().unwrap().len() as i32;
|
||||
|
||||
let new = if let Some(i) = self.selected {
|
||||
i as i32
|
||||
} else {
|
||||
if delta < 0 { len } else { -1 }
|
||||
};
|
||||
|
||||
let new = min(max(new + delta, 0), len - 1);
|
||||
|
||||
self.selected = Some(new as usize);
|
||||
}
|
||||
}
|
||||
|
||||
impl<I: ListItem> View for ListView<I> {
|
||||
fn draw(&self, printer: &Printer<'_, '_>) {
|
||||
for (i, item) in self.content.read().unwrap().iter().enumerate() {
|
||||
let style = if self.selected.is_some() && self.selected.unwrap_or(0) == i {
|
||||
ColorStyle::highlight()
|
||||
} else if item.is_playing(self.queue.clone()) {
|
||||
ColorStyle::secondary()
|
||||
} else {
|
||||
ColorStyle::primary()
|
||||
};
|
||||
|
||||
let left = item.display_left();
|
||||
let right = item.display_right();
|
||||
|
||||
// draw left string
|
||||
printer.with_color(style, |printer| {
|
||||
printer.print((0, i), &left);
|
||||
});
|
||||
|
||||
// draw ".." to indicate a cut off string
|
||||
let max_length = printer.size.x
|
||||
.checked_sub(right.width() + 1)
|
||||
.unwrap_or(0);
|
||||
if max_length < left.width() {
|
||||
let offset = max_length.checked_sub(1).unwrap_or(0);
|
||||
printer.with_color(style, |printer| {
|
||||
printer.print((offset, i), "..");
|
||||
});
|
||||
}
|
||||
|
||||
// draw right string
|
||||
let offset = HAlign::Right.get_offset(right.width(), printer.size.x);
|
||||
|
||||
printer.with_color(style, |printer| {
|
||||
printer.print((offset, i), &right);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn required_size(&mut self, constraint: Vec2) -> Vec2 {
|
||||
Vec2::new(constraint.x, self.content.read().unwrap().len())
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,5 @@ pub mod layout;
|
||||
pub mod playlist;
|
||||
pub mod queue;
|
||||
pub mod search;
|
||||
pub mod splitbutton;
|
||||
pub mod statusbar;
|
||||
pub mod trackbutton;
|
||||
pub mod listview;
|
||||
|
||||
@@ -1,108 +1,28 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::Arc;
|
||||
|
||||
use cursive::direction::Orientation;
|
||||
use cursive::event::Key;
|
||||
use cursive::traits::Boxable;
|
||||
use cursive::traits::Identifiable;
|
||||
use cursive::views::*;
|
||||
use cursive::Cursive;
|
||||
use cursive::view::{ViewWrapper};
|
||||
use cursive::views::{IdView, ScrollView};
|
||||
|
||||
use playlists::{Playlist, PlaylistEvent, Playlists};
|
||||
use playlists::{Playlist, Playlists};
|
||||
use queue::Queue;
|
||||
use ui::splitbutton::SplitButton;
|
||||
use ui::listview::ListView;
|
||||
|
||||
pub struct PlaylistView {
|
||||
pub view: Option<BoxView<ScrollView<IdView<LinearLayout>>>>,
|
||||
queue: Arc<Mutex<Queue>>,
|
||||
playlists: Playlists,
|
||||
list: ScrollView<IdView<ListView<Playlist>>>
|
||||
}
|
||||
|
||||
impl PlaylistView {
|
||||
pub fn new(playlists: &Playlists, queue: Arc<Mutex<Queue>>) -> PlaylistView {
|
||||
let playlists_view = LinearLayout::new(Orientation::Vertical).with_id("playlists");
|
||||
let scrollable = ScrollView::new(playlists_view).full_screen();
|
||||
pub fn new(playlists: &Playlists, queue: Arc<Queue>) -> PlaylistView {
|
||||
let list = ListView::new(playlists.store.clone(), queue).with_id("list");
|
||||
let scrollable = ScrollView::new(list);
|
||||
|
||||
PlaylistView {
|
||||
view: Some(scrollable),
|
||||
queue: queue,
|
||||
playlists: playlists.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn create_button(&self, playlist: &Playlist) -> SplitButton {
|
||||
let trackcount = format!("{} tracks", playlist.tracks.len());
|
||||
let mut button = SplitButton::new(&playlist.meta.name, &trackcount);
|
||||
|
||||
// <enter> plays the selected playlist
|
||||
{
|
||||
let queue_ref = self.queue.clone();
|
||||
let playlist = playlist.clone();
|
||||
button.add_callback(Key::Enter, move |_s| {
|
||||
let mut locked_queue = queue_ref.lock().expect("could not acquire lock");
|
||||
let mut first_played = false;
|
||||
for track in playlist.tracks.iter() {
|
||||
let index = locked_queue.append_next(track);
|
||||
if !first_played {
|
||||
locked_queue.play(index);
|
||||
first_played = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// <space> queues the selected playlist
|
||||
{
|
||||
let queue_ref = self.queue.clone();
|
||||
let playlist = playlist.clone();
|
||||
button.add_callback(' ', move |_s| {
|
||||
let mut locked_queue = queue_ref.lock().expect("could not acquire lock");
|
||||
for track in playlist.tracks.iter() {
|
||||
locked_queue.append(track);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
button
|
||||
}
|
||||
|
||||
fn clear_playlists(&self, playlists: &mut ViewRef<LinearLayout>) {
|
||||
while playlists.len() > 0 {
|
||||
playlists.remove_child(0);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn repopulate(&self, cursive: &mut Cursive) {
|
||||
let view_ref: Option<ViewRef<LinearLayout>> = cursive.find_id("playlists");
|
||||
if let Some(mut playlists) = view_ref {
|
||||
self.clear_playlists(&mut playlists);
|
||||
|
||||
let playlist_store = &self
|
||||
.playlists
|
||||
.store
|
||||
.read()
|
||||
.expect("can't readlock playlists");
|
||||
info!("repopulating {} lists", playlist_store.playlists.len());
|
||||
for list in &playlist_store.playlists {
|
||||
let button = self.create_button(&list);
|
||||
playlists.add_child(button);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_ev(&self, cursive: &mut Cursive, event: PlaylistEvent) {
|
||||
let view_ref: Option<ViewRef<LinearLayout>> = cursive.find_id("playlists");
|
||||
|
||||
if let Some(mut playlists) = view_ref {
|
||||
match event {
|
||||
PlaylistEvent::NewList(index, list) => {
|
||||
let button = self.create_button(&list);
|
||||
|
||||
if let Some(_) = playlists.get_child(index) {
|
||||
playlists.remove_child(index);
|
||||
}
|
||||
playlists.insert_child(index, button);
|
||||
}
|
||||
}
|
||||
list: scrollable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ViewWrapper for PlaylistView {
|
||||
wrap_impl!(self.list: ScrollView<IdView<ListView<Playlist>>>);
|
||||
}
|
||||
|
||||
@@ -1,89 +1,28 @@
|
||||
use cursive::direction::Orientation;
|
||||
use cursive::event::Key;
|
||||
use cursive::traits::Boxable;
|
||||
use cursive::traits::Identifiable;
|
||||
use cursive::views::*;
|
||||
use cursive::Cursive;
|
||||
use cursive::view::{ViewWrapper};
|
||||
use cursive::views::{IdView, ScrollView};
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use queue::Queue;
|
||||
use track::Track;
|
||||
use ui::splitbutton::SplitButton;
|
||||
use ui::trackbutton::TrackButton;
|
||||
use ui::listview::ListView;
|
||||
|
||||
pub struct QueueView {
|
||||
pub view: Option<BoxView<ScrollView<IdView<LinearLayout>>>>,
|
||||
queue: Arc<Mutex<Queue>>,
|
||||
list: ScrollView<IdView<ListView<Track>>>
|
||||
}
|
||||
|
||||
impl QueueView {
|
||||
pub fn new(queue: Arc<Mutex<Queue>>) -> QueueView {
|
||||
let queuelist = LinearLayout::new(Orientation::Vertical).with_id("queue_list");
|
||||
let scrollable = ScrollView::new(queuelist).full_screen();
|
||||
pub fn new(queue: Arc<Queue>) -> QueueView {
|
||||
let list = ListView::new(queue.queue.clone(), queue.clone()).with_id("queue_list");
|
||||
let scrollable = ScrollView::new(list);
|
||||
|
||||
QueueView {
|
||||
view: Some(scrollable),
|
||||
queue: queue,
|
||||
}
|
||||
}
|
||||
|
||||
fn cb_delete(cursive: &mut Cursive, queue: &mut Queue) {
|
||||
let view_ref: Option<ViewRef<LinearLayout>> = cursive.find_id("queue_list");
|
||||
if let Some(mut queuelist) = view_ref {
|
||||
let index = queuelist.get_focus_index();
|
||||
queue.remove(index);
|
||||
queuelist.remove_child(index);
|
||||
}
|
||||
}
|
||||
|
||||
fn cb_play(cursive: &mut Cursive, queue: &mut Queue) {
|
||||
let view_ref: Option<ViewRef<LinearLayout>> = cursive.find_id("queue_list");
|
||||
if let Some(queuelist) = view_ref {
|
||||
let index = queuelist.get_focus_index();
|
||||
queue.play(index);
|
||||
}
|
||||
}
|
||||
|
||||
fn create_button(&self, track: &Track) -> SplitButton {
|
||||
let mut button = TrackButton::new(&track);
|
||||
// 'd' deletes the selected track
|
||||
{
|
||||
let queue_ref = self.queue.clone();
|
||||
button.add_callback('d', move |cursive| {
|
||||
Self::cb_delete(
|
||||
cursive,
|
||||
&mut queue_ref.lock().expect("could not lock queue"),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// <enter> plays the selected track
|
||||
{
|
||||
let queue_ref = self.queue.clone();
|
||||
button.add_callback(Key::Enter, move |cursive| {
|
||||
Self::cb_play(
|
||||
cursive,
|
||||
&mut queue_ref.lock().expect("could not lock queue"),
|
||||
);
|
||||
});
|
||||
}
|
||||
button
|
||||
}
|
||||
|
||||
pub fn repopulate(&self, cursive: &mut Cursive) {
|
||||
let view_ref: Option<ViewRef<LinearLayout>> = cursive.find_id("queue_list");
|
||||
if let Some(mut queuelist) = view_ref {
|
||||
while queuelist.len() > 0 {
|
||||
queuelist.remove_child(0);
|
||||
}
|
||||
|
||||
let queue = self.queue.lock().expect("could not lock queue");
|
||||
for track in queue.iter() {
|
||||
let button = self.create_button(track);
|
||||
queuelist.add_child(button);
|
||||
}
|
||||
list: scrollable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ViewWrapper for QueueView {
|
||||
wrap_impl!(self.list: ScrollView<IdView<ListView<Track>>>);
|
||||
}
|
||||
|
||||
163
src/ui/search.rs
163
src/ui/search.rs
@@ -1,81 +1,120 @@
|
||||
#![allow(unused_imports)]
|
||||
|
||||
use cursive::direction::Orientation;
|
||||
use cursive::event::Key;
|
||||
use cursive::traits::Boxable;
|
||||
use cursive::traits::Identifiable;
|
||||
use cursive::views::*;
|
||||
use cursive::Cursive;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use cursive::event::{AnyCb, Event, EventResult, Key};
|
||||
use cursive::traits::{Boxable, Identifiable, Finder, View};
|
||||
use cursive::view::{Selector, ViewWrapper};
|
||||
use cursive::views::{EditView, IdView, ScrollView, ViewRef};
|
||||
use cursive::{Cursive, Printer, Vec2};
|
||||
use std::cell::RefCell;
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
|
||||
use queue::Queue;
|
||||
use spotify::Spotify;
|
||||
use track::Track;
|
||||
use ui::trackbutton::TrackButton;
|
||||
use ui::listview::ListView;
|
||||
|
||||
pub struct SearchView {
|
||||
pub view: LinearLayout,
|
||||
results: Arc<RwLock<Vec<Track>>>,
|
||||
edit: IdView<EditView>,
|
||||
list: ScrollView<IdView<ListView<Track>>>,
|
||||
edit_focused: bool,
|
||||
}
|
||||
|
||||
impl SearchView {
|
||||
fn search_handler(
|
||||
s: &mut Cursive,
|
||||
input: &str,
|
||||
spotify: Arc<Spotify>,
|
||||
queue: Arc<Mutex<Queue>>,
|
||||
) {
|
||||
let mut results: ViewRef<ListView> = s.find_id("search_results").unwrap();
|
||||
let tracks = spotify.search(input, 50, 0);
|
||||
pub fn new(spotify: Arc<Spotify>, queue: Arc<Queue>) -> SearchView {
|
||||
let results = Arc::new(RwLock::new(Vec::new()));
|
||||
|
||||
results.clear();
|
||||
|
||||
if let Ok(tracks) = tracks {
|
||||
for search_track in tracks.tracks.items {
|
||||
let track = Track::new(&search_track);
|
||||
let mut button = TrackButton::new(&track);
|
||||
|
||||
// <enter> plays the selected track
|
||||
{
|
||||
let queue = queue.clone();
|
||||
let track = track.clone();
|
||||
button.add_callback(Key::Enter, move |_cursive| {
|
||||
let mut queue = queue.lock().unwrap();
|
||||
let index = queue.append_next(&track);
|
||||
queue.play(index);
|
||||
});
|
||||
}
|
||||
|
||||
// <space> queues the selected track
|
||||
{
|
||||
let queue = queue.clone();
|
||||
let track = track.clone();
|
||||
button.add_callback(' ', move |_cursive| {
|
||||
let mut queue = queue.lock().unwrap();
|
||||
queue.append(&track);
|
||||
});
|
||||
}
|
||||
|
||||
results.add_child("", button);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(spotify: Arc<Spotify>, queue: Arc<Mutex<Queue>>) -> SearchView {
|
||||
let queue_ref = queue.clone();
|
||||
let searchfield = EditView::new()
|
||||
.on_submit(move |s, input| {
|
||||
if input.len() > 0 {
|
||||
Self::search_handler(s, input, spotify.clone(), queue_ref.clone());
|
||||
s.call_on_id("search", |v: &mut SearchView| {
|
||||
v.run_search(input, spotify.clone());
|
||||
v.focus_view(&Selector::Id("list")).unwrap();
|
||||
});
|
||||
}
|
||||
})
|
||||
.with_id("search_edit")
|
||||
.full_width()
|
||||
.fixed_height(1);
|
||||
let results = ListView::new().with_id("search_results").full_width();
|
||||
let scrollable = ScrollView::new(results).full_width().full_height();
|
||||
let layout = LinearLayout::new(Orientation::Vertical)
|
||||
.child(searchfield)
|
||||
.child(scrollable);
|
||||
.with_id("search_edit");
|
||||
let list = ListView::new(results.clone(), queue).with_id("list");
|
||||
let scrollable = ScrollView::new(list);
|
||||
|
||||
return SearchView { view: layout };
|
||||
SearchView {
|
||||
results: results,
|
||||
edit: searchfield,
|
||||
list: scrollable,
|
||||
edit_focused: false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_search<S: Into<String>>(&mut self, query: S, spotify: Arc<Spotify>) {
|
||||
let query = query.into();
|
||||
let q = query.clone();
|
||||
self.edit.call_on(&Selector::Id("search_edit"), |v: &mut EditView| {
|
||||
v.set_content(q);
|
||||
});
|
||||
|
||||
if let Ok(results) = spotify.search(&query, 50, 0) {
|
||||
let tracks = results.tracks.items.iter().map(|ft| Track::new(ft)).collect();
|
||||
let mut r = self.results.write().unwrap();
|
||||
*r = tracks;
|
||||
self.edit_focused = false;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn focus_search(&mut self) {
|
||||
self.edit.call_on(&Selector::Id("search_edit"), |v: &mut EditView| {
|
||||
v.set_content("");
|
||||
});
|
||||
self.edit_focused = true;
|
||||
}
|
||||
}
|
||||
|
||||
impl View for SearchView {
|
||||
fn draw(&self, printer: &Printer<'_, '_>) {
|
||||
{
|
||||
let printer = &printer
|
||||
.offset((0, 0))
|
||||
.cropped((printer.size.x, 1))
|
||||
.focused(self.edit_focused);
|
||||
self.edit.draw(printer);
|
||||
}
|
||||
|
||||
let printer = &printer
|
||||
.offset((0, 1))
|
||||
.cropped((printer.size.x, printer.size.y - 1))
|
||||
.focused(!self.edit_focused);
|
||||
self.list.draw(printer);
|
||||
}
|
||||
|
||||
fn layout(&mut self, size: Vec2) {
|
||||
self.edit.layout(Vec2::new(size.x, 1));
|
||||
self.list.layout(Vec2::new(size.x, size.y - 1));
|
||||
}
|
||||
|
||||
fn call_on_any<'a>(&mut self, selector: &Selector<'_>, mut callback: AnyCb<'a>) {
|
||||
self.edit.call_on_any(selector, Box::new(|v| callback(v)));
|
||||
self.list.call_on_any(selector, Box::new(|v| callback(v)));
|
||||
}
|
||||
|
||||
fn focus_view(&mut self, selector: &Selector<'_>) -> Result<(), ()> {
|
||||
if let Selector::Id(s) = selector {
|
||||
self.edit_focused = s == &"search_edit";
|
||||
Ok(())
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
||||
fn on_event(&mut self, event: Event) -> EventResult {
|
||||
if self.edit_focused {
|
||||
if event == Event::Key(Key::Esc) {
|
||||
self.edit_focused = false;
|
||||
EventResult::Consumed(None)
|
||||
} else {
|
||||
self.edit.on_event(event)
|
||||
}
|
||||
} else {
|
||||
self.list.on_event(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
use cursive::align::HAlign;
|
||||
use cursive::direction::Direction;
|
||||
use cursive::event::{Callback, Event, EventResult, EventTrigger};
|
||||
use cursive::theme::ColorStyle;
|
||||
use cursive::traits::View;
|
||||
use cursive::vec::Vec2;
|
||||
use cursive::Cursive;
|
||||
use cursive::Printer;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
pub struct SplitButton {
|
||||
callbacks: Vec<(EventTrigger, Callback)>,
|
||||
|
||||
left: String,
|
||||
right: String,
|
||||
|
||||
enabled: bool,
|
||||
last_size: Vec2,
|
||||
invalidated: bool,
|
||||
}
|
||||
|
||||
impl SplitButton {
|
||||
pub fn new(left: &str, right: &str) -> SplitButton {
|
||||
SplitButton {
|
||||
callbacks: Vec::new(),
|
||||
left: left.to_owned(),
|
||||
right: right.to_owned(),
|
||||
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 SplitButton {
|
||||
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()
|
||||
};
|
||||
|
||||
// draw left string
|
||||
printer.with_color(style, |printer| {
|
||||
printer.print((0, 0), &self.left);
|
||||
});
|
||||
|
||||
// draw ".." to indicate a cut off string
|
||||
let max_length = printer
|
||||
.size
|
||||
.x
|
||||
.checked_sub(self.right.width() + 1)
|
||||
.unwrap_or(0);
|
||||
if max_length < self.left.width() {
|
||||
let offset = max_length.checked_sub(1).unwrap_or(0);
|
||||
printer.with_color(style, |printer| {
|
||||
printer.print((offset, 0), "..");
|
||||
});
|
||||
}
|
||||
|
||||
// draw right string
|
||||
let offset = HAlign::Right.get_offset(self.right.width(), printer.size.x);
|
||||
|
||||
printer.with_color(style, |printer| {
|
||||
printer.print((offset, 0), &self.right);
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::{Arc};
|
||||
|
||||
use cursive::align::HAlign;
|
||||
use cursive::theme::ColorStyle;
|
||||
@@ -11,12 +11,12 @@ use queue::Queue;
|
||||
use spotify::{PlayerEvent, Spotify};
|
||||
|
||||
pub struct StatusBar {
|
||||
queue: Arc<Mutex<Queue>>,
|
||||
queue: Arc<Queue>,
|
||||
spotify: Arc<Spotify>,
|
||||
}
|
||||
|
||||
impl StatusBar {
|
||||
pub fn new(queue: Arc<Mutex<Queue>>, spotify: Arc<Spotify>) -> StatusBar {
|
||||
pub fn new(queue: Arc<Queue>, spotify: Arc<Spotify>) -> StatusBar {
|
||||
StatusBar {
|
||||
queue: queue,
|
||||
spotify: spotify,
|
||||
@@ -55,12 +55,7 @@ impl View for StatusBar {
|
||||
printer.print((0, 1), &state_icon);
|
||||
});
|
||||
|
||||
if let Some(ref t) = self
|
||||
.queue
|
||||
.lock()
|
||||
.expect("could not lock queue")
|
||||
.get_current()
|
||||
{
|
||||
if let Some(ref t) = self.queue.get_current() {
|
||||
let elapsed = self.spotify.get_current_progress();
|
||||
let formatted_elapsed = format!(
|
||||
"{:02}:{:02}",
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
use track::Track;
|
||||
use ui::splitbutton::SplitButton;
|
||||
|
||||
pub struct TrackButton {}
|
||||
|
||||
impl TrackButton {
|
||||
pub fn new(track: &Track) -> SplitButton {
|
||||
let button = SplitButton::new(&track.to_string(), &track.duration_str());
|
||||
button
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user