First dynamic version

This commit is contained in:
Wojciech Kozlowski 2023-04-11 14:53:27 +02:00
parent 147e663450
commit 692b97a783
3 changed files with 226 additions and 38 deletions

View File

@ -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<Self, Error> {
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<Artist> = 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<Artist> = self.collection_manager.get_collection();
let albums: &Vec<Album> = &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<Artist> = self.collection_manager.get_collection();
let albums: &Vec<Album> = &artists[self.selection.artist as usize].albums;
let tracks: &Vec<Track> = &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;
}

View File

@ -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.
_ => {}
}

View File

@ -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<B: Backend>(app: &mut App, frame: &mut Frame<'_, B>) {
@ -16,11 +16,10 @@ pub fn render<B: Backend>(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<ListItem> = collection
let artists: Vec<ListItem> = 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<B: Backend>(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<ListItem> = collection[1]
.albums
let albums: Vec<ListItem> = 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<B: Backend>(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<B: Backend>(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<B: Backend>(app: &mut App, frame: &mut Frame<'_, B>) {
albums_info_rect,
);
let tracks: Vec<ListItem> = collection[1].albums[1]
.tracks
let tracks: Vec<ListItem> = app
.get_tracks()
.iter()
.map(|t| ListItem::new(t.title.as_str()))
.collect();
@ -114,21 +131,31 @@ pub fn render<B: Backend>(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<B: Backend>(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<B: Backend>(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),