Add search suggestions
This commit is contained in:
41
src/app.rs
41
src/app.rs
@ -29,6 +29,11 @@ pub struct Model {
|
|||||||
|
|
||||||
/// Whether we're filtering by duets
|
/// Whether we're filtering by duets
|
||||||
filter_duets: bool,
|
filter_duets: bool,
|
||||||
|
|
||||||
|
query_placeholder: String,
|
||||||
|
query_placeholder_len: usize,
|
||||||
|
|
||||||
|
autotyper: Option<CmdHandle>,
|
||||||
}
|
}
|
||||||
|
|
||||||
const SCROLL_THRESHOLD: usize = 50;
|
const SCROLL_THRESHOLD: usize = 50;
|
||||||
@ -49,9 +54,12 @@ pub enum Msg {
|
|||||||
|
|
||||||
/// The user scrolled the song list
|
/// The user scrolled the song list
|
||||||
Scroll,
|
Scroll,
|
||||||
|
|
||||||
|
/// Type stuff in the search input placeholder
|
||||||
|
Autotyper,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(_url: Url, _orders: &mut impl Orders<Msg>) -> Model {
|
pub fn init(_url: Url, orders: &mut impl Orders<Msg>) -> Model {
|
||||||
let mut songs: Vec<Song> =
|
let mut songs: Vec<Song> =
|
||||||
serde_json::from_str(include_str!("../static/songs.json")).expect("parse songs");
|
serde_json::from_str(include_str!("../static/songs.json")).expect("parse songs");
|
||||||
songs.shuffle(&mut thread_rng());
|
songs.shuffle(&mut thread_rng());
|
||||||
@ -66,6 +74,9 @@ pub fn init(_url: Url, _orders: &mut impl Orders<Msg>) -> Model {
|
|||||||
shown_songs: INITIAL_ELEM_COUNT,
|
shown_songs: INITIAL_ELEM_COUNT,
|
||||||
filter_video: false,
|
filter_video: false,
|
||||||
filter_duets: 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<Msg>) {
|
|||||||
if model.query.is_empty() {
|
if model.query.is_empty() {
|
||||||
model.filter_duets = false;
|
model.filter_duets = false;
|
||||||
model.filter_video = false;
|
model.filter_video = false;
|
||||||
update(Msg::Shuffle, model, orders)
|
update(Msg::Shuffle, model, orders);
|
||||||
} else {
|
} else {
|
||||||
let query = ParsedQuery::parse(&model.query);
|
let query = ParsedQuery::parse(&model.query);
|
||||||
model.filter_duets = query.duet == Some(true);
|
model.filter_duets = query.duet == Some(true);
|
||||||
@ -121,6 +132,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
scroll_to_top();
|
scroll_to_top();
|
||||||
model.query.clear();
|
model.query.clear();
|
||||||
model.songs.shuffle(&mut thread_rng());
|
model.songs.shuffle(&mut thread_rng());
|
||||||
|
autotype_song(model, orders);
|
||||||
}
|
}
|
||||||
Msg::Scroll => {
|
Msg::Scroll => {
|
||||||
let (scroll, max_scroll) = match get_scroll() {
|
let (scroll, max_scroll) = match get_scroll() {
|
||||||
@ -141,6 +153,20 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
orders.perform_cmd(timeout(32 /* ms */, || Msg::Scroll));
|
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<Node<Msg>> {
|
|||||||
div![
|
div![
|
||||||
C![C.song_search_bar],
|
C![C.song_search_bar],
|
||||||
input![
|
input![
|
||||||
|
C![C.song_search_field],
|
||||||
input_ev(Ev::Input, Msg::Search),
|
input_ev(Ev::Input, Msg::Search),
|
||||||
attrs! {
|
attrs! {
|
||||||
At::Placeholder => "Sök",
|
At::Placeholder => &model.query_placeholder[..model.query_placeholder_len],
|
||||||
At::Value => model.query,
|
At::Value => model.query,
|
||||||
},
|
},
|
||||||
C![C.song_search_field],
|
|
||||||
],
|
],
|
||||||
button![
|
button![
|
||||||
C![C.song_sort_button, C.tooltip],
|
C![C.song_sort_button, C.tooltip],
|
||||||
@ -250,6 +276,13 @@ pub fn view(model: &Model) -> Vec<Node<Msg>> {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn autotype_song(model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||||
|
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";
|
const SONG_LIST_ID: &str = "song_list";
|
||||||
|
|
||||||
fn get_song_list_element() -> anyhow::Result<Element> {
|
fn get_song_list_element() -> anyhow::Result<Element> {
|
||||||
|
|||||||
53
src/query.rs
53
src/query.rs
@ -1,3 +1,6 @@
|
|||||||
|
use crate::song::Song;
|
||||||
|
use rand::seq::SliceRandom;
|
||||||
|
use rand::Rng;
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::fmt::{self, Display, Formatter};
|
use std::fmt::{self, Display, Formatter};
|
||||||
use std::ops::Not;
|
use std::ops::Not;
|
||||||
@ -53,6 +56,56 @@ impl<'a> ParsedQuery<'a> {
|
|||||||
|
|
||||||
parsed
|
parsed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generate a parsed query with a few random fields matching a song
|
||||||
|
pub fn random<R: Rng>(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<bool> {
|
fn parse_bool(s: &str) -> Option<bool> {
|
||||||
|
|||||||
@ -43,7 +43,7 @@ body {
|
|||||||
.song_search_bar {
|
.song_search_bar {
|
||||||
position: relative;
|
position: relative;
|
||||||
max-width: 38em;
|
max-width: 38em;
|
||||||
width: 80%;
|
width: 95%;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user