From 692b97a783a38375d11486c8fff79a71dace38b7 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Tue, 11 Apr 2023 14:53:27 +0200 Subject: [PATCH] First dynamic version --- src/tui/app.rs | 165 ++++++++++++++++++++++++++++++++++++++++++--- src/tui/handler.rs | 18 ++++- src/tui/ui.rs | 81 ++++++++++++++-------- 3 files changed, 226 insertions(+), 38 deletions(-) diff --git a/src/tui/app.rs b/src/tui/app.rs index ada31ca..36e96dd 100644 --- a/src/tui/app.rs +++ b/src/tui/app.rs @@ -1,31 +1,178 @@ -use musichoard::collection::{CollectionManager, Collection}; +use musichoard::{collection::CollectionManager, Album, AlbumId, Artist, ArtistId, Track}; use super::Error; -/// Application. +#[derive(Copy, Clone)] +pub enum Category { + Artist, + Album, + Track, +} + +pub struct Selection { + active: Category, + artist: u16, + album: u16, + track: u16, +} + +impl Default for Selection { + fn default() -> Self { + Selection { + active: Category::Artist, + artist: 0, + album: 0, + track: 0, + } + } +} + pub struct App { collection_manager: CollectionManager, + selection: Selection, running: bool, } impl App { - /// Constructs a new instance of [`App`]. pub fn new(mut collection_manager: CollectionManager) -> Result { collection_manager.rescan_library()?; - Ok(App { collection_manager, running: true }) + Ok(App { + collection_manager, + selection: Selection::default(), + running: true, + }) } - /// Whether the app is running. pub fn is_running(&self) -> bool { self.running } - /// Get the list of artists. - pub fn get_collection(&self) -> &Collection { - self.collection_manager.get_collection() + pub fn increment_category(&mut self) { + self.selection.active = match self.selection.active { + Category::Artist => Category::Album, + Category::Album => Category::Track, + Category::Track => Category::Track, + }; + } + + pub fn decrement_category(&mut self) { + self.selection.active = match self.selection.active { + Category::Artist => Category::Artist, + Category::Album => Category::Artist, + Category::Track => Category::Album, + }; + } + + pub fn increment_selection(&mut self) { + match self.selection.active { + Category::Artist => self.increment_artist_selection(), + Category::Album => self.increment_album_selection(), + Category::Track => self.increment_track_selection(), + } + } + + pub fn decrement_selection(&mut self) { + match self.selection.active { + Category::Artist => self.decrement_artist_selection(), + Category::Album => self.decrement_album_selection(), + Category::Track => self.decrement_track_selection(), + } + } + + fn increment_artist_selection(&mut self) { + if let Some(result) = self.selection.artist.checked_add(1) { + let artists: &Vec = self.collection_manager.get_collection(); + if (result as usize) < artists.len() { + self.selection.artist = result; + self.selection.album = 0; + self.selection.track = 0; + } + } + } + + fn decrement_artist_selection(&mut self) { + if let Some(result) = self.selection.artist.checked_sub(1) { + self.selection.artist = result; + self.selection.album = 0; + self.selection.track = 0; + } + } + + fn increment_album_selection(&mut self) { + if let Some(result) = self.selection.album.checked_add(1) { + let artists: &Vec = self.collection_manager.get_collection(); + let albums: &Vec = &artists[self.selection.artist as usize].albums; + if (result as usize) < albums.len() { + self.selection.album = result; + self.selection.track = 0; + } + } + } + + fn decrement_album_selection(&mut self) { + if let Some(result) = self.selection.album.checked_sub(1) { + self.selection.album = result; + self.selection.track = 0; + } + } + + fn increment_track_selection(&mut self) { + let artists: &Vec = self.collection_manager.get_collection(); + let albums: &Vec = &artists[self.selection.artist as usize].albums; + let tracks: &Vec = &albums[self.selection.album as usize].tracks; + if let Some(result) = self.selection.track.checked_add(1) { + if (result as usize) < tracks.len() { + self.selection.track = result; + } + } + } + + fn decrement_track_selection(&mut self) { + if let Some(result) = self.selection.track.checked_sub(1) { + self.selection.track = result; + } + } + + pub fn get_active_category(&self) -> Category { + self.selection.active + } + + pub fn get_artists(&self) -> Vec<&ArtistId> { + self.collection_manager + .get_collection() + .iter() + .map(|a| &a.id) + .collect() + } + + pub fn selected_artist(&self) -> usize { + self.selection.artist as usize + } + + pub fn get_albums(&self) -> Vec<&AlbumId> { + self.collection_manager.get_collection()[self.selection.artist as usize] + .albums + .iter() + .map(|a| &a.id) + .collect() + } + + pub fn selected_album(&self) -> usize { + self.selection.album as usize + } + + pub fn get_tracks(&self) -> Vec<&Track> { + self.collection_manager.get_collection()[self.selection.artist as usize].albums + [self.selection.album as usize] + .tracks + .iter() + .collect() + } + + pub fn selected_track(&self) -> usize { + self.selection.track as usize } - /// Set running to false to quit the application. pub fn quit(&mut self) { self.running = false; } diff --git a/src/tui/handler.rs b/src/tui/handler.rs index e50db02..b177ca6 100644 --- a/src/tui/handler.rs +++ b/src/tui/handler.rs @@ -5,16 +5,30 @@ use super::app::App; /// Handles the key events and updates the state of [`App`]. pub fn handle_key_events(key_event: KeyEvent, app: &mut App) { match key_event.code { - // Exit application on `ESC` or `q` + // Exit application on `ESC` or `q`. KeyCode::Esc | KeyCode::Char('q') => { app.quit(); } - // Exit application on `Ctrl-C` + // Exit application on `Ctrl-C`. KeyCode::Char('c') | KeyCode::Char('C') => { if key_event.modifiers == KeyModifiers::CONTROL { app.quit(); } } + // Category change. + KeyCode::Left => { + app.decrement_category(); + } + KeyCode::Right => { + app.increment_category(); + } + // Selection change. + KeyCode::Up => { + app.decrement_selection(); + } + KeyCode::Down => { + app.increment_selection(); + } // Other handlers you could add here. _ => {} } diff --git a/src/tui/ui.rs b/src/tui/ui.rs index fea75e6..c986d5c 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -3,11 +3,11 @@ use ratatui::{ backend::Backend, layout::{Alignment, Rect}, style::{Color, Modifier, Style}, - widgets::{Block, BorderType, Borders, List, ListItem, Paragraph}, + widgets::{Block, BorderType, Borders, List, ListItem, ListState, Paragraph}, Frame, }; -use super::app::App; +use super::app::{App, Category}; /// Renders the user interface widgets. pub fn render(app: &mut App, frame: &mut Frame<'_, B>) { @@ -16,11 +16,10 @@ pub fn render(app: &mut App, frame: &mut Frame<'_, B>) { // - https://docs.rs/ratatui/latest/ratatui/widgets/index.html // - https://github.com/tui-rs-revival/ratatui/tree/master/examples - let collection: &Collection = app.get_collection(); - - let artists: Vec = collection + let artists: Vec = app + .get_artists() .iter() - .map(|a| ListItem::new(a.id.name.as_str())) + .map(|id| ListItem::new(id.name.as_str())) .collect(); let frame_rect = frame.size(); @@ -34,25 +33,33 @@ pub fn render(app: &mut App, frame: &mut Frame<'_, B>) { height: frame_rect.height, }; - frame.render_widget( + let mut artists_state = ListState::default(); + artists_state.select(Some(app.selected_artist())); + + frame.render_stateful_widget( List::new(artists) .highlight_style(Style::default().add_modifier(Modifier::ITALIC)) - .highlight_symbol(">>") + .highlight_symbol(if let Category::Artist = app.get_active_category() { + ">> " + } else { + " > " + }) .style(Style::default().fg(Color::Cyan).bg(Color::Black)) .block( Block::default() - .title("Artists") + .title(" Artists ") .title_alignment(Alignment::Center) .borders(Borders::ALL) .border_type(BorderType::Rounded), ), artists_rect, + &mut artists_state, ); - let albums: Vec = collection[1] - .albums + let albums: Vec = app + .get_albums() .iter() - .map(|a| ListItem::new(a.id.title.as_str())) + .map(|id| ListItem::new(id.title.as_str())) .collect(); let albums_rect = Rect { @@ -62,21 +69,31 @@ pub fn render(app: &mut App, frame: &mut Frame<'_, B>) { height: frame_rect.height - height_over_three, }; - frame.render_widget( + let mut albums_state = ListState::default(); + albums_state.select(Some(app.selected_album())); + + frame.render_stateful_widget( List::new(albums) .highlight_style(Style::default().add_modifier(Modifier::ITALIC)) - .highlight_symbol(">>") + .highlight_symbol(if let Category::Album = app.get_active_category() { + ">> " + } else { + " > " + }) .style(Style::default().fg(Color::Cyan).bg(Color::Black)) .block( Block::default() - .title("Albums") + .title(" Albums ") .title_alignment(Alignment::Center) .borders(Borders::ALL) .border_type(BorderType::Rounded), ), albums_rect, + &mut albums_state, ); + let album = app.get_albums()[app.selected_album()]; + let albums_info_rect = Rect { x: albums_rect.x, y: albums_rect.y + albums_rect.height, @@ -88,12 +105,12 @@ pub fn render(app: &mut App, frame: &mut Frame<'_, B>) { Paragraph::new(format!( "Title: {}\n\ Year: {}", - collection[1].albums[1].id.title, collection[1].albums[1].id.year, + album.title, album.year, )) .style(Style::default().fg(Color::Cyan).bg(Color::Black)) .block( Block::default() - .title("Album info") + .title(" Album info ") .title_alignment(Alignment::Center) .borders(Borders::ALL) .border_type(BorderType::Rounded), @@ -101,8 +118,8 @@ pub fn render(app: &mut App, frame: &mut Frame<'_, B>) { albums_info_rect, ); - let tracks: Vec = collection[1].albums[1] - .tracks + let tracks: Vec = app + .get_tracks() .iter() .map(|t| ListItem::new(t.title.as_str())) .collect(); @@ -114,21 +131,31 @@ pub fn render(app: &mut App, frame: &mut Frame<'_, B>) { height: frame_rect.height - height_over_three, }; - frame.render_widget( + let mut tracks_state = ListState::default(); + tracks_state.select(Some(app.selected_track())); + + frame.render_stateful_widget( List::new(tracks) .highlight_style(Style::default().add_modifier(Modifier::ITALIC)) - .highlight_symbol(">>") + .highlight_symbol(if let Category::Track = app.get_active_category() { + ">> " + } else { + " > " + }) .style(Style::default().fg(Color::Cyan).bg(Color::Black)) .block( Block::default() - .title("Tracks") + .title(" Tracks ") .title_alignment(Alignment::Center) .borders(Borders::ALL) .border_type(BorderType::Rounded), ), tracks_rect, + &mut tracks_state, ); + let track = app.get_tracks()[app.selected_track()]; + let track_info_rect = Rect { x: tracks_rect.x, y: tracks_rect.y + tracks_rect.height, @@ -142,10 +169,10 @@ pub fn render(app: &mut App, frame: &mut Frame<'_, B>) { Title: {}\n\ Artist: {}\n\ Format: {}", - collection[1].albums[1].tracks[1].number, - collection[1].albums[1].tracks[1].title, - collection[1].albums[1].tracks[1].artist.join("; "), - match collection[1].albums[1].tracks[1].format { + track.number, + track.title, + track.artist.join("; "), + match track.format { TrackFormat::Flac => "FLAC", TrackFormat::Mp3 => "MP3", }, @@ -153,7 +180,7 @@ pub fn render(app: &mut App, frame: &mut Frame<'_, B>) { .style(Style::default().fg(Color::Cyan).bg(Color::Black)) .block( Block::default() - .title("Track info") + .title(" Track info ") .title_alignment(Alignment::Center) .borders(Borders::ALL) .border_type(BorderType::Rounded),