use std::io::Seek; use crate::load_song::LoadError; use crate::model::load_song; use std::path::Path; use image::GenericImageView; use core::ops::RangeInclusive; use iced_native::widget::text_input::Value; use iced_native::widget::text_input::cursor::State; use iced::image::Handle; use crate::palette::Palette; use crate::load_song::extract_cover; use iced::Command; use crate::app::Message; use std::time::Duration; use crate::styles::Theme; use crate::controls::Controls; use image::DynamicImage; use iced::widget::text_input; use iced::widget::scrollable; use iced::clipboard; use iced_lazy::responsive; pub struct Editing { pub lyrics: Vec, pub bg_img: Option, pub controls: Controls, pub theme: Theme, /* UI state */ pub cached_resized_bg: Option, pub scroll_state: scrollable::State, pub image_responsiveness_state: (responsive::State, responsive::State), } pub struct Lyric { pub value: String, pub timestamp: Duration, pub timestamp_raw: String, /* UI state */ pub main_state: text_input::State, pub timestamp_state: text_input::State, } #[derive(Debug, Clone, PartialEq, Eq)] pub enum LyricEvent { LyricChanged(String), TimestampChanged(String), LineAdvanced, } impl LyricEvent { pub fn into_msg(self, line_no: usize) -> Message { Message::LyricEvent { kind: self, line_no, } } } impl Editing { pub fn new(song_path: &Path) -> Result<(Self, Command), LoadError> { let (mut file, fr) = load_song(song_path)?; let mut fr = fr.format; let cover = extract_cover(fr.as_mut()); let theme = cover.as_ref() .map(|cover| { Theme::from_palette( Palette::generate(cover) ) }).unwrap_or_else(Theme::default); let cover = cover.map(|cover| { #[cfg(not(debug_assertions))] let cover = cover.blur((cover.width() / 100) as f32); DynamicImage::ImageBgra8(cover.into_bgra8()) }); let cached_resized_bg = cover.as_ref().map(|cover| Handle::from_pixels( cover.width(), cover.height(), cover.to_bgra8().into_raw(), )); // Reset file handle and drop the format reader file.rewind().map_err(LoadError::OpenError)?; let (controls, cmd) = Controls::new(file); let mut lyrics = Vec::with_capacity(70); lyrics.push(Lyric::new()); let editor = Self { scroll_state: scrollable::State::default(), image_responsiveness_state: Default::default(), bg_img: cover, controls, theme, cached_resized_bg, lyrics, }; Ok((editor, cmd)) } pub fn update(&mut self, message: Message) -> Command { let mut command = None; match message { Message::LyricEvent { line_no, kind: LyricEvent::LyricChanged(newval) } => { self.lyrics[line_no].value = newval }, Message::LyricEvent { line_no, kind: LyricEvent::TimestampChanged(newval) } => { self.lyrics[line_no].timestamp_update(newval) }, Message::LyricEvent { line_no, kind: LyricEvent::LineAdvanced } => { self.advance_line(line_no, self.controls.position()) }, Message::PasteSent => { command = Some(clipboard::read(|clip| if let Some(clip) = clip { Message::PasteRead(clip) } else { Message::Null } )); }, Message::PasteRead(clip_text) => { let clip_pasted_len = clip_text.chars() .filter(|c| *c != '\r' && *c != '\n') .count(); if let Some(line) = self.current_line_mut() { line.value.truncate(line.value.len() - clip_pasted_len); self.insert_text(clip_text); } }, Message::ControlsEvent(e) => { self.controls.handle_event(e) }, Message::Null | Message::PromptForFile | Message::FileOpened(..) | Message::Tick => { }, } command.unwrap_or_else(Command::none) } pub fn advance_line(&mut self, current_line: usize, timestamp: Option) { let new_line = current_line + 1; let line = if new_line == self.lyrics.len() { self.insert_line(new_line, None) } else { self.lyrics.get_mut(new_line) .expect("Unexpected .advance_line with index beyond # of lines") }; line.select(); let previous_line = self.lyrics.get_mut(current_line).unwrap(); previous_line.deselect(); if let Some(timestamp) = timestamp { previous_line.set_timestamp(timestamp); } } pub fn insert_line(&mut self, index: usize, content: Option) -> &mut Lyric { self.lyrics.insert(index, match content { Some(content) => Lyric::new_with_value(content), None => Lyric::new(), }); self.lyrics.get_mut(index).unwrap() } pub fn current_line(&self) -> Option { self.lyrics .iter() .position(Lyric::is_selected) } pub fn current_line_mut(&mut self) -> Option<&mut Lyric> { self.lyrics .iter_mut() .find(|l| l.is_selected()) } pub fn is_animating(&self) -> bool { self.controls.is_playing() } pub fn insert_text(&mut self, text: String) { let mut pieces = text.trim_end() .split('\n') .map(str::trim); if let Some(line_no) = self.current_line() { let current_line = self.lyrics.get_mut(line_no).unwrap(); current_line.deselect(); current_line.value.push_str(pieces.next().unwrap()); let pieces = pieces .collect::>() .into_iter() .map(str::to_owned) .map(Lyric::new_with_value); let n_pieces = pieces.size_hint().0; self.lyrics.splice((line_no + 1)..(line_no + 1), pieces); self.lyrics[line_no + n_pieces].select(); } } } impl Lyric { pub fn new() -> Self { Self::new_with_value(String::with_capacity(70)) } pub fn new_with_value(val: String) -> Self { Lyric { main_state: text_input::State::new(), timestamp_state: text_input::State::new(), timestamp: Duration::ZERO, timestamp_raw: String::from("0:00.000"), value: val, } } pub fn is_selected(&self) -> bool { self.main_state.is_focused() || self.timestamp_state.is_focused() } pub fn select(&mut self) { self.main_state.focus(); self.main_state.move_cursor_to_end(); } pub fn deselect(&mut self) { self.main_state.unfocus(); self.timestamp_state.unfocus(); } pub fn set_timestamp(&mut self, timestamp: Duration) { self.timestamp = timestamp; let seconds = timestamp.as_secs(); let minutes = seconds / 60; let seconds = seconds % 60; let millis = timestamp.as_millis() % 1000; self.timestamp_raw = format!("{}:{:02}.{:03}", minutes, seconds, millis); } pub (crate) fn timestamp_update(&mut self, newval: String) { if let Some((shift, validated)) = Self::clean_timestamp(newval) { self.timestamp = dbg!(Self::parse_validated_timestamp(&validated)); self.timestamp_raw = validated; if shift != 0 { self.timestamp_state.move_cursor_to( match self.timestamp_state.cursor().state(&Value::new(&self.timestamp_raw)) { State::Index(p) => ((p as isize) + shift) as usize, State::Selection { start, ..} => { // Should be impossible, but lets handle it anyway ((start as isize) + shift) as usize }, } ); } } } fn clean_timestamp(mut raw: String) -> Option<(isize, String)> { // Rules: // - [R0] Must have exactly 1 colon (:) // - [R1] Must have exactly 1 period (.) // - [R2] The period must follow the colon // - [R3] No characters outside 0-9, colon, and period // - [R4] Unnecessary leading zeros besides normal padding are trimmed // - [R5] Each section is padded to the appropriate length (reversed for millis) const VALID_CHARS: RangeInclusive = '0'..=':'; const MIN_DIGIT_COUNTS: [usize; 3] = [1, 2, 3]; let mut colon_count = 0; let mut period_count = 0; let mut digit_counts = [0; 3]; for c in raw.chars() { match c { ':' => { colon_count += 1; if colon_count > 1 { return None; // Rejected [R0] } }, '.' => { period_count += 1; if colon_count == 0 /* [R2] */ || period_count > 1 /* [R1] */ { return None; // Rejected } }, _ if VALID_CHARS.contains(&c) || c == '.' => { let section = colon_count + period_count; digit_counts[section] += 1; }, _ => { return None; // Rejected [R3] } } } if period_count == 0 { return None; //Rejected [R1] } let mut i = 0; let mut cursor_shift = 0; for section in 0..3 { while digit_counts[section] < MIN_DIGIT_COUNTS[section] { // [R5] if section == 2 { raw.push('0'); } else { raw.insert(i, '0'); cursor_shift += 1; } digit_counts[section] += 1; } while digit_counts[section] > MIN_DIGIT_COUNTS[section] && if section == 2 { raw.ends_with('0') } else { raw.chars().nth(i).unwrap() == '0' } { // [R4] if section == 2 { raw.truncate(raw.len() - 1); } else { raw.remove(i); cursor_shift -= 1; } digit_counts[section] -= 1; } i += digit_counts[section] + 1; } Some((cursor_shift, raw)) } fn parse_validated_timestamp(s: &str) -> Duration { let (minutes, s) = s.split_at(s.find(':').expect( "parse_validated_timestamp received a timestamp without a :" )); let (seconds, millis) = s.split_at(s.find('.').expect( "parse_validated_timestamp received a timestamp without a . after the :" )); let minutes: u64 = minutes.parse() .expect("parse_validated_timestamp received an invalid number of minutes"); let seconds: u64 = seconds[1..].parse() .expect("parse_validated_timestamp received an invalid number of seconds"); let millis: u32 = millis[1..4].parse() .expect("parse_validated_timestamp received an invalid number of millis"); Duration::new(seconds + minutes * 60, millis * 1_000_000) } }