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::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<Lyrics>,
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<Message>) {
(
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<Message> {
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
}
}
}

View File

@ -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<ProbeResult, LoadError> {
let codecs = default::get_codecs();
@ -36,29 +40,36 @@ pub fn load_song(path: &Path) -> Result<ProbeResult, LoadError> {
).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<DynamicImage>{
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)]

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 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<Lyric>,
scroll_state: scrollable::State,
theme: Theme,
song: Box<dyn FormatReader>,
}
impl Lyrics {
pub fn new() -> Self {
pub fn new(mut song: Box<dyn FormatReader>) -> 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<Message> {
pub fn view(&mut self) -> Element<Message> {
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()
}
}

View File

@ -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<Color> = thumb.into_rgb8()
let exo_img: Vec<Color> = thumb.to_rgb8()
.pixels()
.map(|p| Color {r: p[0], g: p[1], b: p[2], a: 255})
.collect();

View File

@ -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<u8>) -> Color {
Color {
r: color[0] as f32 / 255.,
g: color[1] as f32 / 255.,
b: color[2] as f32 / 255.,
a: 1.
}
}