Extract overlays
All checks were successful
Cargo CI / Build and Test (pull_request) Successful in 1m58s
Cargo CI / Lint (pull_request) Successful in 1m7s

This commit is contained in:
Wojciech Kozlowski 2024-08-29 16:11:43 +02:00
parent 0188d1aadf
commit 38ad041cec
4 changed files with 189 additions and 181 deletions

111
src/tui/ui/info.rs Normal file
View File

@ -0,0 +1,111 @@
use std::collections::HashMap;
use musichoard::collection::{album::Album, artist::Artist, musicbrainz::IMusicBrainzRef};
use ratatui::widgets::{ListState, Paragraph};
struct InfoOverlay;
impl InfoOverlay {
const ITEM_INDENT: &'static str = " ";
const LIST_INDENT: &'static str = " - ";
}
pub struct ArtistOverlay<'a> {
pub properties: Paragraph<'a>,
}
impl<'a> ArtistOverlay<'a> {
fn opt_hashmap_to_string<K: Ord + AsRef<str>, T: AsRef<str>>(
opt_map: Option<&HashMap<K, Vec<T>>>,
item_indent: &str,
list_indent: &str,
) -> String {
opt_map
.map(|map| Self::hashmap_to_string(map, item_indent, list_indent))
.unwrap_or_else(|| String::from(""))
}
fn hashmap_to_string<K: AsRef<str>, T: AsRef<str>>(
map: &HashMap<K, Vec<T>>,
item_indent: &str,
list_indent: &str,
) -> String {
let mut vec: Vec<(&str, &Vec<T>)> = map.iter().map(|(k, v)| (k.as_ref(), v)).collect();
vec.sort_by(|x, y| x.0.cmp(y.0));
let indent = format!("\n{item_indent}");
let list = vec
.iter()
.map(|(k, v)| format!("{k}: {}", Self::slice_to_string(v, list_indent)))
.collect::<Vec<String>>()
.join(&indent);
format!("{indent}{list}")
}
fn slice_to_string<S: AsRef<str>>(vec: &[S], indent: &str) -> String {
if vec.len() < 2 {
vec.first()
.map(|item| item.as_ref())
.unwrap_or("")
.to_string()
} else {
let indent = format!("\n{indent}");
let list = vec
.iter()
.map(|item| item.as_ref())
.collect::<Vec<&str>>()
.join(&indent);
format!("{indent}{list}")
}
}
pub fn new(artists: &'a [Artist], state: &ListState) -> ArtistOverlay<'a> {
let artist = state.selected().map(|i| &artists[i]);
let item_indent = InfoOverlay::ITEM_INDENT;
let list_indent = InfoOverlay::LIST_INDENT;
let double_item_indent = format!("{item_indent}{item_indent}");
let double_list_indent = format!("{item_indent}{list_indent}");
let properties = Paragraph::new(format!(
"Artist: {}\n\n{item_indent}\
MusicBrainz: {}\n{item_indent}\
Properties: {}",
artist.map(|a| a.id.name.as_str()).unwrap_or(""),
artist
.and_then(|a| a.musicbrainz.as_ref().map(|mb| mb.url().as_str()))
.unwrap_or(""),
Self::opt_hashmap_to_string(
artist.map(|a| &a.properties),
&double_item_indent,
&double_list_indent
),
));
ArtistOverlay { properties }
}
}
pub struct AlbumOverlay<'a> {
pub properties: Paragraph<'a>,
}
impl<'a> AlbumOverlay<'a> {
pub fn new(albums: &'a [Album], state: &ListState) -> AlbumOverlay<'a> {
let album = state.selected().map(|i| &albums[i]);
let item_indent = InfoOverlay::ITEM_INDENT;
let properties = Paragraph::new(format!(
"Album: {}\n\n{item_indent}\
MusicBrainz: {}",
album.map(|a| a.id.title.as_str()).unwrap_or(""),
album
.and_then(|a| a.musicbrainz.as_ref().map(|mb| mb.url().as_str()))
.unwrap_or(""),
));
AlbumOverlay { properties }
}
}

View File

@ -1,21 +1,23 @@
mod browse; mod browse;
mod display; mod display;
mod info;
mod minibuffer; mod minibuffer;
mod overlay;
use std::collections::HashMap; mod reload;
use browse::{AlbumArea, AlbumState, ArtistArea, ArtistState, FrameArea, TrackArea, TrackState}; use browse::{AlbumArea, AlbumState, ArtistArea, ArtistState, FrameArea, TrackArea, TrackState};
use display::UiDisplay; use display::UiDisplay;
use info::{AlbumOverlay, ArtistOverlay};
use minibuffer::Minibuffer; use minibuffer::Minibuffer;
use musichoard::collection::{ use musichoard::collection::{album::Album, track::Track, Collection};
album::Album, artist::Artist, musicbrainz::IMusicBrainzRef, track::Track, Collection, use overlay::{OverlayBuilder, OverlaySize};
};
use ratatui::{ use ratatui::{
layout::{Alignment, Rect}, layout::{Alignment, Rect},
style::{Color, Style}, style::{Color, Style},
widgets::{Block, BorderType, Borders, Clear, List, ListItem, ListState, Paragraph, Wrap}, widgets::{Block, BorderType, Borders, Clear, List, ListItem, Paragraph, Wrap},
Frame, Frame,
}; };
use reload::ReloadMenu;
use crate::tui::{ use crate::tui::{
app::{AppPublicState, AppState, Category, IAppAccess, Selection, WidgetState}, app::{AppPublicState, AppState, Category, IAppAccess, Selection, WidgetState},
@ -37,180 +39,6 @@ pub trait IUi {
fn render<APP: IAppAccess>(app: &mut APP, frame: &mut Frame); fn render<APP: IAppAccess>(app: &mut APP, frame: &mut Frame);
} }
enum OverlaySize {
MarginFactor(u16),
Value(u16),
}
impl Default for OverlaySize {
fn default() -> Self {
OverlaySize::MarginFactor(8)
}
}
impl OverlaySize {
fn get(&self, full: u16) -> (u16, u16) {
match self {
OverlaySize::MarginFactor(margin_factor) => {
let margin = full / margin_factor;
(margin, full.saturating_sub(2 * margin))
}
OverlaySize::Value(value) => {
let margin = (full.saturating_sub(*value)) / 2;
(margin, *value)
}
}
}
}
#[derive(Default)]
struct OverlayBuilder {
width: OverlaySize,
height: OverlaySize,
}
impl OverlayBuilder {
fn with_width(mut self, width: OverlaySize) -> OverlayBuilder {
self.width = width;
self
}
fn with_height(mut self, height: OverlaySize) -> OverlayBuilder {
self.height = height;
self
}
fn build(self, frame: Rect) -> Rect {
let (x, width) = self.width.get(frame.width);
let (y, height) = self.height.get(frame.height);
Rect {
x,
y,
width,
height,
}
}
}
struct InfoOverlay;
impl InfoOverlay {
const ITEM_INDENT: &'static str = " ";
const LIST_INDENT: &'static str = " - ";
}
struct ArtistOverlay<'a> {
properties: Paragraph<'a>,
}
impl<'a> ArtistOverlay<'a> {
fn opt_hashmap_to_string<K: Ord + AsRef<str>, T: AsRef<str>>(
opt_map: Option<&HashMap<K, Vec<T>>>,
item_indent: &str,
list_indent: &str,
) -> String {
opt_map
.map(|map| Self::hashmap_to_string(map, item_indent, list_indent))
.unwrap_or_else(|| String::from(""))
}
fn hashmap_to_string<K: AsRef<str>, T: AsRef<str>>(
map: &HashMap<K, Vec<T>>,
item_indent: &str,
list_indent: &str,
) -> String {
let mut vec: Vec<(&str, &Vec<T>)> = map.iter().map(|(k, v)| (k.as_ref(), v)).collect();
vec.sort_by(|x, y| x.0.cmp(y.0));
let indent = format!("\n{item_indent}");
let list = vec
.iter()
.map(|(k, v)| format!("{k}: {}", Self::slice_to_string(v, list_indent)))
.collect::<Vec<String>>()
.join(&indent);
format!("{indent}{list}")
}
fn slice_to_string<S: AsRef<str>>(vec: &[S], indent: &str) -> String {
if vec.len() < 2 {
vec.first()
.map(|item| item.as_ref())
.unwrap_or("")
.to_string()
} else {
let indent = format!("\n{indent}");
let list = vec
.iter()
.map(|item| item.as_ref())
.collect::<Vec<&str>>()
.join(&indent);
format!("{indent}{list}")
}
}
fn new(artists: &'a [Artist], state: &ListState) -> ArtistOverlay<'a> {
let artist = state.selected().map(|i| &artists[i]);
let item_indent = InfoOverlay::ITEM_INDENT;
let list_indent = InfoOverlay::LIST_INDENT;
let double_item_indent = format!("{item_indent}{item_indent}");
let double_list_indent = format!("{item_indent}{list_indent}");
let properties = Paragraph::new(format!(
"Artist: {}\n\n{item_indent}\
MusicBrainz: {}\n{item_indent}\
Properties: {}",
artist.map(|a| a.id.name.as_str()).unwrap_or(""),
artist
.and_then(|a| a.musicbrainz.as_ref().map(|mb| mb.url().as_str()))
.unwrap_or(""),
Self::opt_hashmap_to_string(
artist.map(|a| &a.properties),
&double_item_indent,
&double_list_indent
),
));
ArtistOverlay { properties }
}
}
struct AlbumOverlay<'a> {
properties: Paragraph<'a>,
}
impl<'a> AlbumOverlay<'a> {
fn new(albums: &'a [Album], state: &ListState) -> AlbumOverlay<'a> {
let album = state.selected().map(|i| &albums[i]);
let item_indent = InfoOverlay::ITEM_INDENT;
let properties = Paragraph::new(format!(
"Album: {}\n\n{item_indent}\
MusicBrainz: {}",
album.map(|a| a.id.title.as_str()).unwrap_or(""),
album
.and_then(|a| a.musicbrainz.as_ref().map(|mb| mb.url().as_str()))
.unwrap_or(""),
));
AlbumOverlay { properties }
}
}
struct ReloadMenu;
impl ReloadMenu {
fn paragraph<'a>() -> Paragraph<'a> {
Paragraph::new(
"d: database\n\
l: library",
)
}
}
struct Column<'a> { struct Column<'a> {
paragraph: Paragraph<'a>, paragraph: Paragraph<'a>,
area: Rect, area: Rect,
@ -540,7 +368,7 @@ impl IUi for Ui {
mod tests { mod tests {
use musichoard::collection::{ use musichoard::collection::{
album::{AlbumDate, AlbumId, AlbumPrimaryType, AlbumSecondaryType}, album::{AlbumDate, AlbumId, AlbumPrimaryType, AlbumSecondaryType},
artist::ArtistId, artist::{Artist, ArtistId},
}; };
use crate::tui::{ use crate::tui::{

57
src/tui/ui/overlay.rs Normal file
View File

@ -0,0 +1,57 @@
use ratatui::layout::Rect;
pub enum OverlaySize {
MarginFactor(u16),
Value(u16),
}
impl Default for OverlaySize {
fn default() -> Self {
OverlaySize::MarginFactor(8)
}
}
impl OverlaySize {
fn get(&self, full: u16) -> (u16, u16) {
match self {
OverlaySize::MarginFactor(margin_factor) => {
let margin = full / margin_factor;
(margin, full.saturating_sub(2 * margin))
}
OverlaySize::Value(value) => {
let margin = (full.saturating_sub(*value)) / 2;
(margin, *value)
}
}
}
}
#[derive(Default)]
pub struct OverlayBuilder {
width: OverlaySize,
height: OverlaySize,
}
impl OverlayBuilder {
pub fn with_width(mut self, width: OverlaySize) -> OverlayBuilder {
self.width = width;
self
}
pub fn with_height(mut self, height: OverlaySize) -> OverlayBuilder {
self.height = height;
self
}
pub fn build(self, frame: Rect) -> Rect {
let (x, width) = self.width.get(frame.width);
let (y, height) = self.height.get(frame.height);
Rect {
x,
y,
width,
height,
}
}
}

12
src/tui/ui/reload.rs Normal file
View File

@ -0,0 +1,12 @@
use ratatui::widgets::Paragraph;
pub struct ReloadMenu;
impl ReloadMenu {
pub fn paragraph<'a>() -> Paragraph<'a> {
Paragraph::new(
"d: database\n\
l: library",
)
}
}