From 1ad30399cf4c09411b8cbd690cfabb6001045e77 Mon Sep 17 00:00:00 2001 From: Joakim Hulthe Date: Thu, 30 Dec 2021 23:29:56 +0100 Subject: [PATCH] Initial Commit 2 --- Cargo.lock | 55 ++++++++++++++++++--- Cargo.toml | 1 + index.html | 4 +- src/app.rs | 101 ++++++++++++++++++++++++++++---------- src/fuzzy.rs | 67 +++++++++++++++++++++++++ src/lib.rs | 1 + src/song.rs | 17 +++++++ static/images/video.svg | 21 ++++++++ static/styles/common.scss | 37 +++++++++++--- 9 files changed, 259 insertions(+), 45 deletions(-) create mode 100644 src/fuzzy.rs create mode 100644 static/images/video.svg diff --git a/Cargo.lock b/Cargo.lock index b727826..7a9a8a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -392,12 +392,24 @@ checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" dependencies = [ "getrandom 0.1.16", "libc", - "rand_chacha", - "rand_core", - "rand_hc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc 0.2.0", "rand_pcg", ] +[[package]] +name = "rand" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.3", + "rand_hc 0.3.1", +] + [[package]] name = "rand_chacha" version = "0.2.2" @@ -405,7 +417,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.3", ] [[package]] @@ -417,13 +439,31 @@ dependencies = [ "getrandom 0.1.16", ] +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom 0.2.3", +] + [[package]] name = "rand_hc" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" dependencies = [ - "rand_core", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_hc" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" +dependencies = [ + "rand_core 0.6.3", ] [[package]] @@ -432,7 +472,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" dependencies = [ - "rand_core", + "rand_core 0.5.1", ] [[package]] @@ -483,7 +523,7 @@ dependencies = [ "indexmap", "js-sys", "pulldown-cmark", - "rand", + "rand 0.7.3", "serde", "serde_json", "uuid", @@ -551,6 +591,7 @@ version = "0.1.0" dependencies = [ "anyhow", "css_typegen", + "rand 0.8.4", "seed", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index a0a4394..49e5b60 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ seed = "0.8.0" serde = { version = "1", features = ['derive'] } serde_json = "1" anyhow = "*" +rand = "0.8.4" [dependencies.css_typegen] git = "https://github.com/hulthe/css_typegen.git" diff --git a/index.html b/index.html index 8216416..c3f5fc5 100755 --- a/index.html +++ b/index.html @@ -11,9 +11,7 @@ - @@ -34,6 +32,6 @@ -
+
diff --git a/src/app.rs b/src/app.rs index 899c635..ea9c250 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,13 +1,20 @@ use crate::css::C; use crate::song::Song; use anyhow::anyhow; +use rand::seq::SliceRandom; +use rand::thread_rng; +use seed::app::cmds::timeout; use seed::browser::util::document; use seed::prelude::*; -use seed::{attrs, br, button, div, empty, error, img, input, log, p, span, C, IF}; +use seed::{attrs, button, div, empty, error, img, input, p, span, C, IF}; +use std::cmp::Reverse; pub struct Model { songs: Vec, + filtered_songs: Vec, show_elements: usize, + filter_video: bool, + filter_duets: bool, } const SCROLL_THRESHOLD: usize = 50; @@ -16,22 +23,41 @@ const INITIAL_ELEM_COUNT: usize = 100; //#[derive(Clone, Debug)] pub enum Msg { Search(String), + ToggleVideo, + ToggleDuets, + Shuffle, Scroll, } pub fn init(_url: Url, _orders: &mut impl Orders) -> Model { + let songs: Vec = + serde_json::from_str(include_str!("../static/songs.json")).expect("parse songs"); + let mut filtered_songs: Vec = (0..songs.len()).collect(); + filtered_songs.shuffle(&mut thread_rng()); + Model { - songs: serde_json::from_str(include_str!("../static/songs.json")) - .expect("failed to parsed songs"), + songs, + filtered_songs, show_elements: INITIAL_ELEM_COUNT, + filter_video: false, + filter_duets: false, } } pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { match msg { + Msg::Search(query) if query.is_empty() => update(Msg::Shuffle, model, orders), Msg::Search(query) => { - log!("search query"); + model.filtered_songs.sort_by_cached_key(|&i| { + let song = &model.songs[i]; + let top_score = Reverse(song.fuzzy_compare(&query)); + + (top_score, &song.title, &song.artist, &song.song_hash) + }); } + Msg::ToggleVideo => model.filter_video = !model.filter_video, + Msg::ToggleDuets => model.filter_duets = !model.filter_duets, + Msg::Shuffle => model.filtered_songs.shuffle(&mut thread_rng()), Msg::Scroll => { let (scroll, max_scroll) = match get_scroll() { Ok(v) => v, @@ -46,10 +72,9 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { // when there are fewer elements than this below the scroll viewport, add more const ELEMENT_HEIGHT: i32 = 48; - log!("scroll={}, height={}", scroll, max_scroll); if scroll_left < ELEMENT_HEIGHT * SCROLL_THRESHOLD as i32 { - log!("showing more items"); model.show_elements += 1; + orders.perform_cmd(timeout(32, || Msg::Scroll)); } } } @@ -87,6 +112,13 @@ pub fn view(model: &Model) -> Vec> { ], _ => empty![], }, + IF![song.video.is_some() => div![ + C![C.video_icon, C.tooltip], + span![ + C![C.tooltiptext], + "Musikvideo", + ], + ]], ], ] }; @@ -94,33 +126,48 @@ pub fn view(model: &Model) -> Vec> { vec![ div![ C![C.song_search_bar], - div![ - input![ - attrs! { - At::Placeholder => "Search", - }, - C![C.song_search_field, C.tooltip], - ], - button![ - C![C.song_sort_button, C.tooltip], - span![C![C.tooltiptext], "awawawaw", br![], "aawawaw?"], - ], - button![ - C![C.song_sort_button, C.tooltip], - span![C![C.tooltiptext], "awawawaw"], - ], - button![ - C![C.song_sort_button, C.song_sort_button_right, C.tooltip], - span![C![C.tooltiptext], "awawawaw"], - ], + input![ + input_ev(Ev::Input, Msg::Search), + attrs! { + At::Placeholder => "Search", + }, + C![C.song_search_field], + ], + button![ + C![C.song_sort_button, C.tooltip], + IF![model.filter_duets => C![C.song_sort_button_selected]], + ev(Ev::Click, |_| Msg::ToggleDuets), + span![C![C.tooltiptext], "Endast Duetter"], + "D", + ], + button![ + C![C.song_sort_button, C.tooltip], + IF![model.filter_video => C![C.song_sort_button_selected]], + ev(Ev::Click, |_| Msg::ToggleVideo), + span![C![C.tooltiptext], "Endast med Video"], + "V", + ], + button![ + C![C.song_sort_button, C.song_sort_button_right, C.tooltip], + IF![model.filter_video => C![C.song_sort_button_selected]], + ev(Ev::Click, |_| Msg::Shuffle), + span![C![C.tooltiptext], "Blanda låtar"], + "🔀", ], ], div![ C![C.song_list], attrs! {At::Id => SONG_LIST_ID}, ev(Ev::Scroll, |_| Msg::Scroll), - model.songs.iter().take(model.show_elements).map(song_card), - IF![model.show_elements < model.songs.len() => div![C![C.center, C.penguin]]], + model + .filtered_songs + .iter() + .map(|&i| &model.songs[i]) + .filter(|song| !model.filter_video || song.video.is_some()) + .filter(|song| !model.filter_duets || song.duet().is_some()) + .map(song_card) + .take(model.show_elements), + //IF![model.show_elements < model.songs.len() => div![C![C.center, C.penguin]]], ], ] } diff --git a/src/fuzzy.rs b/src/fuzzy.rs new file mode 100644 index 0000000..3d22889 --- /dev/null +++ b/src/fuzzy.rs @@ -0,0 +1,67 @@ +use std::cmp::{Ord, Ordering, PartialOrd}; + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct FuzzyScore { + pub score: i32, + pub matches: Vec, +} + +#[derive(Copy, Clone, Default, Debug, Eq, PartialEq)] +pub struct FuzzyCharMatch { + pub base_str_index: usize, + pub search_str_index: usize, +} + +impl PartialOrd for FuzzyScore { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for FuzzyScore { + fn cmp(&self, other: &Self) -> Ordering { + self.score.cmp(&other.score) + } +} + +/// Compare a base string to a user-input search +/// +/// Returns a tuple of the match score, as well as the indices of every char in `search` which maps +/// to an index in `base` +pub fn compare(base: B, search: S) -> FuzzyScore +where + B: Iterator + Clone, + S: IntoIterator, +{ + let mut base = base.into_iter().enumerate(); + + // How alike the search string is to self.name + //let mut score = -(search.len() as i32); + let mut score = 0; + + // Vector of which char index in s maps to which char index in self.name + let mut matches = vec![]; + + for (i, sc) in search.into_iter().enumerate() { + let sc = sc.to_ascii_lowercase(); + let mut add = 3; + let mut base_tmp = base.clone(); + while let Some((j, bc)) = base_tmp.next() { + let bc = bc.to_ascii_lowercase(); + if bc == sc { + matches.push(FuzzyCharMatch { + search_str_index: i, + base_str_index: j, + }); + + score += add; + base = base_tmp; + break; + } else { + add = 2; + } + } + } + + FuzzyScore { score, matches } +} diff --git a/src/lib.rs b/src/lib.rs index eca00f7..0adfc8b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ mod app; mod css; +mod fuzzy; mod song; use seed::prelude::wasm_bindgen; diff --git a/src/song.rs b/src/song.rs index 683cca2..d2c6b54 100644 --- a/src/song.rs +++ b/src/song.rs @@ -1,4 +1,6 @@ +use crate::fuzzy::{self, FuzzyScore}; use serde::Deserialize; +use std::cmp::max; #[derive(Deserialize, Debug, Clone, Default)] pub struct Song { @@ -16,3 +18,18 @@ pub struct Song { #[serde(rename = "duetsingerp2")] pub duet_singer_2: Option, } + +impl Song { + pub fn duet(&self) -> Option<(&str, &str)> { + self.duet_singer_1 + .as_deref() + .zip(self.duet_singer_2.as_deref()) + } + + pub fn fuzzy_compare(&self, query: &str) -> FuzzyScore { + let title_score = fuzzy::compare(self.title.chars(), query.chars()); + let artist_score = fuzzy::compare(self.artist.chars(), query.chars()); + + max(title_score, artist_score) + } +} diff --git a/static/images/video.svg b/static/images/video.svg new file mode 100644 index 0000000..8427b41 --- /dev/null +++ b/static/images/video.svg @@ -0,0 +1,21 @@ + + + + + + + + + diff --git a/static/styles/common.scss b/static/styles/common.scss index 824abc2..08874a7 100644 --- a/static/styles/common.scss +++ b/static/styles/common.scss @@ -15,7 +15,7 @@ body { background-color: #0c2738; color: #ffffff; height: 100%; - font-family: Open Sans,serif; + font-family: Ubuntu,serif; } .nobr { @@ -39,6 +39,9 @@ body { .song_search_bar { position: relative; padding: 1em 1em .5em; + max-width: 35em; + width: 80%; + margin: auto; } .song_search_field { @@ -63,6 +66,7 @@ body { background-color: #427493; transition: 0.4s; color: #ffffff; + font-weight: 900; } .song_sort_button:hover { @@ -76,7 +80,7 @@ body { } .song_sort_button_selected { - color: #ffff00; + color: #00ff00; transition: 0.1s; } @@ -99,18 +103,23 @@ body { flex-direction: row; border-radius: 1em; background: black; + max-width: 40em; + margin: auto; margin-bottom: 1em; animation: song_item_enter 1s 1; + box-shadow: #09babe 1px 1px; } @keyframes song_item_enter { from { - margin-left: -16em; - margin-right: 16em; + opacity: 0; + /*margin-left: -16em; + margin-right: 16em;*/ } to { - margin-left: 0; - margin-right: 0; + opacity: 1; + /*margin-left: 0; + margin-right: 0;*/ } } @@ -129,8 +138,9 @@ body { } .song_item_cover { - height: 5em; width: 5em; + height: auto; + object-fit: cover; border-top-left-radius: 1em; border-bottom-left-radius: 1em; } @@ -153,7 +163,10 @@ body { flex-grow: 0; flex-shrink: 1; padding-top: 1.5em; - padding-right: 1.5em; +} + +.song_gizmos div { + margin-right: 1em; } .duet_icon { @@ -164,6 +177,14 @@ body { height: 2em; } +.video_icon { + background-image: url("/images/video.svg"); + background-size: contain; + background-repeat: no-repeat; + width: 2em; + height: 2em; +} + .hidden { visibility: hidden; }