2022-01-08 23:54:24 +00:00
|
|
|
use iced::Space;
|
2022-01-08 02:07:11 +00:00
|
|
|
use iced_native::text_input::Value;
|
|
|
|
use core::ops::RangeInclusive;
|
|
|
|
use core::time::Duration;
|
|
|
|
use iced::Row;
|
|
|
|
use iced::Text;
|
2022-01-07 23:16:59 +00:00
|
|
|
use iced::Container;
|
2022-01-02 04:11:47 +00:00
|
|
|
use iced::Length;
|
2022-01-01 16:20:25 +00:00
|
|
|
use iced::Element;
|
2022-01-08 23:54:24 +00:00
|
|
|
use crate::styles::{Theme, FONT_VG5000};
|
2021-12-30 22:32:52 +00:00
|
|
|
use crate::app::Message;
|
|
|
|
|
|
|
|
use iced::widget::text_input::{self, TextInput};
|
|
|
|
use iced::widget::scrollable::{self, Scrollable};
|
2022-01-02 04:11:47 +00:00
|
|
|
use iced::Align;
|
2022-01-08 02:07:11 +00:00
|
|
|
use iced_native::widget::text_input::cursor::State;
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
pub enum LyricEvent {
|
|
|
|
LyricChanged(String),
|
|
|
|
TimestampChanged(String),
|
|
|
|
LineAdvanced,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl LyricEvent {
|
2022-01-08 15:28:42 +00:00
|
|
|
fn into_msg(self, line_no: usize) -> Message {
|
2022-01-08 02:07:11 +00:00
|
|
|
Message::LyricEvent {
|
|
|
|
kind: self,
|
|
|
|
line_no,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-01-02 03:08:00 +00:00
|
|
|
|
2021-12-30 22:32:52 +00:00
|
|
|
pub struct Lyrics {
|
|
|
|
lines: Vec<Lyric>,
|
|
|
|
scroll_state: scrollable::State,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Lyrics {
|
2022-01-02 04:11:47 +00:00
|
|
|
pub fn new() -> Lyrics {
|
2021-12-31 05:09:01 +00:00
|
|
|
let mut lyric = Lyric::new();
|
|
|
|
lyric.select();
|
2022-01-01 18:21:34 +00:00
|
|
|
|
2022-01-02 04:11:47 +00:00
|
|
|
Self {
|
2021-12-31 05:09:01 +00:00
|
|
|
lines: vec![lyric],
|
2022-01-01 18:21:34 +00:00
|
|
|
scroll_state: scrollable::State::new(),
|
2021-12-30 22:32:52 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-31 20:32:23 +00:00
|
|
|
pub fn insert_text(&mut self, text: String) {
|
|
|
|
|
|
|
|
let mut pieces = text.trim_end()
|
|
|
|
.split('\n')
|
|
|
|
.map(str::trim);
|
|
|
|
|
|
|
|
let (line_no, current_line) = self.current_line_mut();
|
|
|
|
|
|
|
|
current_line.deselect();
|
|
|
|
current_line.value.push_str(pieces.next().unwrap());
|
|
|
|
|
|
|
|
let pieces = pieces
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
.into_iter()
|
|
|
|
.map(str::to_owned)
|
|
|
|
.map(Lyric::new_with_value);
|
|
|
|
let n_pieces = pieces.size_hint().0;
|
|
|
|
|
|
|
|
self.lines.splice((line_no + 1)..(line_no + 1), pieces);
|
|
|
|
|
|
|
|
self.lines[line_no + n_pieces].select();
|
|
|
|
}
|
|
|
|
|
2022-01-08 02:07:11 +00:00
|
|
|
pub fn handle_event(&mut self, line_no: usize, kind: LyricEvent) {
|
|
|
|
match kind {
|
|
|
|
LyricEvent::LyricChanged(newval) => {
|
|
|
|
self.update_line(newval, line_no)
|
|
|
|
},
|
|
|
|
LyricEvent::TimestampChanged(newval) => {
|
|
|
|
self.lines[line_no].timestamp_update(newval)
|
|
|
|
},
|
|
|
|
LyricEvent::LineAdvanced => {
|
|
|
|
self.advance_line(line_no)
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-31 04:01:08 +00:00
|
|
|
pub fn update_line(&mut self, new_content: String, line_no: usize) {
|
2021-12-31 20:32:23 +00:00
|
|
|
self.lines[line_no].value = new_content;
|
2021-12-30 22:32:52 +00:00
|
|
|
}
|
|
|
|
|
2021-12-31 04:22:59 +00:00
|
|
|
pub fn advance_line(&mut self, current_line: usize) {
|
|
|
|
let new_line = current_line + 1;
|
|
|
|
|
2021-12-31 20:32:23 +00:00
|
|
|
let line = if new_line == self.lines.len() {
|
|
|
|
self.insert_line(new_line, None)
|
|
|
|
} else {
|
|
|
|
self.lines.get_mut(new_line)
|
|
|
|
.expect("Unexpected .advance_line with index beyond # of lines")
|
|
|
|
};
|
|
|
|
|
|
|
|
line.select();
|
2021-12-31 04:22:59 +00:00
|
|
|
|
|
|
|
self.lines.get_mut(current_line)
|
|
|
|
.unwrap()
|
|
|
|
.deselect();
|
|
|
|
}
|
|
|
|
|
2021-12-31 20:32:23 +00:00
|
|
|
pub fn insert_line(&mut self, index: usize, content: Option<String>) -> &mut Lyric {
|
|
|
|
self.lines.insert(index, match content {
|
|
|
|
Some(content) => Lyric::new_with_value(content),
|
|
|
|
None => Lyric::new(),
|
|
|
|
});
|
|
|
|
self.lines.get_mut(index).unwrap()
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn current_line_mut(&mut self) -> (usize, &mut Lyric) {
|
|
|
|
self.lines
|
|
|
|
.iter_mut()
|
|
|
|
.enumerate()
|
2022-01-08 15:28:42 +00:00
|
|
|
.find(|(_, l)| l.is_selected())
|
2021-12-31 20:32:23 +00:00
|
|
|
.expect("no line currently selected")
|
|
|
|
}
|
|
|
|
|
2022-01-02 04:11:47 +00:00
|
|
|
pub fn view(&mut self, theme: Theme) -> Element<Message> {
|
2021-12-31 05:09:01 +00:00
|
|
|
let is_sole_line = self.lines.len() == 1;
|
2022-01-08 23:54:24 +00:00
|
|
|
let spacers = (
|
|
|
|
Space::new(Length::Fill, Length::Units(30)),
|
|
|
|
Space::new(Length::Fill, Length::Units(30)),
|
|
|
|
);
|
2022-01-01 18:21:34 +00:00
|
|
|
|
2022-01-07 23:16:59 +00:00
|
|
|
let scroller = self.lines.iter_mut()
|
2021-12-31 04:01:08 +00:00
|
|
|
.enumerate()
|
2022-01-02 04:11:47 +00:00
|
|
|
.map(|(i, l)| l.view(is_sole_line, i, theme))
|
2022-01-08 23:54:24 +00:00
|
|
|
.fold(Scrollable::new(&mut self.scroll_state).push(spacers.0), |s, l| s.push(l))
|
|
|
|
.push(spacers.1)
|
2021-12-31 05:09:01 +00:00
|
|
|
.width(Length::Fill)
|
2022-01-07 23:16:59 +00:00
|
|
|
.align_items(Align::Center);
|
|
|
|
|
|
|
|
Container::new(scroller)
|
|
|
|
.height(Length::Fill)
|
|
|
|
.width(Length::Fill)
|
|
|
|
.center_y()
|
2022-01-02 04:11:47 +00:00
|
|
|
.into()
|
2021-12-30 22:32:52 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-02 04:11:47 +00:00
|
|
|
|
2021-12-30 22:32:52 +00:00
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
pub struct Lyric {
|
2022-01-08 02:07:11 +00:00
|
|
|
main_state: text_input::State,
|
|
|
|
timestamp_state: text_input::State,
|
2021-12-31 20:32:23 +00:00
|
|
|
pub value: String,
|
2022-01-08 02:07:11 +00:00
|
|
|
pub timestamp: Duration,
|
|
|
|
timestamp_raw: String,
|
2021-12-30 22:32:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Lyric {
|
2021-12-31 04:01:08 +00:00
|
|
|
pub fn new() -> Self {
|
2021-12-31 20:32:23 +00:00
|
|
|
Self::new_with_value(String::with_capacity(70))
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn new_with_value(val: String) -> Self {
|
2021-12-30 22:32:52 +00:00
|
|
|
Lyric {
|
2022-01-08 02:07:11 +00:00
|
|
|
main_state: text_input::State::new(),
|
|
|
|
timestamp_state: text_input::State::new(),
|
|
|
|
timestamp: Duration::ZERO,
|
|
|
|
timestamp_raw: String::from("0:00.000"),
|
2021-12-31 20:32:23 +00:00
|
|
|
value: val,
|
2021-12-30 22:32:52 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-31 05:09:01 +00:00
|
|
|
pub fn view(&mut self, show_placeholder: bool, line_no: usize, theme: Theme) -> Element<Message> {
|
|
|
|
|
2022-01-08 23:54:24 +00:00
|
|
|
const SMALL_SIZE: u16 = 20;
|
|
|
|
const LARGE_SIZE: u16 = 25;
|
|
|
|
const TIMESTAMP_W: u16 = 67;
|
|
|
|
const LINE_HEIGHT: u16 = 26;
|
|
|
|
const TOTAL_W: u16 = 400;
|
|
|
|
|
2022-01-08 02:07:11 +00:00
|
|
|
let is_focused = self.is_selected();
|
|
|
|
|
2021-12-31 05:09:01 +00:00
|
|
|
let placeholder = if show_placeholder {
|
|
|
|
"Paste some lyrics to get started"
|
2022-01-08 02:07:11 +00:00
|
|
|
} else if is_focused {
|
2021-12-31 05:09:01 +00:00
|
|
|
"..."
|
|
|
|
} else { "" };
|
|
|
|
|
2022-01-08 23:54:24 +00:00
|
|
|
let size = if is_focused { LARGE_SIZE } else { SMALL_SIZE };
|
2022-01-08 02:07:11 +00:00
|
|
|
|
|
|
|
let timestamp_input = TextInput::new(
|
|
|
|
&mut self.timestamp_state,
|
|
|
|
"",
|
|
|
|
&self.timestamp_raw,
|
2022-01-08 15:28:42 +00:00
|
|
|
move|new_value| LyricEvent::TimestampChanged(new_value).into_msg(line_no),
|
2022-01-08 02:07:11 +00:00
|
|
|
)
|
|
|
|
.style(theme)
|
2022-01-08 23:54:24 +00:00
|
|
|
.size(SMALL_SIZE)
|
|
|
|
.width(Length::Units(TIMESTAMP_W))
|
2022-01-08 15:28:42 +00:00
|
|
|
.on_submit(LyricEvent::LineAdvanced.into_msg(line_no))
|
2022-01-08 23:54:24 +00:00
|
|
|
.font(FONT_VG5000)
|
2022-01-08 02:07:11 +00:00
|
|
|
.into();
|
2021-12-31 05:09:01 +00:00
|
|
|
|
2022-01-08 02:07:11 +00:00
|
|
|
let text_input = TextInput::new(
|
|
|
|
&mut self.main_state,
|
2021-12-31 05:09:01 +00:00
|
|
|
placeholder,
|
2021-12-30 22:32:52 +00:00
|
|
|
&self.value,
|
2022-01-08 15:28:42 +00:00
|
|
|
move|new_value| LyricEvent::LyricChanged(new_value).into_msg(line_no),
|
2021-12-30 22:32:52 +00:00
|
|
|
)
|
2022-01-08 23:54:24 +00:00
|
|
|
.style(theme.active_lyric(is_focused))
|
2021-12-31 05:09:01 +00:00
|
|
|
.size(size)
|
2022-01-08 02:07:11 +00:00
|
|
|
.width(Length::Fill)
|
2022-01-08 15:28:42 +00:00
|
|
|
.on_submit(LyricEvent::LineAdvanced.into_msg(line_no))
|
2022-01-08 23:54:24 +00:00
|
|
|
.font(FONT_VG5000)
|
2022-01-08 02:07:11 +00:00
|
|
|
.into();
|
|
|
|
|
|
|
|
let l_bracket = Text::new("[")
|
2022-01-08 23:54:24 +00:00
|
|
|
.size(SMALL_SIZE)
|
|
|
|
.color(theme.reduced_text_color())
|
|
|
|
.font(FONT_VG5000)
|
2022-01-08 02:07:11 +00:00
|
|
|
.into();
|
2022-01-08 23:54:24 +00:00
|
|
|
let r_bracket = Text::new("] ")
|
|
|
|
.size(SMALL_SIZE)
|
|
|
|
.color(theme.reduced_text_color())
|
|
|
|
.font(FONT_VG5000)
|
2022-01-08 02:07:11 +00:00
|
|
|
.into();
|
|
|
|
|
|
|
|
Row::with_children(vec![l_bracket, timestamp_input, r_bracket, text_input])
|
2022-01-08 23:54:24 +00:00
|
|
|
.width(Length::Units(TOTAL_W))
|
|
|
|
.height(Length::Units(LINE_HEIGHT))
|
2022-01-08 02:07:11 +00:00
|
|
|
.align_items(Align::Center)
|
2021-12-30 22:32:52 +00:00
|
|
|
.into()
|
|
|
|
}
|
|
|
|
|
2021-12-31 04:22:59 +00:00
|
|
|
pub fn select(&mut self) {
|
2022-01-08 02:07:11 +00:00
|
|
|
self.main_state.focus();
|
|
|
|
self.main_state.move_cursor_to_end();
|
2021-12-31 04:22:59 +00:00
|
|
|
}
|
|
|
|
|
2021-12-31 20:32:23 +00:00
|
|
|
pub fn is_selected(&self) -> bool {
|
2022-01-08 02:07:11 +00:00
|
|
|
self.main_state.is_focused() || self.timestamp_state.is_focused()
|
2021-12-31 20:32:23 +00:00
|
|
|
}
|
|
|
|
|
2021-12-31 04:22:59 +00:00
|
|
|
pub fn deselect(&mut self) {
|
2022-01-08 02:07:11 +00:00
|
|
|
self.main_state.unfocus();
|
|
|
|
self.timestamp_state.unfocus();
|
|
|
|
}
|
|
|
|
|
|
|
|
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<char> = '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 {
|
2022-01-08 15:28:42 +00:00
|
|
|
raw.ends_with('0')
|
2022-01-08 02:07:11 +00:00
|
|
|
} 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)
|
2021-12-31 04:22:59 +00:00
|
|
|
}
|
2021-12-30 22:32:52 +00:00
|
|
|
}
|