Add support for grabbing color from the song metadata
This commit is contained in:
parent
a2f37bf3f3
commit
bc19df18cb
88
src/app.rs
88
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<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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue