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:
parent
3cd0cfde18
commit
62d6c43e3c
@ -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
|
||||
|
@ -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.
|
||||
_ => {}
|
||||
}
|
||||
|
205
src/tui/ui.rs
205
src/tui/ui.rs
@ -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();
|
||||
|
Loading…
Reference in New Issue
Block a user