Split ui.rs into modules based on UI element #200
111
src/tui/ui/info.rs
Normal file
111
src/tui/ui/info.rs
Normal 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 }
|
||||
}
|
||||
}
|
@ -1,21 +1,23 @@
|
||||
mod browse;
|
||||
mod display;
|
||||
mod info;
|
||||
mod minibuffer;
|
||||
|
||||
use std::collections::HashMap;
|
||||
mod overlay;
|
||||
mod reload;
|
||||
|
||||
use browse::{AlbumArea, AlbumState, ArtistArea, ArtistState, FrameArea, TrackArea, TrackState};
|
||||
use display::UiDisplay;
|
||||
use info::{AlbumOverlay, ArtistOverlay};
|
||||
use minibuffer::Minibuffer;
|
||||
use musichoard::collection::{
|
||||
album::Album, artist::Artist, musicbrainz::IMusicBrainzRef, track::Track, Collection,
|
||||
};
|
||||
use musichoard::collection::{album::Album, track::Track, Collection};
|
||||
use overlay::{OverlayBuilder, OverlaySize};
|
||||
use ratatui::{
|
||||
layout::{Alignment, Rect},
|
||||
style::{Color, Style},
|
||||
widgets::{Block, BorderType, Borders, Clear, List, ListItem, ListState, Paragraph, Wrap},
|
||||
widgets::{Block, BorderType, Borders, Clear, List, ListItem, Paragraph, Wrap},
|
||||
Frame,
|
||||
};
|
||||
use reload::ReloadMenu;
|
||||
|
||||
use crate::tui::{
|
||||
app::{AppPublicState, AppState, Category, IAppAccess, Selection, WidgetState},
|
||||
@ -37,180 +39,6 @@ pub trait IUi {
|
||||
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> {
|
||||
paragraph: Paragraph<'a>,
|
||||
area: Rect,
|
||||
@ -540,7 +368,7 @@ impl IUi for Ui {
|
||||
mod tests {
|
||||
use musichoard::collection::{
|
||||
album::{AlbumDate, AlbumId, AlbumPrimaryType, AlbumSecondaryType},
|
||||
artist::ArtistId,
|
||||
artist::{Artist, ArtistId},
|
||||
};
|
||||
|
||||
use crate::tui::{
|
||||
|
57
src/tui/ui/overlay.rs
Normal file
57
src/tui/ui/overlay.rs
Normal 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
12
src/tui/ui/reload.rs
Normal 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",
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user