Massive refactor to match the Elm architecture better
This commit is contained in:
parent
31d59fd0ee
commit
415b4e7fc2
File diff suppressed because it is too large
Load Diff
14
Cargo.toml
14
Cargo.toml
|
@ -14,7 +14,7 @@ exoquant = "0.2.0"
|
||||||
image = "0.23.14"
|
image = "0.23.14"
|
||||||
|
|
||||||
# Display windows & graphics
|
# Display windows & graphics
|
||||||
iced_native = "0.4.0"
|
#iced_native = "0.4.0"
|
||||||
|
|
||||||
# Native file dialogs
|
# Native file dialogs
|
||||||
rfd = "0.6.3"
|
rfd = "0.6.3"
|
||||||
|
@ -31,12 +31,20 @@ version = "0.3.0"
|
||||||
[dependencies.iced]
|
[dependencies.iced]
|
||||||
# Display windows & graphics
|
# Display windows & graphics
|
||||||
features = ["canvas", "image"]
|
features = ["canvas", "image"]
|
||||||
version = "0.3.0"
|
git = "https://github.com/iced-rs/iced.git"
|
||||||
|
|
||||||
|
[dependencies.iced_native]
|
||||||
|
# Native-display only GUI features
|
||||||
|
git = "https://github.com/iced-rs/iced.git"
|
||||||
|
|
||||||
|
[dependencies.iced_lazy]
|
||||||
|
# Responsive widget design
|
||||||
|
git = "https://github.com/iced-rs/iced.git"
|
||||||
|
|
||||||
[dependencies.iced_futures]
|
[dependencies.iced_futures]
|
||||||
# Display windows & graphics
|
# Display windows & graphics
|
||||||
features = ["smol"]
|
features = ["smol"]
|
||||||
version = "0.3.0"
|
git = "https://github.com/iced-rs/iced.git"
|
||||||
|
|
||||||
[dependencies.rodio]
|
[dependencies.rodio]
|
||||||
# Playing audio
|
# Playing audio
|
||||||
|
|
110
src/app.rs
110
src/app.rs
|
@ -1,13 +1,10 @@
|
||||||
use crate::lyrics::LyricEvent;
|
use crate::editor::view_editor;
|
||||||
|
use crate::file_select::view_fileselector;
|
||||||
|
use crate::model::Model;
|
||||||
use crate::controls::ControlsEvent;
|
use crate::controls::ControlsEvent;
|
||||||
use crate::load_song::load_song;
|
|
||||||
use crate::editor::Editor;
|
|
||||||
use crate::file_select::FileSelector;
|
|
||||||
use rfd::AsyncFileDialog;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use core::time::Duration;
|
use core::time::Duration;
|
||||||
use iced::Subscription;
|
use iced::Subscription;
|
||||||
use iced::Clipboard;
|
|
||||||
use iced::Command;
|
use iced::Command;
|
||||||
use iced::Application;
|
use iced::Application;
|
||||||
use iced::Element;
|
use iced::Element;
|
||||||
|
@ -17,12 +14,9 @@ use iced_native::subscription;
|
||||||
use iced_native::keyboard;
|
use iced_native::keyboard;
|
||||||
use iced_native::window;
|
use iced_native::window;
|
||||||
use iced_native::event::Event;
|
use iced_native::event::Event;
|
||||||
|
use crate::model::editing::LyricEvent;
|
||||||
|
|
||||||
pub struct DelyriumApp {
|
pub struct DelyriumApp(Model);
|
||||||
lyrics_component: Option<Editor>,
|
|
||||||
file_selector: FileSelector,
|
|
||||||
size: (u32, u32),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum Message {
|
pub enum Message {
|
||||||
|
@ -31,10 +25,10 @@ pub enum Message {
|
||||||
kind: LyricEvent,
|
kind: LyricEvent,
|
||||||
},
|
},
|
||||||
PasteSent,
|
PasteSent,
|
||||||
|
PasteRead(String),
|
||||||
Tick,
|
Tick,
|
||||||
PromptForFile,
|
PromptForFile,
|
||||||
FileOpened(PathBuf),
|
FileOpened(PathBuf),
|
||||||
Resized(u32, u32),
|
|
||||||
ControlsEvent(ControlsEvent),
|
ControlsEvent(ControlsEvent),
|
||||||
Null,
|
Null,
|
||||||
}
|
}
|
||||||
|
@ -46,11 +40,7 @@ impl Application for DelyriumApp {
|
||||||
|
|
||||||
fn new(_: Self::Flags) -> (Self, Command<Message>) {
|
fn new(_: Self::Flags) -> (Self, Command<Message>) {
|
||||||
(
|
(
|
||||||
DelyriumApp {
|
DelyriumApp(Model::DEFAULT),
|
||||||
lyrics_component: None,
|
|
||||||
file_selector: FileSelector::default(),
|
|
||||||
size: (0, 0),
|
|
||||||
},
|
|
||||||
Command::none(),
|
Command::none(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -59,75 +49,18 @@ impl Application for DelyriumApp {
|
||||||
String::from("Delyrium")
|
String::from("Delyrium")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, message: Message, clipboard: &mut Clipboard) -> Command<Message>{
|
fn update(&mut self, message: Message) -> Command<Message>{
|
||||||
let mut command = None;
|
self.0.update(message)
|
||||||
match message {
|
|
||||||
Message::LyricEvent { line_no, kind } => {
|
|
||||||
if let Some(lyrics) = self.lyrics_component.as_mut() {
|
|
||||||
lyrics.handle_lyric_event(line_no, kind);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Message::PasteSent => {
|
|
||||||
if let Some(lyrics) = self.lyrics_component.as_mut() {
|
|
||||||
#[allow(clippy::or_fun_call)] // This is a const
|
|
||||||
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 => {
|
|
||||||
if self.lyrics_component.is_none() {
|
|
||||||
self.file_selector.tick();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Message::FileOpened(path) => {
|
|
||||||
println!("File opened! {}", path.display());
|
|
||||||
let song = load_song(&path).unwrap().format;
|
|
||||||
let (editor, cmd) = Editor::new(song, self.size);
|
|
||||||
self.lyrics_component = Some(editor);
|
|
||||||
command = Some(cmd);
|
|
||||||
},
|
|
||||||
Message::PromptForFile => {
|
|
||||||
let task = async {
|
|
||||||
let handle = AsyncFileDialog::new()
|
|
||||||
.add_filter("Song Files", &["mp3", "flac", "ogg", "opus", "wav", "mkv"])
|
|
||||||
.set_title("Select a song")
|
|
||||||
.pick_file()
|
|
||||||
.await;
|
|
||||||
if let Some(h) = handle {
|
|
||||||
Message::FileOpened(h.path().to_owned())
|
|
||||||
} else {
|
|
||||||
Message::Null
|
|
||||||
}
|
|
||||||
};
|
|
||||||
command = Some(task.into());
|
|
||||||
},
|
|
||||||
Message::Resized(w, h) => {
|
|
||||||
self.size = (w, h);
|
|
||||||
if let Some(lyrics) = self.lyrics_component.as_mut() {
|
|
||||||
lyrics.notify_resized(w, h);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Message::ControlsEvent(e) => {
|
|
||||||
if let Some(lyrics) = self.lyrics_component.as_mut() {
|
|
||||||
lyrics.handle_controls_event(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Message::Null => { },
|
|
||||||
}
|
|
||||||
|
|
||||||
command.unwrap_or_else(Command::none)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view(&mut self) -> Element<Message> {
|
fn view(&mut self) -> Element<Message> {
|
||||||
if let Some(lyrics) = self.lyrics_component.as_mut() {
|
match &mut self.0 {
|
||||||
lyrics.view()
|
Model::Editing(editing) => {
|
||||||
} else {
|
view_editor(editing)
|
||||||
self.file_selector.view()
|
},
|
||||||
|
Model::FilePicker { tick } => {
|
||||||
|
view_fileselector(*tick)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,8 +71,8 @@ impl Application for DelyriumApp {
|
||||||
match (key_code, modifiers) {
|
match (key_code, modifiers) {
|
||||||
(
|
(
|
||||||
keyboard::KeyCode::V,
|
keyboard::KeyCode::V,
|
||||||
keyboard::Modifiers { control, .. }
|
modifiers
|
||||||
) if control => {
|
) if modifiers.control() => {
|
||||||
Some(Message::PasteSent)
|
Some(Message::PasteSent)
|
||||||
}
|
}
|
||||||
_ => { None }
|
_ => { None }
|
||||||
|
@ -148,15 +81,12 @@ impl Application for DelyriumApp {
|
||||||
Event::Window(window::Event::FileDropped(path)) => {
|
Event::Window(window::Event::FileDropped(path)) => {
|
||||||
Some(Message::FileOpened(path))
|
Some(Message::FileOpened(path))
|
||||||
},
|
},
|
||||||
Event::Window(window::Event::Resized{width,height}) => {
|
|
||||||
Some(Message::Resized(width,height))
|
|
||||||
},
|
|
||||||
_ => { None }
|
_ => { None }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let is_animating = if let Some(editor) = &self.lyrics_component {
|
let is_animating = if let Model::Editing(e) = &self.0 {
|
||||||
editor.is_animating()
|
e.is_animating()
|
||||||
} else {
|
} else {
|
||||||
true
|
true
|
||||||
};
|
};
|
||||||
|
|
|
@ -26,7 +26,7 @@ pub enum ControlsEvent {
|
||||||
DurationDiscovered(Duration),
|
DurationDiscovered(Duration),
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ErrorState {
|
pub enum ErrorState {
|
||||||
Error(PlayerError),
|
Error(PlayerError),
|
||||||
NoError {
|
NoError {
|
||||||
player: Player,
|
player: Player,
|
||||||
|
@ -36,9 +36,7 @@ enum ErrorState {
|
||||||
|
|
||||||
use ErrorState::*;
|
use ErrorState::*;
|
||||||
|
|
||||||
pub struct Controls {
|
pub struct Controls(pub ErrorState);
|
||||||
error_state: ErrorState,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Controls {
|
impl Controls {
|
||||||
pub fn new(song: Box<dyn FormatReader>) -> (Self, Command<Message>) {
|
pub fn new(song: Box<dyn FormatReader>) -> (Self, Command<Message>) {
|
||||||
|
@ -50,17 +48,16 @@ impl Controls {
|
||||||
|d| Message::ControlsEvent(ControlsEvent::DurationDiscovered(d)),
|
|d| Message::ControlsEvent(ControlsEvent::DurationDiscovered(d)),
|
||||||
);
|
);
|
||||||
|
|
||||||
(Controls {
|
(
|
||||||
error_state: NoError {
|
Controls(NoError {
|
||||||
has_device: player.has_output_device(),
|
has_device: player.has_output_device(),
|
||||||
player,
|
player,
|
||||||
}
|
}),
|
||||||
}, duration_cmd)
|
duration_cmd
|
||||||
|
)
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
(Controls {
|
(Controls(Error(e)), Command::none())
|
||||||
error_state: Error(e)
|
|
||||||
}, Command::none())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -72,7 +69,7 @@ impl Controls {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_event(&mut self, event: ControlsEvent) {
|
pub fn handle_event(&mut self, event: ControlsEvent) {
|
||||||
if let NoError { player, has_device } = &mut self.error_state {
|
if let NoError { player, has_device } = &mut self.0 {
|
||||||
let result = match event {
|
let result = match event {
|
||||||
ControlsEvent::SeekPosition(pos) => player.seek_percentage(pos),
|
ControlsEvent::SeekPosition(pos) => player.seek_percentage(pos),
|
||||||
ControlsEvent::TogglePlay => player.toggle_play(),
|
ControlsEvent::TogglePlay => player.toggle_play(),
|
||||||
|
@ -88,14 +85,14 @@ impl Controls {
|
||||||
*has_device = now_has_device;
|
*has_device = now_has_device;
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
self.error_state = Error(e);
|
self.0 = Error(e);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_playing(&self) -> bool {
|
pub fn is_playing(&self) -> bool {
|
||||||
if let NoError { player, has_device: true } = &self.error_state {
|
if let NoError { player, has_device: true } = &self.0 {
|
||||||
player.is_playing()
|
player.is_playing()
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
|
@ -107,7 +104,7 @@ impl Controls {
|
||||||
/// If there was an error, this will be `None`. In all other cases, this will return
|
/// If there was an error, this will be `None`. In all other cases, this will return
|
||||||
/// `Some`.
|
/// `Some`.
|
||||||
pub fn position(&self) -> Option<Duration> {
|
pub fn position(&self) -> Option<Duration> {
|
||||||
if let NoError { player, .. } = &self.error_state {
|
if let NoError { player, .. } = &self.0 {
|
||||||
Some(player.position())
|
Some(player.position())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
@ -119,7 +116,7 @@ impl Program<Message> for (&Controls, Theme) {
|
||||||
fn draw(&self, bounds: Rectangle<f32>, _cursor: Cursor) -> Vec<Geometry> {
|
fn draw(&self, bounds: Rectangle<f32>, _cursor: Cursor) -> Vec<Geometry> {
|
||||||
let mut frame = Frame::new(bounds.size());
|
let mut frame = Frame::new(bounds.size());
|
||||||
|
|
||||||
match &self.0.error_state {
|
match &self.0.0 {
|
||||||
NoError { player, has_device: true } => {
|
NoError { player, has_device: true } => {
|
||||||
let mut background = self.1.text_color;
|
let mut background = self.1.text_color;
|
||||||
background.a = 0.2;
|
background.a = 0.2;
|
||||||
|
|
245
src/editor.rs
245
src/editor.rs
|
@ -1,136 +1,135 @@
|
||||||
use crate::lyrics::LyricEvent;
|
use crate::model::editing::Lyric;
|
||||||
use iced::Command;
|
use iced::Text;
|
||||||
use iced::Image;
|
use crate::model::editing::LyricEvent;
|
||||||
use iced::image::Handle;
|
use iced::widget::text_input::TextInput;
|
||||||
use image::DynamicImage;
|
use iced::Alignment;
|
||||||
use crate::controls::ControlsEvent;
|
use iced::widget::scrollable::{self, Scrollable};
|
||||||
|
use iced::Space;
|
||||||
|
use iced::Canvas;
|
||||||
|
use crate::styles::{Theme, FONT_VG5000};
|
||||||
use crate::controls::Controls;
|
use crate::controls::Controls;
|
||||||
use crate::lyrics::Lyric;
|
use crate::model::Editing;
|
||||||
use crate::lyrics::Lyrics;
|
use iced::Image;
|
||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
use iced::Element;
|
use iced::Element;
|
||||||
use crate::styles::Theme;
|
|
||||||
use iced::Container;
|
use iced::Container;
|
||||||
use iced::Row;
|
use iced::Row;
|
||||||
use crate::palette::Palette;
|
|
||||||
use crate::load_song::extract_cover;
|
|
||||||
use iced::Length;
|
use iced::Length;
|
||||||
use image::imageops::FilterType;
|
|
||||||
|
|
||||||
use symphonia::core::formats::FormatReader;
|
pub fn view_editor(editing: &mut Editing) -> Element<Message> {
|
||||||
|
let row = if let Some(margin_bg) = &editing.cached_resized_bg {
|
||||||
|
|
||||||
use image::GenericImageView;
|
let (img1, img2) = (
|
||||||
pub struct Editor {
|
Image::new(margin_bg.clone())
|
||||||
lyrics: Lyrics,
|
.width(Length::FillPortion(1))
|
||||||
theme: Theme,
|
.height(Length::Fill),
|
||||||
controls: Controls,
|
Image::new(margin_bg.clone())
|
||||||
bg_img: DynamicImage,
|
.width(Length::FillPortion(1))
|
||||||
cached_resized_bg: Option<Handle>,
|
.height(Length::Fill),
|
||||||
dimensions: (u32, u32),
|
);
|
||||||
|
|
||||||
|
Row::new()
|
||||||
|
.push(img1)
|
||||||
|
.push(view_progress(&editing.controls, editing.theme))
|
||||||
|
.push(view_lyrics(&mut editing.lyrics, &mut editing.scroll_state, editing.theme))
|
||||||
|
.push(img2)
|
||||||
|
} else {
|
||||||
|
Row::new()
|
||||||
|
.push(view_progress(&editing.controls, editing.theme))
|
||||||
|
.push(view_lyrics(&mut editing.lyrics, &mut editing.scroll_state, editing.theme))
|
||||||
|
};
|
||||||
|
|
||||||
|
Container::new(row)
|
||||||
|
.style(editing.theme)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Editor {
|
pub fn view_lyrics<'a>(lyrics: &'a mut [Lyric], scroll_state: &'a mut scrollable::State, theme: Theme) -> Element<'a, Message> {
|
||||||
pub fn new(mut song: Box<dyn FormatReader>, size: (u32, u32)) -> (Self, Command<Message>) {
|
let is_sole_line = lyrics.len() == 1;
|
||||||
let cover = extract_cover(song.as_mut());
|
let spacers = (
|
||||||
|
Space::new(Length::Fill, Length::Units(30)),
|
||||||
|
Space::new(Length::Fill, Length::Units(30)),
|
||||||
|
);
|
||||||
|
|
||||||
let theme = cover.as_ref()
|
let scroller = lyrics.iter_mut()
|
||||||
.map(|cover| {
|
.enumerate()
|
||||||
Theme::from_palette(
|
.map(|(i, l)| view_lyric(l, is_sole_line, i, theme))
|
||||||
Palette::generate(cover)
|
.fold(Scrollable::new(scroll_state).push(spacers.0), |s, l| s.push(l))
|
||||||
)
|
.push(spacers.1)
|
||||||
}).unwrap_or_else(Theme::default);
|
.width(Length::Fill)
|
||||||
|
.align_items(Alignment::Center);
|
||||||
|
|
||||||
let cover = cover.expect("TODO");
|
Container::new(scroller)
|
||||||
|
.height(Length::Fill)
|
||||||
#[cfg(not(debug_assertions))]
|
.width(Length::Units(400))
|
||||||
let cover = cover.blur((cover.width() / 100) as f32);
|
.center_y()
|
||||||
|
.into()
|
||||||
let bg_img = DynamicImage::ImageBgra8(cover.into_bgra8());
|
}
|
||||||
|
|
||||||
let (controls, cmd) = Controls::new(song);
|
pub fn view_lyric(lyric: &mut Lyric, show_placeholder: bool, line_no: usize, theme: Theme) -> Row<Message> {
|
||||||
|
|
||||||
let mut editor = Self {
|
const SMALL_SIZE: u16 = 20;
|
||||||
lyrics: Lyrics::new(),
|
const LARGE_SIZE: u16 = 25;
|
||||||
dimensions: size,
|
const TIMESTAMP_W: u16 = 67;
|
||||||
cached_resized_bg: None,
|
const LINE_HEIGHT: u16 = 26;
|
||||||
controls, theme, bg_img,
|
const TOTAL_W: u16 = 400;
|
||||||
};
|
|
||||||
|
let is_focused = lyric.is_selected();
|
||||||
editor.notify_resized(size.0, size.1);
|
|
||||||
|
let placeholder = if show_placeholder {
|
||||||
(editor, cmd)
|
"Paste some lyrics to get started"
|
||||||
}
|
} else if is_focused {
|
||||||
|
"..."
|
||||||
// TODO: work on untangling this mess
|
} else { "" };
|
||||||
pub fn handle_controls_event(&mut self, event: ControlsEvent) {
|
|
||||||
self.controls.handle_event(event)
|
let size = if is_focused { LARGE_SIZE } else { SMALL_SIZE };
|
||||||
}
|
|
||||||
pub fn insert_text(&mut self, text: String) {
|
let timestamp_input = TextInput::new(
|
||||||
self.lyrics.insert_text(text);
|
&mut lyric.timestamp_state,
|
||||||
}
|
"",
|
||||||
pub fn handle_lyric_event(&mut self, line_no: usize, kind: LyricEvent) {
|
&lyric.timestamp_raw,
|
||||||
self.lyrics.handle_event(line_no, kind, || self.controls.position())
|
move|new_value| LyricEvent::TimestampChanged(new_value).into_msg(line_no),
|
||||||
}
|
)
|
||||||
pub fn current_line_mut(&mut self) -> (usize, &mut Lyric) {
|
.style(theme)
|
||||||
self.lyrics.current_line_mut()
|
.size(SMALL_SIZE)
|
||||||
}
|
.width(Length::Units(TIMESTAMP_W))
|
||||||
|
.on_submit(LyricEvent::LineAdvanced.into_msg(line_no))
|
||||||
pub fn is_animating(&self) -> bool {
|
.font(FONT_VG5000)
|
||||||
self.controls.is_playing()
|
.into();
|
||||||
}
|
|
||||||
|
let text_input = TextInput::new(
|
||||||
fn calculate_margin_width(&self) -> u32 {
|
&mut lyric.main_state,
|
||||||
let (w, _h) = self.dimensions;
|
placeholder,
|
||||||
let body_size = (w / 5).max(550);
|
&lyric.value,
|
||||||
|
move|new_value| LyricEvent::LyricChanged(new_value).into_msg(line_no),
|
||||||
w.saturating_sub(body_size) / 2
|
)
|
||||||
}
|
.style(theme.active_lyric(is_focused))
|
||||||
|
.size(size)
|
||||||
pub fn notify_resized(&mut self, w: u32, h: u32) {
|
.width(Length::Fill)
|
||||||
self.dimensions = (w, h);
|
.on_submit(LyricEvent::LineAdvanced.into_msg(line_no))
|
||||||
|
.font(FONT_VG5000)
|
||||||
let (_w, h) = self.dimensions;
|
.into();
|
||||||
let margin_w = self.calculate_margin_width();
|
|
||||||
|
let l_bracket = Text::new("[")
|
||||||
self.cached_resized_bg = if margin_w != 0 {
|
.size(SMALL_SIZE)
|
||||||
let resized_bg = self.bg_img.resize_to_fill(margin_w, h, FilterType::Nearest);
|
.color(theme.reduced_text_color())
|
||||||
|
.font(FONT_VG5000)
|
||||||
Some(Handle::from_pixels(
|
.into();
|
||||||
resized_bg.width(),
|
let r_bracket = Text::new("] ")
|
||||||
resized_bg.height(),
|
.size(SMALL_SIZE)
|
||||||
resized_bg.into_bgra8().into_raw()
|
.color(theme.reduced_text_color())
|
||||||
))
|
.font(FONT_VG5000)
|
||||||
} else { None };
|
.into();
|
||||||
}
|
|
||||||
|
Row::with_children(vec![l_bracket, timestamp_input, r_bracket, text_input])
|
||||||
pub fn view(&mut self) -> Element<Message> {
|
.width(Length::Units(TOTAL_W))
|
||||||
|
.height(Length::Units(LINE_HEIGHT))
|
||||||
let row = if let Some(margin_bg) = &self.cached_resized_bg {
|
.align_items(Alignment::Center)
|
||||||
let (w, _h) = self.dimensions;
|
}
|
||||||
|
|
||||||
let (img1, img2) = (
|
pub fn view_progress(controls: &Controls, theme: Theme) -> Canvas<Message, (&Controls, Theme)> {
|
||||||
Image::new(margin_bg.clone())
|
Canvas::new((&*controls, theme))
|
||||||
.width(Length::Units(w as u16))
|
.width(Length::Units(50))
|
||||||
.height(Length::Fill),
|
.height(Length::Fill)
|
||||||
Image::new(margin_bg.clone())
|
|
||||||
.width(Length::Units(w as u16))
|
|
||||||
.height(Length::Fill),
|
|
||||||
);
|
|
||||||
|
|
||||||
Row::new()
|
|
||||||
.push(img1)
|
|
||||||
.push(self.controls.view_progress(self.theme))
|
|
||||||
.push(self.lyrics.view(self.theme))
|
|
||||||
.push(img2)
|
|
||||||
} else {
|
|
||||||
Row::new()
|
|
||||||
.push(self.controls.view_progress(self.theme))
|
|
||||||
.push(self.lyrics.view(self.theme))
|
|
||||||
};
|
|
||||||
|
|
||||||
Container::new(row)
|
|
||||||
.style(self.theme)
|
|
||||||
.height(Length::Fill)
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,8 +15,7 @@ use iced::mouse;
|
||||||
use iced::Color;
|
use iced::Color;
|
||||||
use iced::Rectangle;
|
use iced::Rectangle;
|
||||||
use iced::Length;
|
use iced::Length;
|
||||||
use iced::HorizontalAlignment;
|
use iced::alignment::{Vertical, Horizontal};
|
||||||
use iced::VerticalAlignment;
|
|
||||||
use iced::widget::canvas::{self, Canvas};
|
use iced::widget::canvas::{self, Canvas};
|
||||||
|
|
||||||
/* RYGCBM
|
/* RYGCBM
|
||||||
|
@ -45,26 +44,16 @@ const FONT_MR_PIXEL: Font = Font::External {
|
||||||
bytes: include_bytes!("../fonts/mister-pixel/mister-pixel.otf"),
|
bytes: include_bytes!("../fonts/mister-pixel/mister-pixel.otf"),
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
pub fn view_fileselector(tick: usize) -> Element<'static, Message> {
|
||||||
pub struct FileSelector {
|
Canvas::new(tick)
|
||||||
tick: usize,
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileSelector {
|
impl Program<Message> for usize {
|
||||||
pub fn tick(&mut self) {
|
|
||||||
self.tick = (self.tick + 1) % MAX_TICKS;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn view(&mut self) -> Element<Message> {
|
|
||||||
Canvas::new(self)
|
|
||||||
.width(Length::Fill)
|
|
||||||
.height(Length::Fill)
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Program<Message> for FileSelector {
|
|
||||||
fn draw(&self, bounds: Rectangle, _cursor: Cursor) -> Vec<Geometry> {
|
fn draw(&self, bounds: Rectangle, _cursor: Cursor) -> Vec<Geometry> {
|
||||||
|
let tick = self;
|
||||||
let offset_per_index = MAX_TICKS / COLORS.len();
|
let offset_per_index = MAX_TICKS / COLORS.len();
|
||||||
|
|
||||||
const TEXT_RECT_W: f32 = 350.;
|
const TEXT_RECT_W: f32 = 350.;
|
||||||
|
@ -93,7 +82,7 @@ impl Program<Message> for FileSelector {
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(index, color)| {
|
.map(|(index, color)| {
|
||||||
let size =
|
let size =
|
||||||
((self.tick + offset_per_index * index) % MAX_TICKS) as f32 /
|
((tick + offset_per_index * index) % MAX_TICKS) as f32 /
|
||||||
MAX_TICKS as f32;
|
MAX_TICKS as f32;
|
||||||
|
|
||||||
let top_left = interpolate(text_rect.0, Point::ORIGIN, size);
|
let top_left = interpolate(text_rect.0, Point::ORIGIN, size);
|
||||||
|
@ -114,8 +103,8 @@ impl Program<Message> for FileSelector {
|
||||||
frame.fill_text(
|
frame.fill_text(
|
||||||
Text {
|
Text {
|
||||||
content: String::from("select a song to start"),
|
content: String::from("select a song to start"),
|
||||||
horizontal_alignment: HorizontalAlignment::Center,
|
horizontal_alignment: Horizontal::Center,
|
||||||
vertical_alignment: VerticalAlignment::Center,
|
vertical_alignment: Vertical::Center,
|
||||||
size: 32.,
|
size: 32.,
|
||||||
color: Color::WHITE,
|
color: Color::WHITE,
|
||||||
font: FONT_MR_PIXEL,
|
font: FONT_MR_PIXEL,
|
||||||
|
|
|
@ -3,13 +3,13 @@ use iced::settings::Settings;
|
||||||
|
|
||||||
mod palette;
|
mod palette;
|
||||||
mod app;
|
mod app;
|
||||||
mod lyrics;
|
|
||||||
mod styles;
|
mod styles;
|
||||||
mod file_select;
|
mod file_select;
|
||||||
mod load_song;
|
mod load_song;
|
||||||
mod editor;
|
mod editor;
|
||||||
mod player;
|
mod player;
|
||||||
mod controls;
|
mod controls;
|
||||||
|
mod model;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
app::DelyriumApp::run(Settings::default()).unwrap();
|
app::DelyriumApp::run(Settings::default()).unwrap();
|
||||||
|
|
|
@ -1,19 +1,44 @@
|
||||||
use iced::Space;
|
use image::GenericImageView;
|
||||||
use iced_native::text_input::Value;
|
|
||||||
use core::ops::RangeInclusive;
|
use core::ops::RangeInclusive;
|
||||||
use core::time::Duration;
|
use iced_native::widget::text_input::Value;
|
||||||
use iced::Row;
|
|
||||||
use iced::Text;
|
|
||||||
use iced::Container;
|
|
||||||
use iced::Length;
|
|
||||||
use iced::Element;
|
|
||||||
use crate::styles::{Theme, FONT_VG5000};
|
|
||||||
use crate::app::Message;
|
|
||||||
|
|
||||||
use iced::widget::text_input::{self, TextInput};
|
|
||||||
use iced::widget::scrollable::{self, Scrollable};
|
|
||||||
use iced::Align;
|
|
||||||
use iced_native::widget::text_input::cursor::State;
|
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 symphonia::core::formats::FormatReader;
|
||||||
|
|
||||||
|
use iced::widget::text_input;
|
||||||
|
use iced::widget::scrollable;
|
||||||
|
use iced::clipboard;
|
||||||
|
use iced_lazy::responsive;
|
||||||
|
|
||||||
|
pub struct Editing {
|
||||||
|
pub lyrics: Vec<Lyric>,
|
||||||
|
pub bg_img: Option<DynamicImage>,
|
||||||
|
pub controls: Controls,
|
||||||
|
pub theme: Theme,
|
||||||
|
|
||||||
|
/* UI state */
|
||||||
|
pub cached_resized_bg: Option<Handle>,
|
||||||
|
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)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum LyricEvent {
|
pub enum LyricEvent {
|
||||||
|
@ -23,7 +48,7 @@ pub enum LyricEvent {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LyricEvent {
|
impl LyricEvent {
|
||||||
fn into_msg(self, line_no: usize) -> Message {
|
pub fn into_msg(self, line_no: usize) -> Message {
|
||||||
Message::LyricEvent {
|
Message::LyricEvent {
|
||||||
kind: self,
|
kind: self,
|
||||||
line_no,
|
line_no,
|
||||||
|
@ -31,20 +56,128 @@ impl LyricEvent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Lyrics {
|
impl Editing {
|
||||||
lines: Vec<Lyric>,
|
pub fn new(mut song: Box<dyn FormatReader>) -> (Self, Command<Message>) {
|
||||||
scroll_state: scrollable::State,
|
let cover = extract_cover(song.as_mut());
|
||||||
}
|
|
||||||
|
|
||||||
impl Lyrics {
|
let theme = cover.as_ref()
|
||||||
pub fn new() -> Lyrics {
|
.map(|cover| {
|
||||||
let mut lyric = Lyric::new();
|
Theme::from_palette(
|
||||||
lyric.select();
|
Palette::generate(cover)
|
||||||
|
)
|
||||||
|
}).unwrap_or_else(Theme::default);
|
||||||
|
|
||||||
Self {
|
let cover = cover.map(|cover| {
|
||||||
lines: vec![lyric],
|
#[cfg(not(debug_assertions))]
|
||||||
scroll_state: scrollable::State::new(),
|
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(),
|
||||||
|
));
|
||||||
|
|
||||||
|
let (controls, cmd) = Controls::new(song);
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
(editor, cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&mut self, message: Message) -> Command<Message> {
|
||||||
|
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<Duration>) {
|
||||||
|
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<String>) -> &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<usize> {
|
||||||
|
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) {
|
pub fn insert_text(&mut self, text: String) {
|
||||||
|
@ -53,107 +186,24 @@ impl Lyrics {
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.map(str::trim);
|
.map(str::trim);
|
||||||
|
|
||||||
let (line_no, current_line) = self.current_line_mut();
|
if let Some(line_no) = self.current_line() {
|
||||||
|
let current_line = self.lyrics.get_mut(line_no).unwrap();
|
||||||
|
|
||||||
current_line.deselect();
|
current_line.deselect();
|
||||||
current_line.value.push_str(pieces.next().unwrap());
|
current_line.value.push_str(pieces.next().unwrap());
|
||||||
|
|
||||||
let pieces = pieces
|
let pieces = pieces
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(str::to_owned)
|
.map(str::to_owned)
|
||||||
.map(Lyric::new_with_value);
|
.map(Lyric::new_with_value);
|
||||||
let n_pieces = pieces.size_hint().0;
|
let n_pieces = pieces.size_hint().0;
|
||||||
|
|
||||||
self.lines.splice((line_no + 1)..(line_no + 1), pieces);
|
self.lyrics.splice((line_no + 1)..(line_no + 1), pieces);
|
||||||
|
|
||||||
self.lines[line_no + n_pieces].select();
|
self.lyrics[line_no + n_pieces].select();
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_event(&mut self, line_no: usize, kind: LyricEvent, timestamp: impl Fn() -> Option<Duration>) {
|
|
||||||
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, timestamp())
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_line(&mut self, new_content: String, line_no: usize) {
|
|
||||||
self.lines[line_no].value = new_content;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn advance_line(&mut self, current_line: usize, timestamp: Option<Duration>) {
|
|
||||||
let new_line = current_line + 1;
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
let previous_line = self.lines.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<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()
|
|
||||||
.find(|(_, l)| l.is_selected())
|
|
||||||
.expect("no line currently selected")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn view(&mut self, theme: Theme) -> Element<Message> {
|
|
||||||
let is_sole_line = self.lines.len() == 1;
|
|
||||||
let spacers = (
|
|
||||||
Space::new(Length::Fill, Length::Units(30)),
|
|
||||||
Space::new(Length::Fill, Length::Units(30)),
|
|
||||||
);
|
|
||||||
|
|
||||||
let scroller = self.lines.iter_mut()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(i, l)| l.view(is_sole_line, i, theme))
|
|
||||||
.fold(Scrollable::new(&mut self.scroll_state).push(spacers.0), |s, l| s.push(l))
|
|
||||||
.push(spacers.1)
|
|
||||||
.width(Length::Fill)
|
|
||||||
.align_items(Align::Center);
|
|
||||||
|
|
||||||
Container::new(scroller)
|
|
||||||
.height(Length::Fill)
|
|
||||||
.width(Length::Fill)
|
|
||||||
.center_y()
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Lyric {
|
|
||||||
main_state: text_input::State,
|
|
||||||
timestamp_state: text_input::State,
|
|
||||||
pub value: String,
|
|
||||||
pub timestamp: Duration,
|
|
||||||
timestamp_raw: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Lyric {
|
impl Lyric {
|
||||||
|
@ -171,66 +221,8 @@ impl Lyric {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn view(&mut self, show_placeholder: bool, line_no: usize, theme: Theme) -> Element<Message> {
|
pub fn is_selected(&self) -> bool {
|
||||||
|
self.main_state.is_focused() || self.timestamp_state.is_focused()
|
||||||
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;
|
|
||||||
|
|
||||||
let is_focused = self.is_selected();
|
|
||||||
|
|
||||||
let placeholder = if show_placeholder {
|
|
||||||
"Paste some lyrics to get started"
|
|
||||||
} else if is_focused {
|
|
||||||
"..."
|
|
||||||
} else { "" };
|
|
||||||
|
|
||||||
let size = if is_focused { LARGE_SIZE } else { SMALL_SIZE };
|
|
||||||
|
|
||||||
let timestamp_input = TextInput::new(
|
|
||||||
&mut self.timestamp_state,
|
|
||||||
"",
|
|
||||||
&self.timestamp_raw,
|
|
||||||
move|new_value| LyricEvent::TimestampChanged(new_value).into_msg(line_no),
|
|
||||||
)
|
|
||||||
.style(theme)
|
|
||||||
.size(SMALL_SIZE)
|
|
||||||
.width(Length::Units(TIMESTAMP_W))
|
|
||||||
.on_submit(LyricEvent::LineAdvanced.into_msg(line_no))
|
|
||||||
.font(FONT_VG5000)
|
|
||||||
.into();
|
|
||||||
|
|
||||||
let text_input = TextInput::new(
|
|
||||||
&mut self.main_state,
|
|
||||||
placeholder,
|
|
||||||
&self.value,
|
|
||||||
move|new_value| LyricEvent::LyricChanged(new_value).into_msg(line_no),
|
|
||||||
)
|
|
||||||
.style(theme.active_lyric(is_focused))
|
|
||||||
.size(size)
|
|
||||||
.width(Length::Fill)
|
|
||||||
.on_submit(LyricEvent::LineAdvanced.into_msg(line_no))
|
|
||||||
.font(FONT_VG5000)
|
|
||||||
.into();
|
|
||||||
|
|
||||||
let l_bracket = Text::new("[")
|
|
||||||
.size(SMALL_SIZE)
|
|
||||||
.color(theme.reduced_text_color())
|
|
||||||
.font(FONT_VG5000)
|
|
||||||
.into();
|
|
||||||
let r_bracket = Text::new("] ")
|
|
||||||
.size(SMALL_SIZE)
|
|
||||||
.color(theme.reduced_text_color())
|
|
||||||
.font(FONT_VG5000)
|
|
||||||
.into();
|
|
||||||
|
|
||||||
Row::with_children(vec![l_bracket, timestamp_input, r_bracket, text_input])
|
|
||||||
.width(Length::Units(TOTAL_W))
|
|
||||||
.height(Length::Units(LINE_HEIGHT))
|
|
||||||
.align_items(Align::Center)
|
|
||||||
.into()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select(&mut self) {
|
pub fn select(&mut self) {
|
||||||
|
@ -238,10 +230,6 @@ impl Lyric {
|
||||||
self.main_state.move_cursor_to_end();
|
self.main_state.move_cursor_to_end();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_selected(&self) -> bool {
|
|
||||||
self.main_state.is_focused() || self.timestamp_state.is_focused()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deselect(&mut self) {
|
pub fn deselect(&mut self) {
|
||||||
self.main_state.unfocus();
|
self.main_state.unfocus();
|
||||||
self.timestamp_state.unfocus();
|
self.timestamp_state.unfocus();
|
|
@ -0,0 +1,61 @@
|
||||||
|
pub mod editing;
|
||||||
|
|
||||||
|
use rfd::FileHandle;
|
||||||
|
use rfd::AsyncFileDialog;
|
||||||
|
use crate::load_song::load_song;
|
||||||
|
use iced::Command;
|
||||||
|
use crate::app::Message;
|
||||||
|
|
||||||
|
pub use editing::Editing;
|
||||||
|
|
||||||
|
// We allow a large enum variant here because this is only used in one place, so it's okay
|
||||||
|
// to reserve the extra stack space for when FilePicker becomes Editing
|
||||||
|
#[allow(clippy::large_enum_variant)]
|
||||||
|
pub enum Model {
|
||||||
|
FilePicker { tick: usize },
|
||||||
|
Editing(Editing),
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_TICKS: usize = 900;
|
||||||
|
|
||||||
|
impl Model {
|
||||||
|
pub const DEFAULT: Self = Model::FilePicker { tick: 0 };
|
||||||
|
|
||||||
|
pub fn update(&mut self, message: Message) -> Command<Message> {
|
||||||
|
match self {
|
||||||
|
Self::FilePicker { tick } => {
|
||||||
|
match message {
|
||||||
|
Message::Tick => {
|
||||||
|
*tick = (*tick + 1) % MAX_TICKS;
|
||||||
|
Command::none()
|
||||||
|
},
|
||||||
|
Message::FileOpened(path) => {
|
||||||
|
println!("File opened! {}", path.display());
|
||||||
|
let song = load_song(&path).unwrap().format;
|
||||||
|
let (editing, cmd) = Editing::new(song);
|
||||||
|
*self = Self::Editing(editing);
|
||||||
|
cmd
|
||||||
|
},
|
||||||
|
Message::PromptForFile => {
|
||||||
|
let show_dialog = AsyncFileDialog::new()
|
||||||
|
.add_filter("Song Files", &["mp3", "flac", "ogg", "opus", "wav", "mkv"])
|
||||||
|
.set_title("Select a song")
|
||||||
|
.pick_file();
|
||||||
|
|
||||||
|
let to_message = |handle: Option<FileHandle>| if let Some(h) = handle {
|
||||||
|
Message::FileOpened(h.path().to_owned())
|
||||||
|
} else {
|
||||||
|
Message::Null
|
||||||
|
};
|
||||||
|
|
||||||
|
Command::perform(show_dialog, to_message)
|
||||||
|
},
|
||||||
|
_ => { Command::none() }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Self::Editing(model) => {
|
||||||
|
model.update(message)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue