diff --git a/src/app.rs b/src/app.rs index 1f23b33..90135f1 100644 --- a/src/app.rs +++ b/src/app.rs @@ -29,6 +29,11 @@ pub struct Model { /// Whether we're filtering by duets filter_duets: bool, + + query_placeholder: String, + query_placeholder_len: usize, + + autotyper: Option, } const SCROLL_THRESHOLD: usize = 50; @@ -49,9 +54,12 @@ pub enum Msg { /// The user scrolled the song list Scroll, + + /// Type stuff in the search input placeholder + Autotyper, } -pub fn init(_url: Url, _orders: &mut impl Orders) -> Model { +pub fn init(_url: Url, orders: &mut impl Orders) -> Model { let mut songs: Vec = serde_json::from_str(include_str!("../static/songs.json")).expect("parse songs"); songs.shuffle(&mut thread_rng()); @@ -66,6 +74,9 @@ pub fn init(_url: Url, _orders: &mut impl Orders) -> Model { shown_songs: INITIAL_ELEM_COUNT, filter_video: false, filter_duets: false, + query_placeholder: String::from("Sök"), + query_placeholder_len: 0, + autotyper: Some(orders.perform_cmd_with_handle(timeout(500, || Msg::Autotyper))), } } @@ -81,7 +92,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { if model.query.is_empty() { model.filter_duets = false; model.filter_video = false; - update(Msg::Shuffle, model, orders) + update(Msg::Shuffle, model, orders); } else { let query = ParsedQuery::parse(&model.query); model.filter_duets = query.duet == Some(true); @@ -121,6 +132,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { scroll_to_top(); model.query.clear(); model.songs.shuffle(&mut thread_rng()); + autotype_song(model, orders); } Msg::Scroll => { let (scroll, max_scroll) = match get_scroll() { @@ -141,6 +153,20 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { orders.perform_cmd(timeout(32 /* ms */, || Msg::Scroll)); } } + Msg::Autotyper => { + model.query_placeholder_len += 1; + while !model + .query_placeholder + .is_char_boundary(model.query_placeholder_len) + { + model.query_placeholder_len += 1; + } + + if model.query_placeholder_len < model.query_placeholder.len() { + model.autotyper = + Some(orders.perform_cmd_with_handle(timeout(80, || Msg::Autotyper))); + } + } } } @@ -206,12 +232,12 @@ pub fn view(model: &Model) -> Vec> { div![ C![C.song_search_bar], input![ + C![C.song_search_field], input_ev(Ev::Input, Msg::Search), attrs! { - At::Placeholder => "Sök", + At::Placeholder => &model.query_placeholder[..model.query_placeholder_len], At::Value => model.query, }, - C![C.song_search_field], ], button![ C![C.song_sort_button, C.tooltip], @@ -250,6 +276,13 @@ pub fn view(model: &Model) -> Vec> { ] } +pub fn autotype_song(model: &mut Model, orders: &mut impl Orders) { + let (_, song) = &model.songs[0]; + model.query_placeholder = ParsedQuery::random(song, &mut thread_rng()).to_string(); + model.query_placeholder_len = 0; + model.autotyper = Some(orders.perform_cmd_with_handle(timeout(100, || Msg::Autotyper))); +} + const SONG_LIST_ID: &str = "song_list"; fn get_song_list_element() -> anyhow::Result { diff --git a/src/query.rs b/src/query.rs index ddff95f..742c911 100644 --- a/src/query.rs +++ b/src/query.rs @@ -1,3 +1,6 @@ +use crate::song::Song; +use rand::seq::SliceRandom; +use rand::Rng; use std::borrow::Cow; use std::fmt::{self, Display, Formatter}; use std::ops::Not; @@ -53,6 +56,56 @@ impl<'a> ParsedQuery<'a> { parsed } + + /// Generate a parsed query with a few random fields matching a song + pub fn random(song: &'a Song, rng: &mut R) -> Self { + let until_space = + |s: &'a str| -> &'a str { s.trim().split_whitespace().next().unwrap_or("") }; + + let mut primary_fields: [&dyn Fn(Self) -> Self; 4] = [ + &|query| Self { + plain: Some(Cow::Borrowed(&song.title)), + ..query + }, + &|query| Self { + plain: Some(Cow::Borrowed(&song.artist)), + ..query + }, + &|query| Self { + title: Some(until_space(&song.title)), + ..query + }, + &|query| Self { + artist: Some(until_space(&song.artist)), + ..query + }, + ]; + + let mut extra_fields: [&dyn Fn(Self) -> Self; 3] = [ + &|query| Self { + language: song.language.as_deref().map(until_space), + ..query + }, + &|query| Self { + genre: song.genre.as_deref().map(until_space), + ..query + }, + &|query| Self { + year: song.year.as_deref().map(until_space), + ..query + }, + ]; + + primary_fields.shuffle(rng); + extra_fields.shuffle(rng); + + let primary_fields = primary_fields.into_iter().take(1); + let extra_fields = extra_fields.into_iter().take(rng.gen_range(0..2)); + + primary_fields + .chain(extra_fields) + .fold(Self::default(), |query, field| field(query)) + } } fn parse_bool(s: &str) -> Option { diff --git a/static/styles/common.scss b/static/styles/common.scss index 1cc5ab3..eb1ff2d 100644 --- a/static/styles/common.scss +++ b/static/styles/common.scss @@ -43,7 +43,7 @@ body { .song_search_bar { position: relative; max-width: 38em; - width: 80%; + width: 95%; margin: auto; margin-top: 1em; }