Add a field that indicates album ownership #156
@ -5,7 +5,7 @@ use std::{
|
|||||||
|
|
||||||
use crate::core::collection::{
|
use crate::core::collection::{
|
||||||
merge::{Merge, MergeSorted, WithId},
|
merge::{Merge, MergeSorted, WithId},
|
||||||
track::Track,
|
track::{Track, TrackFormat},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// An album is a collection of tracks that were released together.
|
/// An album is a collection of tracks that were released together.
|
||||||
@ -40,22 +40,16 @@ pub struct AlbumDate {
|
|||||||
pub day: u8,
|
pub day: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for AlbumDate {
|
impl AlbumDate {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
pub fn new<M: Into<AlbumMonth>>(year: u32, month: M, day: u8) -> Self {
|
||||||
if self.month.is_none() {
|
AlbumDate {
|
||||||
write!(f, "{}", self.year)
|
year,
|
||||||
} else if self.day == 0 {
|
month: month.into(),
|
||||||
write!(f, "{}‐{:02}", self.year, self.month as u8)
|
day,
|
||||||
} else {
|
|
||||||
write!(f, "{}‐{:02}‐{:02}", self.year, self.month as u8, self.day)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The album's sequence to determine order when two or more albums have the same release date.
|
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd, Ord, Eq, Hash)]
|
|
||||||
pub struct AlbumSeq(pub u8);
|
|
||||||
|
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd, Ord, Eq, Hash)]
|
#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd, Ord, Eq, Hash)]
|
||||||
pub enum AlbumMonth {
|
pub enum AlbumMonth {
|
||||||
@ -96,16 +90,39 @@ impl From<u8> for AlbumMonth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AlbumMonth {
|
impl AlbumMonth {
|
||||||
fn is_none(&self) -> bool {
|
pub fn is_none(&self) -> bool {
|
||||||
matches!(self, AlbumMonth::None)
|
matches!(self, AlbumMonth::None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The album's sequence to determine order when two or more albums have the same release date.
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd, Ord, Eq, Hash)]
|
||||||
|
pub struct AlbumSeq(pub u8);
|
||||||
|
|
||||||
|
/// The album's ownership status.
|
||||||
|
pub enum AlbumStatus {
|
||||||
|
None,
|
||||||
|
Owned(TrackFormat),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AlbumStatus {
|
||||||
|
pub fn from_tracks(tracks: &[Track]) -> AlbumStatus {
|
||||||
|
match tracks.iter().map(|t| t.quality.format).min() {
|
||||||
|
Some(format) => AlbumStatus::Owned(format),
|
||||||
|
None => AlbumStatus::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Album {
|
impl Album {
|
||||||
pub fn get_sort_key(&self) -> (&AlbumDate, &AlbumSeq, &AlbumId) {
|
pub fn get_sort_key(&self) -> (&AlbumDate, &AlbumSeq, &AlbumId) {
|
||||||
(&self.date, &self.seq, &self.id)
|
(&self.date, &self.seq, &self.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_status(&self) -> AlbumStatus {
|
||||||
|
AlbumStatus::from_tracks(&self.tracks)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_seq(&mut self, seq: AlbumSeq) {
|
pub fn set_seq(&mut self, seq: AlbumSeq) {
|
||||||
self.seq = seq;
|
self.seq = seq;
|
||||||
}
|
}
|
||||||
@ -166,16 +183,6 @@ mod tests {
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
impl AlbumDate {
|
|
||||||
fn new<M: Into<AlbumMonth>>(year: u32, month: M, day: u8) -> Self {
|
|
||||||
AlbumDate {
|
|
||||||
year,
|
|
||||||
month: month.into(),
|
|
||||||
day,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn album_month() {
|
fn album_month() {
|
||||||
assert_eq!(<u8 as Into<AlbumMonth>>::into(0), AlbumMonth::None);
|
assert_eq!(<u8 as Into<AlbumMonth>>::into(0), AlbumMonth::None);
|
||||||
@ -195,14 +202,6 @@ mod tests {
|
|||||||
assert_eq!(<u8 as Into<AlbumMonth>>::into(255), AlbumMonth::None);
|
assert_eq!(<u8 as Into<AlbumMonth>>::into(255), AlbumMonth::None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn album_display() {
|
|
||||||
assert_eq!(AlbumDate::default().to_string(), "0");
|
|
||||||
assert_eq!(AlbumDate::new(1990, 0, 0).to_string(), "1990");
|
|
||||||
assert_eq!(AlbumDate::new(1990, 5, 0).to_string(), "1990‐05");
|
|
||||||
assert_eq!(AlbumDate::new(1990, 5, 6).to_string(), "1990‐05‐06");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn same_date_seq_cmp() {
|
fn same_date_seq_cmp() {
|
||||||
let date = AlbumDate::new(2024, 3, 2);
|
let date = AlbumDate::new(2024, 3, 2);
|
||||||
|
@ -33,10 +33,10 @@ impl Track {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The track file format.
|
/// The track file format.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
pub enum TrackFormat {
|
pub enum TrackFormat {
|
||||||
Flac,
|
|
||||||
Mp3,
|
Mp3,
|
||||||
|
Flac,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialOrd for Track {
|
impl PartialOrd for Track {
|
||||||
@ -61,6 +61,12 @@ impl Merge for Track {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn track_ord() {
|
||||||
|
assert!(TrackFormat::Mp3 < TrackFormat::Flac);
|
||||||
|
assert!(TrackFormat::Flac > TrackFormat::Mp3);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn merge_track() {
|
fn merge_track() {
|
||||||
let left = Track {
|
let left = Track {
|
||||||
|
162
src/tui/ui.rs
162
src/tui/ui.rs
@ -1,20 +1,28 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use musichoard::collection::{
|
use musichoard::collection::{
|
||||||
album::Album,
|
album::{Album, AlbumDate, AlbumSeq, AlbumStatus},
|
||||||
artist::Artist,
|
artist::Artist,
|
||||||
track::{Track, TrackFormat},
|
track::{Track, TrackFormat, TrackQuality},
|
||||||
Collection,
|
Collection,
|
||||||
};
|
};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Alignment, Rect},
|
layout::{Alignment, Rect},
|
||||||
style::{Color, Style},
|
style::{Color, Style},
|
||||||
|
text::Line,
|
||||||
widgets::{Block, BorderType, Borders, Clear, List, ListItem, ListState, Paragraph, Wrap},
|
widgets::{Block, BorderType, Borders, Clear, List, ListItem, ListState, Paragraph, Wrap},
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::tui::app::{AppPublicState, AppState, Category, IAppAccess, Selection, WidgetState};
|
use crate::tui::app::{AppPublicState, AppState, Category, IAppAccess, Selection, WidgetState};
|
||||||
|
|
||||||
|
const COLOR_BG: Color = Color::Black;
|
||||||
|
const COLOR_BG_HL: Color = Color::DarkGray;
|
||||||
|
const COLOR_FG: Color = Color::White;
|
||||||
|
const COLOR_FG_ERR: Color = Color::Red;
|
||||||
|
const COLOR_FG_WARN: Color = Color::LightYellow;
|
||||||
|
const COLOR_FG_GOOD: Color = Color::LightGreen;
|
||||||
|
|
||||||
pub trait IUi {
|
pub trait IUi {
|
||||||
fn render<APP: IAppAccess>(app: &mut APP, frame: &mut Frame);
|
fn render<APP: IAppAccess>(app: &mut APP, frame: &mut Frame);
|
||||||
}
|
}
|
||||||
@ -280,20 +288,25 @@ impl<'a, 'b> AlbumState<'a, 'b> {
|
|||||||
let list = List::new(
|
let list = List::new(
|
||||||
albums
|
albums
|
||||||
.iter()
|
.iter()
|
||||||
.map(|a| ListItem::new(a.id.title.as_str()))
|
.map(Self::to_list_item)
|
||||||
.collect::<Vec<ListItem>>(),
|
.collect::<Vec<ListItem>>(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let album = state.list.selected().map(|i| &albums[i]);
|
let album = state.list.selected().map(|i| &albums[i]);
|
||||||
let info = Paragraph::new(format!(
|
let info = Paragraph::new(format!(
|
||||||
"Title: {}\n\
|
"Title: {}\n\
|
||||||
Date: {}{}",
|
Date: {}{}\n\
|
||||||
|
Status: {}",
|
||||||
album.map(|a| a.id.title.as_str()).unwrap_or(""),
|
album.map(|a| a.id.title.as_str()).unwrap_or(""),
|
||||||
album.map(|a| a.date.to_string()).unwrap_or_default(),
|
|
||||||
album
|
album
|
||||||
.filter(|a| a.seq.0 > 0)
|
.map(|a| Self::display_album_date(&a.date))
|
||||||
.map(|a| format!(" ({})", a.seq.0))
|
.unwrap_or_default(),
|
||||||
.unwrap_or_default()
|
album
|
||||||
|
.map(|a| Self::display_album_seq(&a.seq))
|
||||||
|
.unwrap_or_default(),
|
||||||
|
album
|
||||||
|
.map(|a| Self::display_album_status(&a.get_status()))
|
||||||
|
.unwrap_or("")
|
||||||
));
|
));
|
||||||
|
|
||||||
AlbumState {
|
AlbumState {
|
||||||
@ -303,6 +316,45 @@ impl<'a, 'b> AlbumState<'a, 'b> {
|
|||||||
info,
|
info,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn to_list_item(album: &Album) -> ListItem {
|
||||||
|
let line = match album.get_status() {
|
||||||
|
AlbumStatus::None => Line::raw(album.id.title.as_str()),
|
||||||
|
AlbumStatus::Owned(format) => match format {
|
||||||
|
TrackFormat::Mp3 => Line::styled(album.id.title.as_str(), COLOR_FG_WARN),
|
||||||
|
TrackFormat::Flac => Line::styled(album.id.title.as_str(), COLOR_FG_GOOD),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
ListItem::new(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_album_date(date: &AlbumDate) -> String {
|
||||||
|
if date.month.is_none() {
|
||||||
|
format!("{}", date.year)
|
||||||
|
} else if date.day == 0 {
|
||||||
|
format!("{}‐{:02}", date.year, date.month as u8)
|
||||||
|
} else {
|
||||||
|
format!("{}‐{:02}‐{:02}", date.year, date.month as u8, date.day)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_album_seq(seq: &AlbumSeq) -> String {
|
||||||
|
if seq.0 > 0 {
|
||||||
|
format!(" ({})", seq.0)
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_album_status(status: &AlbumStatus) -> &'static str {
|
||||||
|
match status {
|
||||||
|
AlbumStatus::None => "None",
|
||||||
|
AlbumStatus::Owned(format) => match format {
|
||||||
|
TrackFormat::Mp3 => "MP3",
|
||||||
|
TrackFormat::Flac => "FLAC",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TrackState<'a, 'b> {
|
struct TrackState<'a, 'b> {
|
||||||
@ -331,10 +383,7 @@ impl<'a, 'b> TrackState<'a, 'b> {
|
|||||||
track.map(|t| t.id.title.as_str()).unwrap_or(""),
|
track.map(|t| t.id.title.as_str()).unwrap_or(""),
|
||||||
track.map(|t| t.artist.join("; ")).unwrap_or_default(),
|
track.map(|t| t.artist.join("; ")).unwrap_or_default(),
|
||||||
track
|
track
|
||||||
.map(|t| match t.quality.format {
|
.map(|t| Self::display_track_quality(&t.quality))
|
||||||
TrackFormat::Flac => "FLAC".to_string(),
|
|
||||||
TrackFormat::Mp3 => format!("MP3 {}kbps", t.quality.bitrate),
|
|
||||||
})
|
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
));
|
));
|
||||||
|
|
||||||
@ -345,6 +394,13 @@ impl<'a, 'b> TrackState<'a, 'b> {
|
|||||||
info,
|
info,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn display_track_quality(quality: &TrackQuality) -> String {
|
||||||
|
match quality.format {
|
||||||
|
TrackFormat::Flac => "FLAC".to_string(),
|
||||||
|
TrackFormat::Mp3 => format!("MP3 {}kbps", quality.bitrate),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Minibuffer<'a> {
|
struct Minibuffer<'a> {
|
||||||
@ -429,11 +485,11 @@ pub struct Ui;
|
|||||||
|
|
||||||
impl Ui {
|
impl Ui {
|
||||||
fn style(_active: bool, error: bool) -> Style {
|
fn style(_active: bool, error: bool) -> Style {
|
||||||
let style = Style::default().bg(Color::Black);
|
let style = Style::default().bg(COLOR_BG);
|
||||||
if error {
|
if error {
|
||||||
style.fg(Color::Red)
|
style.fg(COLOR_FG_ERR)
|
||||||
} else {
|
} else {
|
||||||
style.fg(Color::White)
|
style.fg(COLOR_FG)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -442,10 +498,11 @@ impl Ui {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn highlight_style(active: bool) -> Style {
|
fn highlight_style(active: bool) -> Style {
|
||||||
|
// Do not set the fg color here as it will overwrite any list-specific customisation.
|
||||||
if active {
|
if active {
|
||||||
Style::default().fg(Color::White).bg(Color::DarkGray)
|
Style::default().bg(COLOR_BG_HL)
|
||||||
} else {
|
} else {
|
||||||
Self::style(false, false)
|
Style::default().bg(COLOR_BG)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -687,6 +744,8 @@ impl IUi for Ui {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use musichoard::collection::{album::AlbumId, artist::ArtistId};
|
||||||
|
|
||||||
use crate::tui::{
|
use crate::tui::{
|
||||||
app::{AppPublic, AppPublicInner, Delta},
|
app::{AppPublic, AppPublicInner, Delta},
|
||||||
testmod::COLLECTION,
|
testmod::COLLECTION,
|
||||||
@ -743,6 +802,61 @@ mod tests {
|
|||||||
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
|
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn display_album_date() {
|
||||||
|
assert_eq!(AlbumState::display_album_date(&AlbumDate::default()), "0");
|
||||||
|
assert_eq!(
|
||||||
|
AlbumState::display_album_date(&AlbumDate::new(1990, 0, 0)),
|
||||||
|
"1990"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
AlbumState::display_album_date(&AlbumDate::new(1990, 5, 0)),
|
||||||
|
"1990‐05"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
AlbumState::display_album_date(&AlbumDate::new(1990, 5, 6)),
|
||||||
|
"1990‐05‐06"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn display_album_seq() {
|
||||||
|
assert_eq!(AlbumState::display_album_seq(&AlbumSeq::default()), "");
|
||||||
|
assert_eq!(AlbumState::display_album_seq(&AlbumSeq(0)), "");
|
||||||
|
assert_eq!(AlbumState::display_album_seq(&AlbumSeq(5)), " (5)");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn display_album_status() {
|
||||||
|
assert_eq!(AlbumState::display_album_status(&AlbumStatus::None), "None");
|
||||||
|
assert_eq!(
|
||||||
|
AlbumState::display_album_status(&AlbumStatus::Owned(TrackFormat::Mp3)),
|
||||||
|
"MP3"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
AlbumState::display_album_status(&AlbumStatus::Owned(TrackFormat::Flac)),
|
||||||
|
"FLAC"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn display_track_quality() {
|
||||||
|
assert_eq!(
|
||||||
|
TrackState::display_track_quality(&TrackQuality {
|
||||||
|
format: TrackFormat::Flac,
|
||||||
|
bitrate: 1411
|
||||||
|
}),
|
||||||
|
"FLAC"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
TrackState::display_track_quality(&TrackQuality {
|
||||||
|
format: TrackFormat::Mp3,
|
||||||
|
bitrate: 218
|
||||||
|
}),
|
||||||
|
"MP3 218kbps"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty() {
|
fn empty() {
|
||||||
let artists: Vec<Artist> = vec![];
|
let artists: Vec<Artist> = vec![];
|
||||||
@ -751,6 +865,20 @@ mod tests {
|
|||||||
draw_test_suite(&artists, &mut selection);
|
draw_test_suite(&artists, &mut selection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_album() {
|
||||||
|
let mut artists: Vec<Artist> = vec![Artist::new(ArtistId::new("An artist"))];
|
||||||
|
artists[0].albums.push(Album {
|
||||||
|
id: AlbumId::new("An album"),
|
||||||
|
date: AlbumDate::default(),
|
||||||
|
seq: AlbumSeq::default(),
|
||||||
|
tracks: vec![],
|
||||||
|
});
|
||||||
|
let mut selection = Selection::new(&artists);
|
||||||
|
|
||||||
|
draw_test_suite(&artists, &mut selection);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn collection() {
|
fn collection() {
|
||||||
let artists = &COLLECTION;
|
let artists = &COLLECTION;
|
||||||
|
Loading…
Reference in New Issue
Block a user