diff --git a/src/core/song_search.rs b/src/core/song_search.rs index 87e13af..25be4d1 100644 --- a/src/core/song_search.rs +++ b/src/core/song_search.rs @@ -1,22 +1,18 @@ -use std::collections::HashMap; - -#[allow(unused)] -use crate::core::settings; use crate::core::songs::{Song, VerseName}; use itertools::Itertools; use miette::{IntoDiagnostic, Result, miette}; use nom::branch::alt; use nom::bytes::complete::{tag, take_till, take_till1, take_until}; -use nom::character::complete::{ - digit0, multispace0, newline, space0, -}; -use nom::combinator::{complete, eof, peek, rest}; -use nom::multi::{many0, many1, separated_list1}; -use nom::sequence::{delimited, pair, preceded, terminated}; +use nom::character::complete::{digit0, space0}; +use nom::combinator::rest; +use nom::multi::many1; +use nom::sequence::{delimited, pair}; use nom::{IResult, Parser}; use reqwest::header; use serde::{Deserialize, Serialize}; use serde_json::Value; +use std::collections::HashMap; +use std::fmt::Display; #[derive( Clone, @@ -56,6 +52,18 @@ pub enum Provider { LyricsCom, } +impl Display for Provider { + fn fmt( + &self, + f: &mut std::fmt::Formatter<'_>, + ) -> std::fmt::Result { + match self { + Provider::Genius { .. } => f.write_str("Genius"), + Provider::LyricsCom => f.write_str("Lyrics.com"), + } + } +} + impl From for Song { fn from(online_song: OnlineSong) -> Self { let map = if online_song.provider @@ -66,17 +74,18 @@ impl From for Song { ) .ok() } else { - None + let mut map = HashMap::new(); + map.entry(VerseName::Verse { number: 1 }) + .or_insert(online_song.lyrics); + Some(map) }; - let song = Self { + Self { title: online_song.title, author: Some(online_song.author), verse_map: map, ..Default::default() - }; - - song + } } } @@ -86,8 +95,6 @@ fn parse_genius_lyrics( ) -> Result> { let (input, chunks) = many1(pair(parse_verse_name, alt((take_until("["), rest)))) - // separated_list1(pair(newline, newline), ident) - // many1(complete(take_until("["))) .parse(lyrics) .map_err(|e| e.to_owned()) .into_diagnostic()?; @@ -102,38 +109,12 @@ fn parse_genius_lyrics( name = name.next(); } - map.entry(name).or_insert(lyric.trim().to_string()); + map.entry(name).or_insert_with(|| lyric.trim().to_string()); } Ok(map) } -#[allow(unused)] -fn ident(input: &str) -> IResult<&str, &str> { - dbg!(&input); - preceded(space0, any).parse(input) -} - -#[allow(unused)] -fn any(input: &str) -> IResult<&str, &str> { - dbg!(&input); - complete(take_until("\n\n")).parse(input) -} - -#[allow(unused)] -fn block(input: &str) -> IResult<&str, &str> { - dbg!(&input); - terminated(ident, pair(newline, newline)).parse(input) -} - -#[allow(unused)] -fn parse_verse(chunk: &str) -> IResult<&str, (VerseName, String)> { - let (input, verse_name) = parse_verse_name.parse(chunk)?; - let lyrics = input.trim().to_string(); - Ok((input, (verse_name, lyrics))) -} - -#[allow(unused)] fn parse_verse_name(line: &str) -> IResult<&str, VerseName> { let (input, (name, _, num, _, _)) = delimited( (tag("["), space0), @@ -167,7 +148,7 @@ fn parse_verse_name(line: &str) -> IResult<&str, VerseName> { } pub async fn search_genius_links( - query: impl AsRef + std::fmt::Display, + query: String, auth_token: String, ) -> Result> { // let Some(auth_token) = option_env!("GENIUS_TOKEN") else { @@ -273,7 +254,7 @@ pub async fn get_genius_lyrics( ); let lyrics = lyrics.replace("
", "\n"); song.provider = Provider::Genius { - parsable: lyrics.contains("["), + parsable: lyrics.contains('['), }; song.lyrics = lyrics; Ok(song) @@ -391,7 +372,7 @@ mod test { link: "https://genius.com/North-point-worship-death-was-arrested-lyrics".to_string(), }; let hits = search_genius_links( - "Death was arrested", + "Death was arrested".to_string(), env!("GENIUS_TOKEN").to_string(), ) .await @@ -529,7 +510,6 @@ mod test { Ok(()) } - #[allow(unreachable_code)] #[test] fn test_parse_song() -> Result<()> { let song = r#"[Verse 1] @@ -588,24 +568,6 @@ Aw man, that was good"#; let new_map = parse_genius_lyrics(&new_song)?; dbg!(map); dbg!(new_map); - panic!(); - Ok(()) - } - - #[test] - fn test_block_parsing() -> Result<()> { - let chorus = r#"[Chorus] -I'm singing, "Hallelujah" (Hallelujah) -God is able, hallelujah (Hallelujah) -God is faithful, hallelujah -Lord, I'm gonna sing (Come on now, sing it) -Oh I'm singing, "Hallelujah" (Hallelujah) -God is able, hallelujah (Hallelujah) -God is faithful, hallelujah (God is so good) -Lord, I'm gonna sing (Sing it, Dave) - -"#; - let _thing = block.parse(chorus).into_diagnostic()?; Ok(()) } } diff --git a/src/main.rs b/src/main.rs index 2f428aa..897eb13 100755 --- a/src/main.rs +++ b/src/main.rs @@ -176,6 +176,7 @@ struct App { config_handler: Option, obs_connection: String, view_mode: ViewMode, + genius_token_hidden: bool, } #[allow(dead_code)] @@ -231,6 +232,8 @@ enum Message { SetObsConnection(String), ModifiersPressed(Modifiers), ViewModeSwitch(ViewMode), + ShowGeniusToken, + SetGeniusToken(String), } #[allow(dead_code)] @@ -353,7 +356,10 @@ impl cosmic::Application for App { let items: Arc> = Arc::new(vec![]); let presenter = Presenter::with_items(items.clone()); - let song_editor = SongEditor::new(Arc::clone(&fontdb)); + let song_editor = SongEditor::new( + Arc::clone(&fontdb), + settings.genius_token.clone(), + ); // for item in items.iter() { // nav_model.insert().text(item.title()).data(item.clone()); @@ -433,6 +439,7 @@ impl cosmic::Application for App { config_handler, obs_connection: String::new(), view_mode: ViewMode::Row, + genius_token_hidden: true, }; let mut batch = vec![]; @@ -826,6 +833,20 @@ impl cosmic::Application for App { Message::SetObsUrl(self.obs_connection.clone()), ), ); + let genius_token = settings::item::builder("Token") + .control( + text_input::secure_input( + "", + self.settings + .genius_token + .clone() + .unwrap_or_default(), + Some(Message::ShowGeniusToken), + self.genius_token_hidden, + ) + .select_on_focus(true) + .on_input(Message::SetGeniusToken), + ); let settings_column = column![ icon::from_name("dialog-close") .symbolic(true) @@ -837,17 +858,25 @@ impl cosmic::Application for App { .padding(space_s) .align_right(Length::Fill) .align_top(60), - horizontal().height(space_xxl), settings::section() .title("Obs Settings") .add(obs_socket) .add(apply_button) .apply(container) .center_x(Length::Fill) - .align_top(Length::Fill) - .padding([0, space_xxxl * 2]) + .align_top(Length::Fill), + settings::section() + .title("Genius Auth Token") + .add(genius_token) + .apply(container) + .center_x(Length::Fill) + .align_top(Length::Fill), + horizontal().height(space_xxl), ] - .height(Length::Fill); + .spacing(space_s) + .height(Length::Fill) + .apply(container) + .padding(space_xxl); let settings_container = settings_column .apply(container) .style(nav_bar_style) @@ -1560,9 +1589,31 @@ impl cosmic::Application for App { Task::none() } Message::SetObsConnection(url) => { + if let Some(config_handler) = + self.config_handler.as_ref() + { + // todo!() + (); + } self.obs_connection = url; Task::none() } + Message::SetGeniusToken(token) => { + if let Some(config_handler) = + self.config_handler.as_ref() + { + self.settings.set_genius_token( + config_handler, + Some(token.clone()), + ); + self.song_editor.genius_token = Some(token); + } + Task::none() + } + Message::ShowGeniusToken => { + self.genius_token_hidden = !self.genius_token_hidden; + Task::none() + } Message::ModifiersPressed(modifiers) => { if modifiers.is_empty() { self.modifiers_pressed = None; diff --git a/src/ui/song_editor.rs b/src/ui/song_editor.rs index 71bdc72..b9db0d6 100755 --- a/src/ui/song_editor.rs +++ b/src/ui/song_editor.rs @@ -5,11 +5,13 @@ use std::io::{self}; use std::path::PathBuf; use std::sync::Arc; +use cosmic::cosmic_config::CosmicConfigEntry; use cosmic::dialog::file_chooser::FileFilter; use cosmic::dialog::file_chooser::open::Dialog; use cosmic::iced::alignment::{Horizontal, Vertical}; use cosmic::iced::core::widget::tree; use cosmic::iced::font::{Style, Weight}; +use cosmic::iced::theme::Base; use cosmic::iced::widget::scrollable::{ self as iced_scrollable, AbsoluteOffset, Direction, Scrollbar, }; @@ -18,6 +20,7 @@ use cosmic::iced::{ Background as ContainerBackground, Border, Color, Length, Padding, Shadow, Vector, color, task, }; +use cosmic::widget::button::Catalog; use cosmic::widget::color_picker::ColorPickerUpdate; use cosmic::widget::grid::{self}; use cosmic::widget::space::{self, horizontal}; @@ -36,7 +39,9 @@ use itertools::Itertools; use tracing::{debug, error}; use crate::core::service_items::ServiceTrait; +use crate::core::settings; use crate::core::slide::{Slide, TextAlignment}; +use crate::core::song_search::{self, OnlineSong}; use crate::core::songs::{Song, VerseName}; use crate::ui::presenter::slide_view; use crate::ui::slide_editor::SlideEditor; @@ -89,6 +94,9 @@ pub struct SongEditor { shadow_color_model: ColorPickerModel, shadow_tools_open: bool, importing: bool, + search_input: String, + search_results: Option>, + pub genius_token: Option, } #[allow(clippy::large_enum_variant)] @@ -143,6 +151,9 @@ pub enum Message { UpdateShadowOffsetX(usize), UpdateShadowOffsetY(usize), ChangeFontWeight, + SearchUpdate(String), + SearchSong(String), + UpdateSearchResults(Result, String>), } #[derive(Debug, Clone)] @@ -182,7 +193,10 @@ impl Display for Face { #[allow(clippy::cast_possible_truncation)] impl SongEditor { - pub fn new(font_db: Arc) -> Self { + pub fn new( + font_db: Arc, + genius_token: Option, + ) -> Self { let fonts = font_dir(); debug!(?fonts); let fonts: Vec = font_db @@ -337,6 +351,9 @@ impl SongEditor { ), shadow_tools_open: false, importing: false, + search_input: String::new(), + search_results: None, + genius_token, } } @@ -923,6 +940,28 @@ impl SongEditor { Message::OpenShadowTools => { self.shadow_tools_open = !self.shadow_tools_open; } + Message::SearchUpdate(query) => { + self.search_input = query; + } + Message::SearchSong(query) => { + return Action::Task(Task::perform( + song_search::search_genius_links( + query, + self.genius_token.clone().unwrap_or_default(), + ), + |res| { + Message::UpdateSearchResults( + res.map_err(|e| e.to_string()), + ) + }, + )); + } + Message::UpdateSearchResults(result) => match result { + Ok(songs) => self.search_results = Some(songs), + Err(e) => { + error!("Cannot find songs: {e}"); + } + }, Message::None => (), } Action::None @@ -1852,7 +1891,73 @@ impl SongEditor { } pub fn import_view(&self) -> Element { - todo!("need to add an import view") + let search_bar = + cosmic::widget::search_input("", &self.search_input) + .on_input(Message::SearchUpdate) + .on_submit(Message::SearchSong) + .label("Search for Song"); + let submit_button = + button::icon(icon::from_name("document-send-symbolic")) + .on_press(Message::SearchSong( + self.search_input.clone(), + )); + + let search_results = + self.search_results.as_ref().map_or_else( + || space::horizontal().apply(container), + |songs| { + let songs: Vec> = songs + .iter() + .map(|song| { + let title = text::heading(&song.title); + let author = text::body(&song.author); + let link = text::body(&song.link); + let provider = + text::body(song.provider.to_string()) + .apply(container) + .style(|t| { + container::Style::default() + .color( + t.cosmic() + .palette + .accent_green, + ) + .border( + Border::default() + .rounded( + t.cosmic() + .radius_s( + ), + ), + ) + }) + .padding( + theme::spacing().space_s, + ); + row![ + column![title, author, link].spacing( + theme::spacing().space_xxs + ), + space::horizontal(), + provider + ] + .padding(theme::spacing().space_s) + .apply(container) + .class(theme::Container::Card) + .into() + }) + .collect(); + + column::with_children(songs) + .spacing(theme::spacing().space_s) + .apply(container) + }, + ); + + let search_row = + row![search_bar, space::horizontal(), submit_button]; + + column![search_row, search_results].into() } pub const fn editing(&self) -> bool { @@ -2045,7 +2150,7 @@ impl Default for SongEditor { fn default() -> Self { let mut fontdb = fontdb::Database::new(); fontdb.load_system_fonts(); - Self::new(Arc::new(fontdb)) + Self::new(Arc::new(fontdb), None) } }