Add support for grabbing color from the song metadata

This commit is contained in:
Emi Simpson 2022-01-01 13:21:34 -05:00
parent a2f37bf3f3
commit bc19df18cb
Signed by: Emi
GPG Key ID: A12F2C2FFDC3D847
5 changed files with 127 additions and 90 deletions

View File

@ -1,8 +1,6 @@
use crate::load_song::extract_cover;
use crate::load_song::load_song; use crate::load_song::load_song;
use crate::lyrics::Lyrics; use crate::lyrics::Lyrics;
use crate::file_select::FileSelector; use crate::file_select::FileSelector;
use crate::styles::Theme;
use rfd::AsyncFileDialog; use rfd::AsyncFileDialog;
use std::path::PathBuf; use std::path::PathBuf;
use core::time::Duration; use core::time::Duration;
@ -10,11 +8,7 @@ use iced::Subscription;
use iced::Clipboard; use iced::Clipboard;
use iced::Command; use iced::Command;
use iced::Application; use iced::Application;
use iced::Container;
use iced::Row;
use iced::Element; use iced::Element;
use iced::Length;
use iced::Align;
use iced::executor; use iced::executor;
use iced_futures::time; use iced_futures::time;
use iced_native::subscription; use iced_native::subscription;
@ -22,19 +16,11 @@ use iced_native::keyboard;
use iced_native::window; use iced_native::window;
use iced_native::event::Event; use iced_native::event::Event;
#[derive(Clone, Debug)]
pub struct DelyriumApp { pub struct DelyriumApp {
lyrics_component: Lyrics, lyrics_component: Option<Lyrics>,
theme: Theme,
mode: AppMode,
file_selector: FileSelector, file_selector: FileSelector,
} }
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum AppMode {
FileSelect, Main
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum Message { pub enum Message {
LyricChanged { LyricChanged {
@ -57,9 +43,7 @@ impl Application for DelyriumApp {
fn new(_: Self::Flags) -> (Self, Command<Message>) { fn new(_: Self::Flags) -> (Self, Command<Message>) {
( (
DelyriumApp { DelyriumApp {
lyrics_component: Lyrics::new(), lyrics_component: None,
theme: Theme::default(),
mode: AppMode::FileSelect,
file_selector: FileSelector::default(), file_selector: FileSelector::default(),
}, },
Command::none(), Command::none(),
@ -74,32 +58,35 @@ impl Application for DelyriumApp {
let mut command = None; let mut command = None;
match message { match message {
Message::LyricChanged { line_no, new_value } => { Message::LyricChanged { line_no, new_value } => {
self.lyrics_component.update_line(new_value, line_no); if let Some(lyrics) = self.lyrics_component.as_mut() {
lyrics.update_line(new_value, line_no);
}
}, },
Message::LineAdvanced(current_line) => { Message::LineAdvanced(current_line) => {
self.lyrics_component.advance_line(current_line); if let Some(lyrics) = self.lyrics_component.as_mut() {
lyrics.advance_line(current_line);
}
}, },
Message::PasteSent => { Message::PasteSent => {
let clip_text = clipboard.read().unwrap_or(String::new()); if let Some(lyrics) = self.lyrics_component.as_mut() {
let clip_pasted_len = clip_text.chars() let clip_text = clipboard.read().unwrap_or(String::new());
.filter(|c| *c != '\r' && *c != '\n') let clip_pasted_len = clip_text.chars()
.count(); .filter(|c| *c != '\r' && *c != '\n')
let line = self.lyrics_component.current_line_mut().1; .count();
line.value.truncate(line.value.len() - clip_pasted_len); let line = lyrics.current_line_mut().1;
self.lyrics_component.insert_text(clip_text); line.value.truncate(line.value.len() - clip_pasted_len);
lyrics.insert_text(clip_text);
}
}, },
Message::Tick => { Message::Tick => {
match self.mode { if self.lyrics_component.is_none() {
AppMode::FileSelect => { self.file_selector.tick();
self.file_selector.tick();
},
_ => { },
} }
}, },
Message::FileOpened(path) => { Message::FileOpened(path) => {
println!("File opened! {}", path.display()); println!("File opened! {}", path.display());
let mut song = load_song(&path).unwrap(); let song = load_song(&path).unwrap().format;
extract_cover(&mut song); self.lyrics_component = Some(Lyrics::new(song));
}, },
Message::PromptForFile => { Message::PromptForFile => {
let task = async { let task = async {
@ -123,20 +110,10 @@ impl Application for DelyriumApp {
} }
fn view(&mut self) -> Element<Message> { fn view(&mut self) -> Element<Message> {
match self.mode { if let Some(lyrics) = self.lyrics_component.as_mut() {
AppMode::Main => { lyrics.view()
Container::new( } else {
Row::new() self.file_selector.view()
.push(self.lyrics_component.view(self.theme))
)
.align_y(Align::Center)
.style(self.theme)
.height(Length::Fill)
.into()
},
AppMode::FileSelect => {
self.file_selector.view()
}
} }
} }
@ -163,14 +140,13 @@ impl Application for DelyriumApp {
let fps30 = time::every(Duration::from_millis(1000 / 30)).map(|_| Message::Tick); let fps30 = time::every(Duration::from_millis(1000 / 30)).map(|_| Message::Tick);
match self.mode { if self.lyrics_component.is_none() {
AppMode::FileSelect => { Subscription::batch([
Subscription::batch([ runtime_events,
runtime_events, fps30
fps30 ])
]) } else {
}, runtime_events
AppMode::Main => runtime_events,
} }
} }
} }

View File

@ -1,3 +1,4 @@
use image::DynamicImage;
use std::error::Error; use std::error::Error;
use std::fs::OpenOptions; use std::fs::OpenOptions;
use std::path::Path; use std::path::Path;
@ -7,6 +8,9 @@ use std::ffi::OsStr;
use symphonia::default; use symphonia::default;
use symphonia::core::io::MediaSourceStream; use symphonia::core::io::MediaSourceStream;
use symphonia::core::probe::{Hint, ProbeResult}; use symphonia::core::probe::{Hint, ProbeResult};
use symphonia::core::meta::MetadataRevision;
use symphonia::core::meta::StandardVisualKey;
use symphonia::core::formats::FormatReader;
pub fn load_song(path: &Path) -> Result<ProbeResult, LoadError> { pub fn load_song(path: &Path) -> Result<ProbeResult, LoadError> {
let codecs = default::get_codecs(); let codecs = default::get_codecs();
@ -36,29 +40,36 @@ pub fn load_song(path: &Path) -> Result<ProbeResult, LoadError> {
).map_err(LoadError::SymphoniaError) ).map_err(LoadError::SymphoniaError)
} }
pub fn extract_cover(ProbeResult { format, metadata }: &mut ProbeResult) { pub fn extract_cover(format: &mut dyn FormatReader) -> Option<DynamicImage>{
if let Some(metadata) = metadata.get() { format.metadata()
if let Some(current) = metadata.current() { .current()
for tag in current.tags() { .into_iter()
println!("Probed Tag: {}", tag); .flat_map(MetadataRevision::visuals)
} .filter_map(|vis| image::load_from_memory(&vis.data).ok().map(|img| (vis.usage, img)))
println!("{} Visuals in Probed Metadata", current.visuals().len()); .max_by_key(|(usage, _)|
} else { usage.map(|usage| match usage {
println!("No current probed metadata"); StandardVisualKey::FrontCover => 00,
} StandardVisualKey::FileIcon => 01,
} else { StandardVisualKey::Illustration => 02,
println!("No probed metadata"); StandardVisualKey::BandArtistLogo => 03,
} StandardVisualKey::BackCover => 04,
StandardVisualKey::Media => 05,
let normal_metadata = format.metadata(); StandardVisualKey::Leaflet => 06,
if let Some(current) = normal_metadata.current() { StandardVisualKey::OtherIcon => 07,
for tag in current.tags() { StandardVisualKey::LeadArtistPerformerSoloist => 08,
println!("Probed Tag: {}", tag); StandardVisualKey::ArtistPerformer => 09,
} StandardVisualKey::Conductor => 10,
println!("{} Visuals in Normal Metadata", current.visuals().len()); StandardVisualKey::BandOrchestra => 11,
} else { StandardVisualKey::Composer => 12,
println!("No current normal metadata"); StandardVisualKey::Lyricist => 13,
} StandardVisualKey::RecordingLocation => 14,
StandardVisualKey::RecordingSession => 15,
StandardVisualKey::Performance => 16,
StandardVisualKey::ScreenCapture => 17,
StandardVisualKey::PublisherStudioLogo => 18,
}).unwrap_or(u32::MAX)
)
.map(|(_, img)| img)
} }
#[derive(Debug)] #[derive(Debug)]

View File

@ -1,3 +1,7 @@
use iced::Container;
use iced::Row;
use crate::palette::Palette;
use crate::load_song::extract_cover;
use iced::Element; use iced::Element;
use crate::styles::Theme; use crate::styles::Theme;
use crate::app::Message; use crate::app::Message;
@ -7,19 +11,33 @@ use iced::Length;
use iced::widget::text_input::{self, TextInput}; use iced::widget::text_input::{self, TextInput};
use iced::widget::scrollable::{self, Scrollable}; use iced::widget::scrollable::{self, Scrollable};
#[derive(Clone, Debug)] use symphonia::core::formats::FormatReader;
pub struct Lyrics { pub struct Lyrics {
lines: Vec<Lyric>, lines: Vec<Lyric>,
scroll_state: scrollable::State, scroll_state: scrollable::State,
theme: Theme,
song: Box<dyn FormatReader>,
} }
impl Lyrics { impl Lyrics {
pub fn new() -> Self { pub fn new(mut song: Box<dyn FormatReader>) -> Self {
let mut lyric = Lyric::new(); let mut lyric = Lyric::new();
lyric.select(); lyric.select();
let cover = extract_cover(song.as_mut());
let theme = cover.map(|cover| {
Theme::from_palette(
Palette::generate(&cover)
)
}).unwrap_or_else(|| Theme::default());
Lyrics { Lyrics {
lines: vec![lyric], lines: vec![lyric],
scroll_state: scrollable::State::new() scroll_state: scrollable::State::new(),
song, theme,
} }
} }
@ -84,15 +102,24 @@ impl Lyrics {
.expect("no line currently selected") .expect("no line currently selected")
} }
pub fn view(&mut self, theme: Theme) -> Element<Message> { pub fn view(&mut self) -> Element<Message> {
let is_sole_line = self.lines.len() == 1; let is_sole_line = self.lines.len() == 1;
self.lines.iter_mut()
let lyrics = self.lines.iter_mut()
.enumerate() .enumerate()
.map(|(i, l)| l.view(is_sole_line, i, theme)) .map(|(i, l)| l.view(is_sole_line, i, self.theme))
.fold(Scrollable::new(&mut self.scroll_state), |s, l| s.push(l)) .fold(Scrollable::new(&mut self.scroll_state), |s, l| s.push(l))
.width(Length::Fill) .width(Length::Fill)
.align_items(Align::Center) .align_items(Align::Center);
.into()
Container::new(
Row::new()
.push(lyrics)
)
.align_y(Align::Center)
.style(self.theme)
.height(Length::Fill)
.into()
} }
} }

View File

@ -18,7 +18,9 @@ pub struct Palette {
} }
impl Palette { impl Palette {
pub fn generate(img: DynamicImage) -> Self { pub fn generate(img: &DynamicImage) -> Self {
let _thumb;
// Scale the image down if it's too big // Scale the image down if it's too big
let thumb = if img.width() * img.height() > MAX_SIZE_PIXELS { let thumb = if img.width() * img.height() > MAX_SIZE_PIXELS {
@ -29,14 +31,15 @@ impl Palette {
let new_width = (ratio * RESIZE_TARGET_PIXELS).sqrt(); let new_width = (ratio * RESIZE_TARGET_PIXELS).sqrt();
let new_height = RESIZE_TARGET_PIXELS / new_width; let new_height = RESIZE_TARGET_PIXELS / new_width;
img.thumbnail(new_width as u32, new_height as u32) _thumb = img.thumbnail(new_width as u32, new_height as u32);
&_thumb
} else { } else {
img img
}; };
// Convert to exoquant image // Convert to exoquant image
let width = thumb.width(); let width = thumb.width();
let exo_img: Vec<Color> = thumb.into_rgb8() let exo_img: Vec<Color> = thumb.to_rgb8()
.pixels() .pixels()
.map(|p| Color {r: p[0], g: p[1], b: p[2], a: 255}) .map(|p| Color {r: p[0], g: p[1], b: p[2], a: 255})
.collect(); .collect();

View File

@ -1,3 +1,5 @@
use image::Rgb;
use crate::palette::Palette;
use iced::Background; use iced::Background;
use iced::Color; use iced::Color;
use iced::widget::container; use iced::widget::container;
@ -11,6 +13,15 @@ pub struct Theme {
pub text_color: Color, pub text_color: Color,
} }
impl Theme {
pub fn from_palette(palette: Palette) -> Self {
Theme {
base_color: img_color_to_iced(palette.dominant_color()),
text_color: Color::WHITE,
}
}
}
impl Default for Theme { impl Default for Theme {
fn default() -> Self { fn default() -> Self {
Theme { Theme {
@ -58,3 +69,12 @@ impl text_input::StyleSheet for Theme {
self.text_color self.text_color
} }
} }
fn img_color_to_iced(color: &Rgb<u8>) -> Color {
Color {
r: color[0] as f32 / 255.,
g: color[1] as f32 / 255.,
b: color[2] as f32 / 255.,
a: 1.
}
}