Add a popup window for artist metadata (#70)

Closes #56

Reviewed-on: https://git.wojciechkozlowski.eu/wojtek/musichoard/pulls/70
This commit is contained in:
Wojciech Kozlowski 2023-05-21 22:48:48 +02:00
parent 3cd0cfde18
commit 62d6c43e3c
3 changed files with 178 additions and 37 deletions

View File

@ -1,5 +1,11 @@
# Music Hoard
## Usage notes
### Text selection
To select and copy text use the terminal-specific modifier key (on Linux this is usually the Shift key).
## Code Coverage
### Pre-requisites

View File

@ -67,6 +67,10 @@ impl<UI: IUi> IEventHandlerPrivate<UI> for EventHandler {
KeyCode::Down => {
ui.increment_selection();
}
// Toggle overlay.
KeyCode::Char('m') | KeyCode::Char('M') => {
ui.toggle_overlay();
}
// Other keys.
_ => {}
}

View File

@ -1,11 +1,11 @@
use std::fmt;
use musichoard::{Album, Artist, Collection, Format, Track};
use musichoard::{Album, Artist, Collection, Format, IUrl, Track};
use ratatui::{
backend::Backend,
layout::{Alignment, Rect},
style::{Color, Style},
widgets::{Block, BorderType, Borders, List, ListItem, ListState, Paragraph},
widgets::{Block, BorderType, Borders, Clear, List, ListItem, ListState, Paragraph},
Frame,
};
@ -42,6 +42,7 @@ pub trait IUi {
fn increment_selection(&mut self);
fn decrement_selection(&mut self);
fn toggle_overlay(&mut self);
fn render<B: Backend>(&mut self, frame: &mut Frame<'_, B>);
}
@ -275,6 +276,7 @@ impl Selection {
pub struct Ui<MH> {
music_hoard: MH,
selection: Selection,
overlay: bool,
running: bool,
}
@ -299,7 +301,7 @@ struct FrameArea {
}
impl FrameArea {
fn new(frame: Rect) -> FrameArea {
fn new(frame: Rect) -> Self {
let width_one_third = frame.width / 3;
let height_one_third = frame.height / 3;
@ -357,6 +359,28 @@ impl FrameArea {
}
}
struct OverlayArea {
artist: Rect,
}
impl OverlayArea {
fn new(frame: Rect) -> Self {
let margin_factor = 8;
let width_margin = frame.width / margin_factor;
let height_margin = frame.height / margin_factor;
let artist = Rect {
x: width_margin,
y: height_margin,
width: frame.width - (2 * width_margin),
height: frame.height - (2 * height_margin),
};
OverlayArea { artist }
}
}
struct ArtistState<'a, 'b> {
active: bool,
list: List<'a>,
@ -380,6 +404,55 @@ impl<'a, 'b> ArtistState<'a, 'b> {
}
}
struct ArtistOverlay<'a> {
properties: Paragraph<'a>,
}
impl<'a> ArtistOverlay<'a> {
fn opt_opt_to_str<U: IUrl>(opt: Option<Option<&U>>) -> &str {
opt.flatten().map(|item| item.url()).unwrap_or("")
}
fn opt_vec_to_string<U: IUrl>(opt_vec: Option<&Vec<U>>, indent: &str) -> String {
opt_vec
.map(|vec| {
if vec.len() < 2 {
vec.get(0).map(|item| item.url()).unwrap_or("").to_string()
} else {
let indent = format!("\n{indent}");
let list = vec
.iter()
.map(|item| item.url())
.collect::<Vec<&str>>()
.join(&indent);
format!("{indent}{list}")
}
})
.unwrap_or_else(|| String::from(""))
}
fn new(artists: &'a [Artist], state: &ListState) -> ArtistOverlay<'a> {
let artist = state.selected().map(|i| &artists[i]);
let item_indent = " ";
let list_indent = " - ";
let properties = Paragraph::new(format!(
"Artist: {}\n\n{item_indent}\
MusicBrainz: {}\n{item_indent}\
MusicButler: {}\n{item_indent}\
Bandcamp: {}\n{item_indent}\
Qobuz: {}",
artist.map(|a| a.id.name.as_str()).unwrap_or(""),
Self::opt_opt_to_str(artist.map(|a| a.properties.musicbrainz.as_ref())),
Self::opt_vec_to_string(artist.map(|a| &a.properties.musicbutler), list_indent),
Self::opt_vec_to_string(artist.map(|a| &a.properties.bandcamp), list_indent),
Self::opt_opt_to_str(artist.map(|a| a.properties.qobuz.as_ref())),
));
ArtistOverlay { properties }
}
}
struct AlbumState<'a, 'b> {
active: bool,
list: List<'a>,
@ -469,6 +542,7 @@ impl<MH: IMusicHoard> Ui<MH> {
Ok(Ui {
music_hoard,
selection,
overlay: false,
running: true,
})
}
@ -531,6 +605,21 @@ impl<MH: IMusicHoard> Ui<MH> {
);
}
fn render_overlay_widget<B: Backend>(
title: &str,
paragraph: Paragraph,
area: Rect,
frame: &mut Frame<'_, B>,
) {
frame.render_widget(Clear, area);
frame.render_widget(
paragraph
.style(Self::style(true))
.block(Self::block(title, true)),
area,
);
}
fn render_artist_column<B: Backend>(st: ArtistState, ar: ArtistArea, fr: &mut Frame<'_, B>) {
Self::render_list_widget("Artists", st.list, st.state, st.active, ar.list, fr);
}
@ -544,41 +633,8 @@ impl<MH: IMusicHoard> Ui<MH> {
Self::render_list_widget("Tracks", st.list, st.state, st.active, ar.list, fr);
Self::render_info_widget("Track info", st.info, st.active, ar.info, fr);
}
}
impl<MH: IMusicHoard> IUi for Ui<MH> {
fn is_running(&self) -> bool {
self.running
}
fn quit(&mut self) {
self.running = false;
}
fn save(&mut self) -> Result<(), UiError> {
self.music_hoard.save_to_database()?;
Ok(())
}
fn increment_category(&mut self) {
self.selection.increment_category();
}
fn decrement_category(&mut self) {
self.selection.decrement_category();
}
fn increment_selection(&mut self) {
self.selection
.increment_selection(self.music_hoard.get_collection());
}
fn decrement_selection(&mut self) {
self.selection
.decrement_selection(self.music_hoard.get_collection());
}
fn render<B: Backend>(&mut self, frame: &mut Frame<'_, B>) {
fn render_collection<B: Backend>(&mut self, frame: &mut Frame<'_, B>) {
let active = self.selection.active;
let areas = FrameArea::new(frame.size());
@ -622,6 +678,60 @@ impl<MH: IMusicHoard> IUi for Ui<MH> {
Self::render_track_column(track_state, areas.track, frame);
}
fn render_overlay<B: Backend>(&mut self, frame: &mut Frame<'_, B>) {
let areas = OverlayArea::new(frame.size());
let artists = self.music_hoard.get_collection();
let artist_selection = &mut self.selection.artist;
let artist_overlay = ArtistOverlay::new(artists, &artist_selection.state);
Self::render_overlay_widget("Artist", artist_overlay.properties, areas.artist, frame);
}
}
impl<MH: IMusicHoard> IUi for Ui<MH> {
fn is_running(&self) -> bool {
self.running
}
fn quit(&mut self) {
self.running = false;
}
fn save(&mut self) -> Result<(), UiError> {
self.music_hoard.save_to_database()?;
Ok(())
}
fn increment_category(&mut self) {
self.selection.increment_category();
}
fn decrement_category(&mut self) {
self.selection.decrement_category();
}
fn increment_selection(&mut self) {
self.selection
.increment_selection(self.music_hoard.get_collection());
}
fn decrement_selection(&mut self) {
self.selection
.decrement_selection(self.music_hoard.get_collection());
}
fn toggle_overlay(&mut self) {
self.overlay = !self.overlay;
}
fn render<B: Backend>(&mut self, frame: &mut Frame<'_, B>) {
self.render_collection(frame);
if self.overlay {
self.render_overlay(frame);
}
}
}
#[cfg(test)]
@ -1060,6 +1170,27 @@ mod tests {
terminal.draw(|frame| ui.render(frame)).unwrap();
}
#[test]
fn overlay() {
let mut terminal = terminal();
let mut ui = ui(COLLECTION.to_owned());
terminal.draw(|frame| ui.render(frame)).unwrap();
ui.toggle_overlay();
terminal.draw(|frame| ui.render(frame)).unwrap();
// Change the artist (which has a multi-link entry).
ui.increment_selection();
terminal.draw(|frame| ui.render(frame)).unwrap();
ui.toggle_overlay();
terminal.draw(|frame| ui.render(frame)).unwrap();
}
#[test]
fn errors() {
let ui_err: UiError = musichoard::Error::DatabaseError(String::from("get rekt")).into();