diff --git a/README.md b/README.md index 89c2bb7..115bbfb 100644 --- a/README.md +++ b/README.md @@ -36,10 +36,11 @@ have them configurable. * Navigate through the screens using the F-keys: * `F1`: Queue + * `c` clears the entire queue + * `d` deletes the currently selected track + * `s` opens a dialog to save the queue to a playlist * `F2`: Search * `F3`: Playlists - * `d` deletes the currently selected track - * `c` clears the entire playlist * Tracks and playlists can be played using `Return` and queued using `Space` * `Shift-p` toggles playback of a track * `Shift-s` stops a track diff --git a/src/main.rs b/src/main.rs index 7571208..5c72131 100644 --- a/src/main.rs +++ b/src/main.rs @@ -78,7 +78,7 @@ fn setup_logging(filename: &str) -> Result<(), fern::InitError> { fn main() { let matches = App::new("ncspot") .version("0.1.0") - .author("Henrik Friedrichsen ") + .author("Henrik Friedrichsen and contributors") .about("cross-platform ncurses Spotify client") .arg( Arg::with_name("debug") @@ -152,7 +152,7 @@ fn main() { let playlistsview = ui::playlists::PlaylistView::new(&playlists, queue.clone()); - let queueview = ui::queue::QueueView::new(queue.clone()); + let queueview = ui::queue::QueueView::new(queue.clone(), playlists.clone()); let status = ui::statusbar::StatusBar::new(queue.clone(), spotify.clone()); diff --git a/src/playlists.rs b/src/playlists.rs index c707928..0599f24 100644 --- a/src/playlists.rs +++ b/src/playlists.rs @@ -1,7 +1,7 @@ use std::iter::Iterator; use std::ops::Deref; use std::path::PathBuf; -use std::sync::{Arc, RwLock}; +use std::sync::{Arc, RwLock, RwLockReadGuard}; use rspotify::spotify::model::playlist::SimplifiedPlaylist; @@ -58,6 +58,12 @@ impl Playlists { } } + pub fn items(&self) -> RwLockReadGuard> { + self.store + .read() + .expect("could not readlock listview content") + } + pub fn load_cache(&self) { if let Ok(contents) = std::fs::read_to_string(&self.cache_path) { debug!( @@ -142,6 +148,22 @@ impl Playlists { store.len() - 1 } + pub fn overwrite_playlist(&self, id: &str, tracks: &Vec) { + debug!("saving {} tracks to {}", tracks.len(), id); + self.spotify.overwrite_playlist(id, &tracks); + + self.fetch_playlists(); + self.save_cache(); + } + + pub fn save_playlist(&self, name: &str, tracks: &Vec) { + debug!("saving {} tracks to new list {}", tracks.len(), name); + match self.spotify.create_playlist(name, None, None) { + Some(id) => self.overwrite_playlist(&id, &tracks), + None => error!("could not create new playlist.."), + } + } + pub fn fetch_playlists(&self) { debug!("loading playlists"); let mut stale_lists = self.store.read().unwrap().clone(); diff --git a/src/theme.rs b/src/theme.rs index 228c1c5..919d025 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -17,7 +17,7 @@ macro_rules! load_color { pub fn load(cfg: &Config) -> Theme { let mut palette = Palette::default(); - let borders = BorderStyle::None; + let borders = BorderStyle::Simple; palette[Background] = load_color!(cfg, background, TerminalDefault); palette[View] = load_color!(cfg, background, TerminalDefault); diff --git a/src/ui/listview.rs b/src/ui/listview.rs index 1b399e8..aa860a1 100644 --- a/src/ui/listview.rs +++ b/src/ui/listview.rs @@ -1,5 +1,5 @@ use std::cmp::{max, min}; -use std::sync::{Arc, RwLock}; +use std::sync::{Arc, RwLock, RwLockReadGuard}; use cursive::align::HAlign; use cursive::event::{Event, EventResult, MouseButton, MouseEvent}; @@ -54,6 +54,12 @@ impl ListView { let new = self.selected as i32 + delta; self.move_focus_to(max(new, 0) as usize); } + + pub fn content(&self) -> RwLockReadGuard> { + self.content + .read() + .expect("could not readlock listview content") + } } impl View for ListView { diff --git a/src/ui/queue.rs b/src/ui/queue.rs index 9e31b3a..317aeef 100644 --- a/src/ui/queue.rs +++ b/src/ui/queue.rs @@ -1,25 +1,98 @@ -use cursive::traits::Identifiable; +use cursive::event::{Callback, Event, EventResult}; +use cursive::traits::{Boxable, Identifiable, View}; use cursive::view::ViewWrapper; -use cursive::views::IdView; +use cursive::views::{Dialog, EditView, IdView, ScrollView, SelectView}; +use cursive::Cursive; use std::sync::Arc; +use playlists::Playlists; use queue::Queue; use track::Track; use ui::listview::ListView; +use ui::modal::Modal; pub struct QueueView { list: IdView>, + playlists: Arc, } impl QueueView { - pub fn new(queue: Arc) -> QueueView { + pub fn new(queue: Arc, playlists: Arc) -> QueueView { let list = ListView::new(queue.queue.clone(), queue.clone()).with_id("queue_list"); - QueueView { list: list } + QueueView { + list: list, + playlists: playlists, + } + } + + fn save_dialog_cb(s: &mut Cursive, playlists: Arc, id: Option) { + let tracks = s + .call_on_id("queue_list", |view: &mut ListView<_>| { + view.content().clone() + }) + .unwrap(); + match id { + Some(id) => { + playlists.overwrite_playlist(&id, &tracks); + s.pop_layer(); + } + None => { + s.pop_layer(); + let edit = EditView::new() + .on_submit(move |s: &mut Cursive, name| { + playlists.save_playlist(name, &tracks); + s.pop_layer(); + }) + .with_id("name") + .fixed_width(20); + let dialog = Dialog::new() + .title("Enter name") + .dismiss_button("Cancel") + .padding((1, 1, 1, 0)) + .content(edit); + s.add_layer(Modal::new(dialog)); + } + } + } + + fn save_dialog(playlists: Arc) -> Modal { + let mut list_select: SelectView> = SelectView::new().autojump(); + list_select.add_item("[Create new]", None); + + for ref list in playlists.items().iter() { + list_select.add_item(list.meta.name.clone(), Some(list.meta.id.clone())); + } + + list_select.set_on_submit(move |s, selected| { + Self::save_dialog_cb(s, playlists.clone(), selected.clone()) + }); + + let dialog = Dialog::new() + .title("Save to existing or new playlist?") + .dismiss_button("Cancel") + .padding((1, 1, 1, 0)) + .content(ScrollView::new(list_select)); + Modal::new(dialog) } } impl ViewWrapper for QueueView { wrap_impl!(self.list: IdView>); + + fn wrap_on_event(&mut self, ch: Event) -> EventResult { + match ch { + Event::Char('s') => { + debug!("save list"); + let playlists = self.playlists.clone(); + let cb = move |s: &mut Cursive| { + let dialog = Self::save_dialog(playlists.clone()); + s.add_layer(dialog) + }; + EventResult::Consumed(Some(Callback::from_fn(cb))) + } + _ => self.list.on_event(ch), + } + } }