Add a popup window for artist metadata #70
@ -1,5 +1,11 @@
|
|||||||
# Music Hoard
|
# 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
|
## Code Coverage
|
||||||
|
|
||||||
### Pre-requisites
|
### Pre-requisites
|
||||||
|
@ -67,6 +67,10 @@ impl<UI: IUi> IEventHandlerPrivate<UI> for EventHandler {
|
|||||||
KeyCode::Down => {
|
KeyCode::Down => {
|
||||||
ui.increment_selection();
|
ui.increment_selection();
|
||||||
}
|
}
|
||||||
|
// Toggle overlay.
|
||||||
|
KeyCode::Char('m') | KeyCode::Char('M') => {
|
||||||
|
ui.toggle_overlay();
|
||||||
|
}
|
||||||
// Other keys.
|
// Other keys.
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
205
src/tui/ui.rs
205
src/tui/ui.rs
@ -1,11 +1,11 @@
|
|||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use musichoard::{Album, Artist, Collection, Format, Track};
|
use musichoard::{Album, Artist, Collection, Format, IUrl, Track};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::Backend,
|
backend::Backend,
|
||||||
layout::{Alignment, Rect},
|
layout::{Alignment, Rect},
|
||||||
style::{Color, Style},
|
style::{Color, Style},
|
||||||
widgets::{Block, BorderType, Borders, List, ListItem, ListState, Paragraph},
|
widgets::{Block, BorderType, Borders, Clear, List, ListItem, ListState, Paragraph},
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -42,6 +42,7 @@ pub trait IUi {
|
|||||||
fn increment_selection(&mut self);
|
fn increment_selection(&mut self);
|
||||||
fn decrement_selection(&mut self);
|
fn decrement_selection(&mut self);
|
||||||
|
|
||||||
|
fn toggle_overlay(&mut self);
|
||||||
fn render<B: Backend>(&mut self, frame: &mut Frame<'_, B>);
|
fn render<B: Backend>(&mut self, frame: &mut Frame<'_, B>);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -275,6 +276,7 @@ impl Selection {
|
|||||||
pub struct Ui<MH> {
|
pub struct Ui<MH> {
|
||||||
music_hoard: MH,
|
music_hoard: MH,
|
||||||
selection: Selection,
|
selection: Selection,
|
||||||
|
overlay: bool,
|
||||||
running: bool,
|
running: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -299,7 +301,7 @@ struct FrameArea {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FrameArea {
|
impl FrameArea {
|
||||||
fn new(frame: Rect) -> FrameArea {
|
fn new(frame: Rect) -> Self {
|
||||||
let width_one_third = frame.width / 3;
|
let width_one_third = frame.width / 3;
|
||||||
let height_one_third = frame.height / 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> {
|
struct ArtistState<'a, 'b> {
|
||||||
active: bool,
|
active: bool,
|
||||||
list: List<'a>,
|
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> {
|
struct AlbumState<'a, 'b> {
|
||||||
active: bool,
|
active: bool,
|
||||||
list: List<'a>,
|
list: List<'a>,
|
||||||
@ -469,6 +542,7 @@ impl<MH: IMusicHoard> Ui<MH> {
|
|||||||
Ok(Ui {
|
Ok(Ui {
|
||||||
music_hoard,
|
music_hoard,
|
||||||
selection,
|
selection,
|
||||||
|
overlay: false,
|
||||||
running: true,
|
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>) {
|
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);
|
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_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);
|
Self::render_info_widget("Track info", st.info, st.active, ar.info, fr);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl<MH: IMusicHoard> IUi for Ui<MH> {
|
fn render_collection<B: Backend>(&mut self, frame: &mut Frame<'_, B>) {
|
||||||
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>) {
|
|
||||||
let active = self.selection.active;
|
let active = self.selection.active;
|
||||||
let areas = FrameArea::new(frame.size());
|
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);
|
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)]
|
#[cfg(test)]
|
||||||
@ -1060,6 +1170,27 @@ mod tests {
|
|||||||
terminal.draw(|frame| ui.render(frame)).unwrap();
|
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]
|
#[test]
|
||||||
fn errors() {
|
fn errors() {
|
||||||
let ui_err: UiError = musichoard::Error::DatabaseError(String::from("get rekt")).into();
|
let ui_err: UiError = musichoard::Error::DatabaseError(String::from("get rekt")).into();
|
||||||
|
Loading…
Reference in New Issue
Block a user