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::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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue