diff --git a/src/app.rs b/src/app.rs index c63576f..b0bc0d4 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,8 +1,6 @@ -use crate::load_song::extract_cover; use crate::load_song::load_song; use crate::lyrics::Lyrics; use crate::file_select::FileSelector; -use crate::styles::Theme; use rfd::AsyncFileDialog; use std::path::PathBuf; use core::time::Duration; @@ -10,11 +8,7 @@ use iced::Subscription; use iced::Clipboard; use iced::Command; use iced::Application; -use iced::Container; -use iced::Row; use iced::Element; -use iced::Length; -use iced::Align; use iced::executor; use iced_futures::time; use iced_native::subscription; @@ -22,19 +16,11 @@ use iced_native::keyboard; use iced_native::window; use iced_native::event::Event; -#[derive(Clone, Debug)] pub struct DelyriumApp { - lyrics_component: Lyrics, - theme: Theme, - mode: AppMode, + lyrics_component: Option, file_selector: FileSelector, } -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum AppMode { - FileSelect, Main -} - #[derive(Clone, Debug)] pub enum Message { LyricChanged { @@ -57,9 +43,7 @@ impl Application for DelyriumApp { fn new(_: Self::Flags) -> (Self, Command) { ( DelyriumApp { - lyrics_component: Lyrics::new(), - theme: Theme::default(), - mode: AppMode::FileSelect, + lyrics_component: None, file_selector: FileSelector::default(), }, Command::none(), @@ -74,32 +58,35 @@ impl Application for DelyriumApp { let mut command = None; match message { 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) => { - self.lyrics_component.advance_line(current_line); + if let Some(lyrics) = self.lyrics_component.as_mut() { + lyrics.advance_line(current_line); + } }, Message::PasteSent => { - let clip_text = clipboard.read().unwrap_or(String::new()); - let clip_pasted_len = clip_text.chars() - .filter(|c| *c != '\r' && *c != '\n') - .count(); - let line = self.lyrics_component.current_line_mut().1; - line.value.truncate(line.value.len() - clip_pasted_len); - self.lyrics_component.insert_text(clip_text); + if let Some(lyrics) = self.lyrics_component.as_mut() { + let clip_text = clipboard.read().unwrap_or(String::new()); + let clip_pasted_len = clip_text.chars() + .filter(|c| *c != '\r' && *c != '\n') + .count(); + let line = lyrics.current_line_mut().1; + line.value.truncate(line.value.len() - clip_pasted_len); + lyrics.insert_text(clip_text); + } }, Message::Tick => { - match self.mode { - AppMode::FileSelect => { - self.file_selector.tick(); - }, - _ => { }, + if self.lyrics_component.is_none() { + self.file_selector.tick(); } }, Message::FileOpened(path) => { println!("File opened! {}", path.display()); - let mut song = load_song(&path).unwrap(); - extract_cover(&mut song); + let song = load_song(&path).unwrap().format; + self.lyrics_component = Some(Lyrics::new(song)); }, Message::PromptForFile => { let task = async { @@ -123,20 +110,10 @@ impl Application for DelyriumApp { } fn view(&mut self) -> Element { - match self.mode { - AppMode::Main => { - Container::new( - Row::new() - .push(self.lyrics_component.view(self.theme)) - ) - .align_y(Align::Center) - .style(self.theme) - .height(Length::Fill) - .into() - }, - AppMode::FileSelect => { - self.file_selector.view() - } + if let Some(lyrics) = self.lyrics_component.as_mut() { + lyrics.view() + } else { + self.file_selector.view() } } @@ -163,14 +140,13 @@ impl Application for DelyriumApp { let fps30 = time::every(Duration::from_millis(1000 / 30)).map(|_| Message::Tick); - match self.mode { - AppMode::FileSelect => { - Subscription::batch([ - runtime_events, - fps30 - ]) - }, - AppMode::Main => runtime_events, + if self.lyrics_component.is_none() { + Subscription::batch([ + runtime_events, + fps30 + ]) + } else { + runtime_events } } } diff --git a/src/load_song.rs b/src/load_song.rs index 80bbac3..4aad798 100644 --- a/src/load_song.rs +++ b/src/load_song.rs @@ -1,3 +1,4 @@ +use image::DynamicImage; use std::error::Error; use std::fs::OpenOptions; use std::path::Path; @@ -7,6 +8,9 @@ use std::ffi::OsStr; use symphonia::default; use symphonia::core::io::MediaSourceStream; 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 { let codecs = default::get_codecs(); @@ -36,29 +40,36 @@ pub fn load_song(path: &Path) -> Result { ).map_err(LoadError::SymphoniaError) } -pub fn extract_cover(ProbeResult { format, metadata }: &mut ProbeResult) { - if let Some(metadata) = metadata.get() { - if let Some(current) = metadata.current() { - for tag in current.tags() { - println!("Probed Tag: {}", tag); - } - println!("{} Visuals in Probed Metadata", current.visuals().len()); - } else { - println!("No current probed metadata"); - } - } else { - println!("No probed metadata"); - } - - let normal_metadata = format.metadata(); - if let Some(current) = normal_metadata.current() { - for tag in current.tags() { - println!("Probed Tag: {}", tag); - } - println!("{} Visuals in Normal Metadata", current.visuals().len()); - } else { - println!("No current normal metadata"); - } +pub fn extract_cover(format: &mut dyn FormatReader) -> Option{ + format.metadata() + .current() + .into_iter() + .flat_map(MetadataRevision::visuals) + .filter_map(|vis| image::load_from_memory(&vis.data).ok().map(|img| (vis.usage, img))) + .max_by_key(|(usage, _)| + usage.map(|usage| match usage { + StandardVisualKey::FrontCover => 00, + StandardVisualKey::FileIcon => 01, + StandardVisualKey::Illustration => 02, + StandardVisualKey::BandArtistLogo => 03, + StandardVisualKey::BackCover => 04, + StandardVisualKey::Media => 05, + StandardVisualKey::Leaflet => 06, + StandardVisualKey::OtherIcon => 07, + StandardVisualKey::LeadArtistPerformerSoloist => 08, + StandardVisualKey::ArtistPerformer => 09, + StandardVisualKey::Conductor => 10, + StandardVisualKey::BandOrchestra => 11, + StandardVisualKey::Composer => 12, + 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)] diff --git a/src/lyrics.rs b/src/lyrics.rs index bb8f5cd..20bd8b2 100644 --- a/src/lyrics.rs +++ b/src/lyrics.rs @@ -1,3 +1,7 @@ +use iced::Container; +use iced::Row; +use crate::palette::Palette; +use crate::load_song::extract_cover; use iced::Element; use crate::styles::Theme; use crate::app::Message; @@ -7,19 +11,33 @@ use iced::Length; use iced::widget::text_input::{self, TextInput}; use iced::widget::scrollable::{self, Scrollable}; -#[derive(Clone, Debug)] +use symphonia::core::formats::FormatReader; + pub struct Lyrics { lines: Vec, scroll_state: scrollable::State, + theme: Theme, + song: Box, } impl Lyrics { - pub fn new() -> Self { + pub fn new(mut song: Box) -> Self { + let mut lyric = Lyric::new(); 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 { 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") } - pub fn view(&mut self, theme: Theme) -> Element { + pub fn view(&mut self) -> Element { let is_sole_line = self.lines.len() == 1; - self.lines.iter_mut() + + let lyrics = self.lines.iter_mut() .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)) .width(Length::Fill) - .align_items(Align::Center) - .into() + .align_items(Align::Center); + + Container::new( + Row::new() + .push(lyrics) + ) + .align_y(Align::Center) + .style(self.theme) + .height(Length::Fill) + .into() } } diff --git a/src/palette.rs b/src/palette.rs index 71084aa..06fd710 100644 --- a/src/palette.rs +++ b/src/palette.rs @@ -18,7 +18,9 @@ pub struct 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 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_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 { img }; // Convert to exoquant image let width = thumb.width(); - let exo_img: Vec = thumb.into_rgb8() + let exo_img: Vec = thumb.to_rgb8() .pixels() .map(|p| Color {r: p[0], g: p[1], b: p[2], a: 255}) .collect(); diff --git a/src/styles.rs b/src/styles.rs index 501e720..ff7fa05 100644 --- a/src/styles.rs +++ b/src/styles.rs @@ -1,3 +1,5 @@ +use image::Rgb; +use crate::palette::Palette; use iced::Background; use iced::Color; use iced::widget::container; @@ -11,6 +13,15 @@ pub struct Theme { 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 { fn default() -> Self { Theme { @@ -58,3 +69,12 @@ impl text_input::StyleSheet for Theme { self.text_color } } + +fn img_color_to_iced(color: &Rgb) -> Color { + Color { + r: color[0] as f32 / 255., + g: color[1] as f32 / 255., + b: color[2] as f32 / 255., + a: 1. + } +}