Add a popup window for artist metadata #70
@ -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.
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
185
src/tui/ui.rs
185
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,56 @@ 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 {
|
||||||
|
match opt_vec {
|
||||||
|
Some(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}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => 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 +543,7 @@ impl<MH: IMusicHoard> Ui<MH> {
|
|||||||
Ok(Ui {
|
Ok(Ui {
|
||||||
music_hoard,
|
music_hoard,
|
||||||
selection,
|
selection,
|
||||||
|
overlay: false,
|
||||||
running: true,
|
running: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -531,6 +606,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 +634,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 +679,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)]
|
||||||
|
Loading…
Reference in New Issue
Block a user