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 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
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