diff --git a/src/collection/mod.rs b/src/collection/mod.rs index 87d7098..8834ef2 100644 --- a/src/collection/mod.rs +++ b/src/collection/mod.rs @@ -1,4 +1,10 @@ -use crate::{database::{Database, self}, library::{Library, Query, self}, Artist}; +use std::fmt; + +use crate::{ + database::{self, Database}, + library::{self, Library, Query}, + Artist, +}; /// The collection type. pub type Collection = Vec; @@ -12,6 +18,17 @@ pub enum Error { DatabaseError(String), } +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Self::LibraryError(ref s) => write!(f, "failed to read/write from/to the library: {s}"), + Self::DatabaseError(ref s) => { + write!(f, "failed to read/write from/to the database: {s}") + } + } + } +} + impl From for Error { fn from(err: library::Error) -> Error { Error::LibraryError(err.to_string()) @@ -51,4 +68,8 @@ impl CollectionManager { self.database.write(&self.collection)?; Ok(()) } + + pub fn get_collection(&self) -> &Collection { + &self.collection + } } diff --git a/src/main.rs b/src/main.rs index d70ebe5..9cbaf1c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,7 +13,7 @@ use musichoard::{ library::{ beets::{BeetsLibrary, BeetsLibraryCommandExecutor}, Library, Query, - }, + }, collection::CollectionManager, }; mod tui; @@ -46,17 +46,19 @@ struct Opt { fn main() { // Create the application. - let mut app = App::new(); + let opt = Opt::from_args(); - // let opt = Opt::from_args(); + let beets = BeetsLibrary::new(Box::new( + BeetsLibraryCommandExecutor::default().config(opt.beets_config_file_path.as_deref()), + )); - // let mut beets = BeetsLibrary::new(Box::new( - // BeetsLibraryCommandExecutor::default().config(opt.beets_config_file_path.as_deref()), - // )); + let database = JsonDatabase::new(Box::new(JsonDatabaseFileBackend::new( + &opt.database_file_path, + ))); - // let collection = beets - // .list(&Query::new()) - // .expect("failed to query the library"); + let collection_manager = CollectionManager::new(Box::new(beets), Box::new(database)); + + let mut app = App::new(collection_manager).expect("failed to initialise app"); // Initialize the terminal user interface. let backend = CrosstermBackend::new(io::stdout()); @@ -65,11 +67,9 @@ fn main() { let mut tui = Tui::new(terminal, events); tui.init(); - // Start the main loop. - while app.running { - // Render the user interface. + // Main loop. + while app.is_running() { tui.draw(&mut app); - // Handle events. match tui.events.next_event() { Event::Key(key_event) => handle_key_events(key_event, &mut app), Event::Mouse(_) => {} @@ -79,12 +79,4 @@ fn main() { // Exit the user interface. tui.exit(); - - // let mut database = JsonDatabase::new(Box::new(JsonDatabaseFileBackend::new( - // &opt.database_file_path, - // ))); - - // database - // .write(&collection) - // .expect("failed to write to the database"); } diff --git a/src/tui/app.rs b/src/tui/app.rs index 143fcfd..ada31ca 100644 --- a/src/tui/app.rs +++ b/src/tui/app.rs @@ -1,44 +1,32 @@ -/// Application. -#[derive(Debug)] -pub struct App { - /// Is the application running? - pub running: bool, - /// counter - pub counter: u8, -} +use musichoard::collection::{CollectionManager, Collection}; -impl Default for App { - fn default() -> Self { - Self { - running: true, - counter: 0, - } - } +use super::Error; + +/// Application. +pub struct App { + collection_manager: CollectionManager, + running: bool, } impl App { /// Constructs a new instance of [`App`]. - pub fn new() -> Self { - Self::default() + pub fn new(mut collection_manager: CollectionManager) -> Result { + collection_manager.rescan_library()?; + Ok(App { collection_manager, running: true }) } - /// Handles the tick event of the terminal. - pub fn tick(&self) {} + /// 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() + } /// Set running to false to quit the application. pub fn quit(&mut self) { self.running = false; } - - pub fn increment_counter(&mut self) { - if let Some(res) = self.counter.checked_add(1) { - self.counter = res; - } - } - - pub fn decrement_counter(&mut self) { - if let Some(res) = self.counter.checked_sub(1) { - self.counter = res; - } - } } diff --git a/src/tui/handler.rs b/src/tui/handler.rs index 9f8aa9d..e50db02 100644 --- a/src/tui/handler.rs +++ b/src/tui/handler.rs @@ -15,13 +15,6 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) { app.quit(); } } - // Counter handlers - KeyCode::Right => { - app.increment_counter(); - } - KeyCode::Left => { - app.decrement_counter(); - } // Other handlers you could add here. _ => {} } diff --git a/src/tui/mod.rs b/src/tui/mod.rs index 673612a..cff0299 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -1,5 +1,6 @@ use crossterm::event::{DisableMouseCapture, EnableMouseCapture}; use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}; +use musichoard::collection; use ratatui::backend::Backend; use ratatui::Terminal; use std::io; @@ -14,6 +15,19 @@ use event::EventHandler; use self::app::App; +/// Error type for the TUI. +#[derive(Debug)] +pub enum Error { + /// The collection manager failed. + CollectionError(String), +} + +impl From for Error { + fn from(err: collection::Error) -> Error { + Error::CollectionError(err.to_string()) + } +} + /// Representation of a terminal user interface. /// /// It is responsible for setting up the terminal, diff --git a/src/tui/ui.rs b/src/tui/ui.rs index 403066a..fea75e6 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -1,8 +1,9 @@ +use musichoard::{collection::Collection, TrackFormat}; use ratatui::{ backend::Backend, - layout::Alignment, - style::{Color, Style}, - widgets::{Block, BorderType, Borders, Paragraph}, + layout::{Alignment, Rect}, + style::{Color, Modifier, Style}, + widgets::{Block, BorderType, Borders, List, ListItem, Paragraph}, Frame, }; @@ -14,23 +15,149 @@ pub fn render(app: &mut App, frame: &mut Frame<'_, B>) { // See the following resources: // - 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 + .iter() + .map(|a| ListItem::new(a.id.name.as_str())) + .collect(); + + let frame_rect = frame.size(); + let width_over_three = frame_rect.width / 3; + let height_over_three = frame_rect.height / 3; + + let artists_rect = Rect { + x: frame_rect.x, + y: frame_rect.y, + width: width_over_three, + height: frame_rect.height, + }; + + frame.render_widget( + List::new(artists) + .highlight_style(Style::default().add_modifier(Modifier::ITALIC)) + .highlight_symbol(">>") + .style(Style::default().fg(Color::Cyan).bg(Color::Black)) + .block( + Block::default() + .title("Artists") + .title_alignment(Alignment::Center) + .borders(Borders::ALL) + .border_type(BorderType::Rounded), + ), + artists_rect, + ); + + let albums: Vec = collection[1] + .albums + .iter() + .map(|a| ListItem::new(a.id.title.as_str())) + .collect(); + + let albums_rect = Rect { + x: artists_rect.x + artists_rect.width, + y: frame_rect.y, + width: width_over_three, + height: frame_rect.height - height_over_three, + }; + + frame.render_widget( + List::new(albums) + .highlight_style(Style::default().add_modifier(Modifier::ITALIC)) + .highlight_symbol(">>") + .style(Style::default().fg(Color::Cyan).bg(Color::Black)) + .block( + Block::default() + .title("Albums") + .title_alignment(Alignment::Center) + .borders(Borders::ALL) + .border_type(BorderType::Rounded), + ), + albums_rect, + ); + + let albums_info_rect = Rect { + x: albums_rect.x, + y: albums_rect.y + albums_rect.height, + width: albums_rect.width, + height: height_over_three, + }; + frame.render_widget( Paragraph::new(format!( - "This is a tui template.\n\ - Press `Esc`, `Ctrl-C` or `q` to stop running.\n\ - Press left and right to increment and decrement the counter respectively.\n\ - Counter: {}", - app.counter + "Title: {}\n\ + Year: {}", + collection[1].albums[1].id.title, collection[1].albums[1].id.year, )) + .style(Style::default().fg(Color::Cyan).bg(Color::Black)) .block( Block::default() - .title("Template") + .title("Album info") .title_alignment(Alignment::Center) .borders(Borders::ALL) .border_type(BorderType::Rounded), - ) + ), + albums_info_rect, + ); + + let tracks: Vec = collection[1].albums[1] + .tracks + .iter() + .map(|t| ListItem::new(t.title.as_str())) + .collect(); + + let tracks_rect = Rect { + x: albums_rect.x + albums_rect.width, + y: frame_rect.y, + width: frame_rect.width - 2 * width_over_three, + height: frame_rect.height - height_over_three, + }; + + frame.render_widget( + List::new(tracks) + .highlight_style(Style::default().add_modifier(Modifier::ITALIC)) + .highlight_symbol(">>") + .style(Style::default().fg(Color::Cyan).bg(Color::Black)) + .block( + Block::default() + .title("Tracks") + .title_alignment(Alignment::Center) + .borders(Borders::ALL) + .border_type(BorderType::Rounded), + ), + tracks_rect, + ); + + let track_info_rect = Rect { + x: tracks_rect.x, + y: tracks_rect.y + tracks_rect.height, + width: tracks_rect.width, + height: height_over_three, + }; + + frame.render_widget( + Paragraph::new(format!( + "Track: {}\n\ + 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 { + TrackFormat::Flac => "FLAC", + TrackFormat::Mp3 => "MP3", + }, + )) .style(Style::default().fg(Color::Cyan).bg(Color::Black)) - .alignment(Alignment::Center), - frame.size(), - ) + .block( + Block::default() + .title("Track info") + .title_alignment(Alignment::Center) + .borders(Borders::ALL) + .border_type(BorderType::Rounded), + ), + track_info_rect, + ); }