Skip to content

Commit

Permalink
refactor: Library.ArtistList
Browse files Browse the repository at this point in the history
feat: Library.on_select_songs_fn + other small quality-of-life improvements
  • Loading branch information
lautarodragan committed Oct 9, 2024
1 parent e43ed7b commit 24a534f
Show file tree
Hide file tree
Showing 10 changed files with 364 additions and 107 deletions.
9 changes: 9 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,15 @@ impl<'a> App<'a> {
}
}
});
library.on_select_songs_fn({
let player = player.clone();
let library = library.clone();

move |songs| {
player.enqueue_songs(songs);
library.on_key(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)); // hackish way to "select_next()"
}
});

let playlist = Arc::new(ui::Playlists::new(config.theme, state.playlists));
playlist.on_select({
Expand Down
1 change: 1 addition & 0 deletions src/components/library.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ pub mod widget;
pub mod keyboard_handler;

mod song_list;
mod artist_list;

pub use library::*;
5 changes: 5 additions & 0 deletions src/components/library/artist_list.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mod artist_list;
mod widget;
mod keyboard_handler;

pub use artist_list::ArtistList;
83 changes: 83 additions & 0 deletions src/components/library/artist_list/artist_list.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
use std::{
cmp::Ordering,
collections::{HashMap, HashSet},
sync::{
atomic::{AtomicUsize, Ordering as AtomicOrdering},
Mutex,
Arc,
},
path::PathBuf,
};

use crossterm::event::KeyEvent;

use crate::{
config::Theme,
};

pub struct ArtistList<'a> {
pub(super) theme: Theme,

pub artists: Mutex<Vec<String>>,
pub(super) selected_index: AtomicUsize,

pub(super) on_select_fn: Mutex<Box<dyn FnMut(String) + 'a>>,
pub(super) on_confirm_fn: Mutex<Box<dyn FnMut(String) + 'a>>,
pub(super) on_delete_fn: Mutex<Box<dyn FnMut(String) + 'a>>,

pub(super) offset: AtomicUsize,
pub(super) height: AtomicUsize,
}

impl<'a> ArtistList<'a> {
pub fn new(theme: Theme) -> Self {
Self {
theme,

on_select_fn: Mutex::new(Box::new(|_| {}) as _),
on_confirm_fn: Mutex::new(Box::new(|_| {}) as _),
on_delete_fn: Mutex::new(Box::new(|_| {}) as _),

artists: Mutex::new(Vec::new()),
selected_index: AtomicUsize::new(0),

offset: AtomicUsize::new(0),
height: AtomicUsize::new(0),
}
}

pub fn on_select(&self, cb: impl FnMut(String) + 'a) {
*self.on_select_fn.lock().unwrap() = Box::new(cb);
}

pub fn on_confirm(&self, cb: impl FnMut(String) + 'a) {
*self.on_confirm_fn.lock().unwrap() = Box::new(cb);
}

pub fn on_delete(&self, cb: impl FnMut(String) + 'a) {
*self.on_delete_fn.lock().unwrap() = Box::new(cb);
}

pub fn set_artists(&self, artists: Vec<String>) {
*self.artists.lock().unwrap() = artists;
}

pub fn selected_artist(&self) -> String {
let artists = self.artists.lock().unwrap();

let i = self.selected_index.load(AtomicOrdering::SeqCst);

if i >= artists.len() {
log::error!(target: "::ArtistList.selected_artist()", "selected_index > artists.len");
return "".to_string();
}

artists[i].clone()
}
}

impl Drop for ArtistList<'_> {
fn drop(&mut self) {
log::trace!("Artists.drop()");
}
}
126 changes: 126 additions & 0 deletions src/components/library/artist_list/keyboard_handler.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
use std::sync::atomic::Ordering;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};

use crate::{
ui::KeyboardHandlerRef,
};

use super::artist_list::ArtistList;

impl<'a> KeyboardHandlerRef<'a> for ArtistList<'a> {

fn on_key(&self, key: KeyEvent) -> bool {
let target = "::ArtistList.on_key";
log::trace!(target: target, "{:?}", key);

match key.code {
KeyCode::Up | KeyCode::Down | KeyCode::Home | KeyCode::End => {
self.on_artist_list_directional_key(key);
let artist = self.selected_artist();
self.on_select_fn.lock().unwrap()(artist);
},
KeyCode::Enter => {
let artist = self.selected_artist();
self.on_confirm_fn.lock().unwrap()(artist);
},
KeyCode::Delete => {
let mut artists = self.artists.lock().unwrap();
let i = self.selected_index.load(Ordering::SeqCst);

let removed_artist = artists.remove(i);

self.on_delete_fn.lock().unwrap()(removed_artist);

let i = i.saturating_sub(1).min(artists.len().saturating_sub(1));

self.selected_index.store(i, Ordering::SeqCst);
self.offset.store(0, Ordering::SeqCst);

let artist = artists[i].clone();

drop(artists);

self.on_select_fn.lock().unwrap()(artist);
},
KeyCode::Char(char) => {
let artists = self.artists.lock().unwrap();
let char_up = char.to_lowercase().to_string();
let char_low = char.to_uppercase().to_string();

let Some(i) = artists.iter().position(|a|
a.starts_with(char) ||
a.starts_with(char_up.as_str()) ||
a.starts_with(char_low.as_str())
) else {
return false;
};

self.selected_index.store(i, Ordering::SeqCst);

let artist = artists[i].clone();
drop(artists);

self.on_select_fn.lock().unwrap()(artist);
}
_ => {},
}

true
}
}


impl<'a> ArtistList<'a> {

fn on_artist_list_directional_key(&self, key: KeyEvent) {
let artists = self.artists.lock().unwrap();
let length = artists.len() as i32;

let height = self.height.load(Ordering::Relaxed) as i32;
let padding = 5;

let mut offset = self.offset.load(Ordering::SeqCst) as i32;
let mut i = self.selected_index.load(Ordering::SeqCst) as i32;

match key.code {
KeyCode::Up | KeyCode::Down => {
if key.code == KeyCode::Up {
i -= 1;
} else {
i += 1;
}

let padding = if key.code == KeyCode::Up {
padding
} else {
height.saturating_sub(padding).saturating_sub(1)
};

if (key.code == KeyCode::Up && i < offset + padding) || (key.code == KeyCode::Down && i > offset + padding) {
offset = if i > padding {
i - padding
} else {
0
};
}

},
KeyCode::Home => {
i = 0;
offset = 0;
},
KeyCode::End => {
i = length - 1;
offset = i - height + padding;
},
_ => {},
}

offset = offset.min(length - height).max(0);
i = i.min(length - 1).max(0);

self.offset.store(offset as usize, Ordering::SeqCst);
self.selected_index.store(i as usize, Ordering::SeqCst);
}

}
59 changes: 59 additions & 0 deletions src/components/library/artist_list/widget.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use std::sync::atomic::Ordering;

use ratatui::{
prelude::Widget,
buffer::Buffer,
layout::{Constraint, Layout, Rect},
style::{Color, Style},
widgets::{WidgetRef},
};

use super::artist_list::ArtistList;

fn line_style(theme: &crate::config::Theme, index: usize, selected_index: usize, list_has_focus: bool) -> Style {
if index == selected_index {
if list_has_focus {
Style::default().fg(theme.foreground_selected).bg(theme.background_selected)
} else {
Style::default().fg(theme.foreground_selected).bg(theme.background_selected_blur)
}
} else {
Style::default().fg(theme.foreground_secondary).bg(theme.background)
}
}

impl<'a> WidgetRef for ArtistList<'a> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
self.height.store(area.height as usize, Ordering::Relaxed);

let artists = &self.artists.lock().unwrap();

if artists.len() < 1 {
return;
}

let selected_index = self.selected_index.load(Ordering::Relaxed);
let offset = self.offset.load(Ordering::Relaxed);

for i in 0..artists.len().min(area.height as usize) {
let artist_index = i + offset;

if artist_index >= artists.len() {
log::error!("artist index {artist_index} > artists.len() {} offset={offset}", artists.len());
break;
}

let artist = artists[artist_index].clone();
let area = Rect {
y: area.y + i as u16,
height: 1,
..area
};

let style = line_style(&self.theme, artist_index, selected_index, true);
let line = ratatui::text::Line::from(artist).style(style);

line.render_ref(area, buf);
}
}
}
68 changes: 9 additions & 59 deletions src/components/library/keyboard_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,74 +11,24 @@ impl<'a> KeyboardHandlerRef<'a> for Library<'a> {

fn on_key(&self, key: KeyEvent) -> bool {
log::trace!(target: "::library.on_key", "start {:?}", key);
let focused_element_guard = self.focused_element();

let focused_element = self.focused_element();

match key.code {
KeyCode::Tab => {
self.set_focused_element(match focused_element_guard {
self.set_focused_element(match focused_element {
LibraryScreenElement::ArtistList => LibraryScreenElement::SongList,
LibraryScreenElement::SongList => LibraryScreenElement::ArtistList,
});
true
}
_ if focused_element_guard == LibraryScreenElement::ArtistList => {
self.on_key_event_artist_list(key);
},
_ if focused_element_guard == LibraryScreenElement::SongList => {
self.song_list.on_key(key);
_ if focused_element == LibraryScreenElement::ArtistList => {
self.artist_list.on_key(key)
},
_ => {
return false;
_ if focused_element == LibraryScreenElement::SongList => {
self.song_list.on_key(key)
},
_ => false,
}

true
}
}


impl<'a> Library<'a> {

fn on_key_event_artist_list(&self, key: KeyEvent) {
let mut artists = self.artists.lock().unwrap();
let len = artists.len();

match key.code {
KeyCode::Up => {
let _ = self.selected_artist_index.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |a| { Some(a.saturating_sub(1)) });
self.selected_song_index.store(0, Ordering::SeqCst);
self.offset.store(0, Ordering::SeqCst);
},
KeyCode::Down => {
let _ = self.selected_artist_index.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |a| { Some(a.saturating_add(1).min(len.saturating_sub(1))) });
self.selected_song_index.store(0, Ordering::SeqCst);
self.offset.store(0, Ordering::SeqCst);
},
KeyCode::Home => {
self.selected_artist_index.store(0, Ordering::SeqCst);
self.selected_song_index.store(0, Ordering::SeqCst);
self.offset.store(0, Ordering::SeqCst);
},
KeyCode::End => {
self.selected_artist_index.store(len.saturating_sub(1), Ordering::SeqCst);
self.selected_song_index.store(0, Ordering::SeqCst);
self.offset.store(0, Ordering::SeqCst);
},
KeyCode::Delete => {
let removed_artist = artists.remove(self.selected_artist_index.load(Ordering::SeqCst));
let mut songs = self.songs.lock().unwrap();
songs.remove(removed_artist.as_str());
self.offset.store(0, Ordering::SeqCst);
let _ = self.selected_artist_index.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |a| { Some(a.saturating_sub(1).min(len.saturating_sub(1))) });
},
_ => {},
}


let artist = artists[self.selected_artist_index.load(Ordering::SeqCst)].as_str();
let songs = self.songs.lock().unwrap();
let artist_songs = songs.get(artist).unwrap();
let artist_songs = artist_songs.iter().map(|s| s.clone()).collect();
self.song_list.set_songs(artist_songs);
}

}
Loading

0 comments on commit 24a534f

Please sign in to comment.