Improve search
This commit is contained in:
115
src/app.rs
115
src/app.rs
@ -1,4 +1,5 @@
|
||||
use crate::css::C;
|
||||
use crate::query::ParsedQuery;
|
||||
use crate::song::Song;
|
||||
use anyhow::anyhow;
|
||||
use rand::seq::SliceRandom;
|
||||
@ -8,11 +9,14 @@ use seed::browser::util::document;
|
||||
use seed::prelude::*;
|
||||
use seed::{attrs, button, div, empty, error, img, input, p, span, C, IF};
|
||||
use std::cmp::Reverse;
|
||||
use web_sys::Element;
|
||||
|
||||
pub struct Model {
|
||||
songs: Vec<Song>,
|
||||
query: String,
|
||||
filtered_songs: Vec<usize>,
|
||||
show_elements: usize,
|
||||
hidden_songs: usize,
|
||||
shown_songs: usize,
|
||||
filter_video: bool,
|
||||
filter_duets: bool,
|
||||
}
|
||||
@ -20,7 +24,6 @@ pub struct Model {
|
||||
const SCROLL_THRESHOLD: usize = 50;
|
||||
const INITIAL_ELEM_COUNT: usize = 100;
|
||||
|
||||
//#[derive(Clone, Debug)]
|
||||
pub enum Msg {
|
||||
Search(String),
|
||||
ToggleVideo,
|
||||
@ -37,8 +40,10 @@ pub fn init(_url: Url, _orders: &mut impl Orders<Msg>) -> Model {
|
||||
|
||||
Model {
|
||||
songs,
|
||||
query: String::new(),
|
||||
filtered_songs,
|
||||
show_elements: INITIAL_ELEM_COUNT,
|
||||
hidden_songs: 0,
|
||||
shown_songs: INITIAL_ELEM_COUNT,
|
||||
filter_video: false,
|
||||
filter_duets: false,
|
||||
}
|
||||
@ -46,18 +51,57 @@ pub fn init(_url: Url, _orders: &mut impl Orders<Msg>) -> Model {
|
||||
|
||||
pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
match msg {
|
||||
Msg::Search(query) if query.is_empty() => update(Msg::Shuffle, model, orders),
|
||||
Msg::Search(query) => {
|
||||
model.filtered_songs.sort_by_cached_key(|&i| {
|
||||
let song = &model.songs[i];
|
||||
let top_score = Reverse(song.fuzzy_compare(&query));
|
||||
model.hidden_songs = 0;
|
||||
model.shown_songs = INITIAL_ELEM_COUNT;
|
||||
scroll_to_top();
|
||||
|
||||
(top_score, &song.title, &song.artist, &song.song_hash)
|
||||
});
|
||||
model.query = query;
|
||||
|
||||
if model.query.is_empty() {
|
||||
model.filter_duets = false;
|
||||
model.filter_video = false;
|
||||
update(Msg::Shuffle, model, orders)
|
||||
} else {
|
||||
let query = ParsedQuery::parse(&model.query);
|
||||
model.filtered_songs.sort_by_cached_key(|&i| {
|
||||
let song = &model.songs[i];
|
||||
let score = song.fuzzy_compare(&query);
|
||||
if score < Default::default() {
|
||||
model.hidden_songs += 1;
|
||||
}
|
||||
|
||||
let top_score = Reverse(score);
|
||||
|
||||
(top_score, &song.title, &song.artist, &song.song_hash)
|
||||
});
|
||||
model.filter_duets = query.duet == Some(true);
|
||||
model.filter_video = query.video == Some(true);
|
||||
}
|
||||
}
|
||||
Msg::ToggleVideo => {
|
||||
let mut query = ParsedQuery::parse(&model.query);
|
||||
query.video = match query.video {
|
||||
Some(true) => None,
|
||||
None | Some(false) => Some(true),
|
||||
};
|
||||
update(Msg::Search(query.to_string()), model, orders);
|
||||
}
|
||||
Msg::ToggleDuets => {
|
||||
let mut query = ParsedQuery::parse(&model.query);
|
||||
query.duet = match query.duet {
|
||||
Some(true) => None,
|
||||
None | Some(false) => Some(true),
|
||||
};
|
||||
update(Msg::Search(query.to_string()), model, orders);
|
||||
}
|
||||
Msg::Shuffle => {
|
||||
model.hidden_songs = 0;
|
||||
model.shown_songs = INITIAL_ELEM_COUNT;
|
||||
scroll_to_top();
|
||||
model.query.clear();
|
||||
model.filtered_songs.shuffle(&mut thread_rng());
|
||||
}
|
||||
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,
|
||||
@ -73,8 +117,8 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
const ELEMENT_HEIGHT: i32 = 48;
|
||||
|
||||
if scroll_left < ELEMENT_HEIGHT * SCROLL_THRESHOLD as i32 {
|
||||
model.show_elements += 1;
|
||||
orders.perform_cmd(timeout(32, || Msg::Scroll));
|
||||
model.shown_songs += 1;
|
||||
orders.perform_cmd(timeout(32 /* ms */, || Msg::Scroll));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -100,7 +144,7 @@ pub fn view(model: &Model) -> Vec<Node<Msg>> {
|
||||
C![C.song_gizmos],
|
||||
match (&song.duet_singer_1, &song.duet_singer_2) {
|
||||
(Some(p1), Some(p2)) => div![
|
||||
C![C.duet_icon, C.tooltip],
|
||||
C![C.gizmo, C.duet_icon, C.tooltip],
|
||||
span![
|
||||
C![C.tooltiptext],
|
||||
"Duet",
|
||||
@ -113,12 +157,26 @@ pub fn view(model: &Model) -> Vec<Node<Msg>> {
|
||||
_ => empty![],
|
||||
},
|
||||
IF![song.video.is_some() => div![
|
||||
C![C.video_icon, C.tooltip],
|
||||
C![C.gizmo, C.video_icon, C.tooltip],
|
||||
span![
|
||||
C![C.tooltiptext],
|
||||
"Musikvideo",
|
||||
],
|
||||
]],
|
||||
match &song.language {
|
||||
Some(language) => div![
|
||||
C![C.gizmo, C.flag_icon, C.tooltip],
|
||||
span![C![C.tooltiptext], language],
|
||||
],
|
||||
None => empty![],
|
||||
},
|
||||
match &song.genre {
|
||||
Some(genre) => div![
|
||||
C![C.gizmo, C.note_icon, C.tooltip],
|
||||
span![C![C.tooltiptext], genre],
|
||||
],
|
||||
None => empty![],
|
||||
},
|
||||
],
|
||||
]
|
||||
};
|
||||
@ -129,7 +187,8 @@ pub fn view(model: &Model) -> Vec<Node<Msg>> {
|
||||
input![
|
||||
input_ev(Ev::Input, Msg::Search),
|
||||
attrs! {
|
||||
At::Placeholder => "Search",
|
||||
At::Placeholder => "Sök",
|
||||
At::Value => model.query,
|
||||
},
|
||||
C![C.song_search_field],
|
||||
],
|
||||
@ -163,21 +222,29 @@ pub fn view(model: &Model) -> Vec<Node<Msg>> {
|
||||
.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]]],
|
||||
.take(model.filtered_songs.len() - model.hidden_songs)
|
||||
.take(model.shown_songs),
|
||||
],
|
||||
]
|
||||
}
|
||||
|
||||
const SONG_LIST_ID: &str = "song_list";
|
||||
|
||||
fn get_scroll() -> anyhow::Result<(i32, i32)> {
|
||||
let list = document()
|
||||
fn get_song_list_element() -> anyhow::Result<Element> {
|
||||
document()
|
||||
.get_element_by_id(SONG_LIST_ID)
|
||||
.ok_or(anyhow!("Failed to access song list element"))?;
|
||||
.ok_or(anyhow!("Failed to access song list element"))
|
||||
}
|
||||
|
||||
fn scroll_to_top() {
|
||||
if let Ok(elem) = get_song_list_element() {
|
||||
elem.scroll_to_with_x_and_y(0.0, 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_scroll() -> anyhow::Result<(i32, i32)> {
|
||||
let list = get_song_list_element()?;
|
||||
let scroll = list.scroll_top();
|
||||
let height = list.client_height();
|
||||
let max = (list.scroll_height() - height).max(0);
|
||||
|
||||
44
src/fuzzy.rs
44
src/fuzzy.rs
@ -1,28 +1,4 @@
|
||||
use std::cmp::{Ord, Ordering, PartialOrd};
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct FuzzyScore {
|
||||
pub score: i32,
|
||||
pub matches: Vec<FuzzyCharMatch>,
|
||||
}
|
||||
|
||||
#[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<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for FuzzyScore {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.score.cmp(&other.score)
|
||||
}
|
||||
}
|
||||
pub type FuzzyScore = i32;
|
||||
|
||||
/// Compare a base string to a user-input search
|
||||
///
|
||||
@ -39,21 +15,13 @@ where
|
||||
//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() {
|
||||
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() {
|
||||
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;
|
||||
@ -63,5 +31,9 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
FuzzyScore { score, matches }
|
||||
score
|
||||
}
|
||||
|
||||
pub fn max_score(query: &str) -> FuzzyScore {
|
||||
compare(query.chars(), query.chars())
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
mod app;
|
||||
mod css;
|
||||
mod fuzzy;
|
||||
mod query;
|
||||
mod song;
|
||||
|
||||
use seed::prelude::wasm_bindgen;
|
||||
|
||||
117
src/query.rs
Normal file
117
src/query.rs
Normal file
@ -0,0 +1,117 @@
|
||||
use std::borrow::Cow;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::ops::Not;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ParsedQuery<'a> {
|
||||
/// Unspecified query.
|
||||
pub plain: Option<Cow<'a, str>>,
|
||||
|
||||
/// Query a specific title
|
||||
pub title: Option<&'a str>,
|
||||
|
||||
/// Query a specific artist
|
||||
pub artist: Option<&'a str>,
|
||||
|
||||
/// Whether the song is a duet
|
||||
pub duet: Option<bool>,
|
||||
|
||||
/// Whether the song has a video
|
||||
pub video: Option<bool>,
|
||||
|
||||
/// Query a specific language
|
||||
pub language: Option<&'a str>,
|
||||
|
||||
/// Query a specific genre
|
||||
pub genre: Option<&'a str>,
|
||||
|
||||
/// Query from a specifc year
|
||||
pub year: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> ParsedQuery<'a> {
|
||||
pub fn parse(s: &'a str) -> Self {
|
||||
let mut parsed = ParsedQuery {
|
||||
plain: extract_plain(s),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let kvs = extract_key_values(s);
|
||||
|
||||
for (k, v) in kvs {
|
||||
match k {
|
||||
"title" => parsed.title = Some(v),
|
||||
"artist" => parsed.artist = Some(v),
|
||||
"duet" => parsed.duet = parse_bool(v),
|
||||
"video" => parsed.video = parse_bool(v),
|
||||
"lang" => parsed.language = Some(v),
|
||||
"genre" => parsed.genre = Some(v),
|
||||
"year" => parsed.year = Some(v),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
parsed
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_bool(s: &str) -> Option<bool> {
|
||||
match s {
|
||||
"true" | "yes" => Some(true),
|
||||
"false" | "no" => Some(false),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_plain(s: &str) -> Option<Cow<str>> {
|
||||
let plain: String =
|
||||
s.split(' ')
|
||||
.filter(|word| !word.contains(':'))
|
||||
.fold(String::new(), |mut a, b| {
|
||||
if !a.is_empty() {
|
||||
a.push(' ');
|
||||
}
|
||||
a.push_str(b);
|
||||
a
|
||||
});
|
||||
|
||||
plain.is_empty().not().then(|| Cow::Owned(plain))
|
||||
}
|
||||
|
||||
fn extract_key_values(s: &str) -> impl Iterator<Item = (&str, &str)> {
|
||||
s.split_whitespace().filter_map(|s| s.split_once(':'))
|
||||
}
|
||||
|
||||
impl Display for ParsedQuery<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
let mut first = true;
|
||||
let mut w = |prefix: &str, display: Option<&dyn Display>| -> fmt::Result {
|
||||
match display {
|
||||
Some(display) => {
|
||||
if first {
|
||||
first = false;
|
||||
} else {
|
||||
write!(f, " ")?;
|
||||
}
|
||||
write!(f, "{}{}", prefix, display)
|
||||
}
|
||||
None => Ok(()),
|
||||
}
|
||||
};
|
||||
|
||||
fn display<T: Display>(v: &Option<T>) -> Option<&dyn Display> {
|
||||
v.as_ref().map(|s| s as &dyn Display)
|
||||
}
|
||||
|
||||
w("", display(&self.plain))?;
|
||||
w("title:", display(&self.title))?;
|
||||
w("artist:", display(&self.artist))?;
|
||||
w("duet:", display(&self.duet))?;
|
||||
w("video:", display(&self.video))?;
|
||||
w("lang:", display(&self.language))?;
|
||||
w("genre:", display(&self.genre))?;
|
||||
w("year:", display(&self.year))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
55
src/song.rs
55
src/song.rs
@ -1,4 +1,5 @@
|
||||
use crate::fuzzy::{self, FuzzyScore};
|
||||
use crate::query::ParsedQuery;
|
||||
use serde::Deserialize;
|
||||
use std::cmp::max;
|
||||
|
||||
@ -26,10 +27,56 @@ impl Song {
|
||||
.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());
|
||||
pub fn fuzzy_compare(&self, query: &ParsedQuery) -> FuzzyScore {
|
||||
let bad = || -1;
|
||||
|
||||
max(title_score, artist_score)
|
||||
let filter_strs = |query: Option<&str>, item: Option<&str>| {
|
||||
if let Some(query) = query {
|
||||
match item {
|
||||
Some(item) => {
|
||||
let score = fuzzy::compare(item.chars(), query.chars());
|
||||
if score < fuzzy::max_score(query) / 2 {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
None => return false,
|
||||
}
|
||||
}
|
||||
true
|
||||
};
|
||||
|
||||
let filter_bool =
|
||||
|query: Option<bool>, item| !matches!(query, Some(query) if query != item);
|
||||
|
||||
let filters: &[&dyn Fn() -> bool] = &[
|
||||
&|| filter_bool(query.duet, self.duet().is_some()),
|
||||
&|| filter_bool(query.video, self.video.is_some()),
|
||||
&|| filter_strs(query.language, self.language.as_deref()),
|
||||
&|| filter_strs(query.genre, self.genre.as_deref()),
|
||||
&|| filter_strs(query.year, self.year.as_deref()),
|
||||
];
|
||||
|
||||
if !filters.iter().all(|f| f()) {
|
||||
return bad();
|
||||
}
|
||||
|
||||
let mut score = FuzzyScore::default();
|
||||
if let Some(plain) = &query.plain {
|
||||
let title_score = fuzzy::compare(self.title.chars(), plain.chars());
|
||||
let artist_score = fuzzy::compare(self.artist.chars(), plain.chars());
|
||||
score = max(title_score, artist_score);
|
||||
}
|
||||
|
||||
if let Some(title) = query.title {
|
||||
let new_score = fuzzy::compare(self.title.chars(), title.chars());
|
||||
score = max(score, new_score);
|
||||
}
|
||||
|
||||
if let Some(artist) = query.artist {
|
||||
let new_score = fuzzy::compare(self.artist.chars(), artist.chars());
|
||||
score = max(score, new_score);
|
||||
}
|
||||
|
||||
score
|
||||
}
|
||||
}
|
||||
|
||||
21
static/images/flag.svg
Normal file
21
static/images/flag.svg
Normal file
@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="64"
|
||||
height="64"
|
||||
viewBox="0 0 16.933333 16.933334"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<g
|
||||
id="layer1">
|
||||
<path
|
||||
style="fill:#ffffff;stroke-width:0.0172872"
|
||||
d="M 2.0431764,16.908998 C 1.9602187,16.872757 1.876554,16.793057 1.8422936,16.717632 1.8116739,16.650224 1.8105455,16.401051 1.810179,9.6279558 L 1.8097869,2.6081437 1.6641359,2.5092064 C 1.2472056,2.2259971 1.0391181,1.8036281 1.0713576,1.3060066 1.0844166,1.1044411 1.1224816,0.96380799 1.2142435,0.77810771 1.3901797,0.42206407 1.7319615,0.15177983 2.1468873,0.0405643 2.3323772,-0.00915361 2.7302473,-0.01417349 2.8988814,0.03107665 3.3528542,0.15289318 3.7091938,0.44111518 3.8848869,0.82859726 4.0597307,1.2142068 4.0366627,1.7326461 3.8292893,2.0781429 3.7421304,2.2233555 3.57164,2.3977544 3.4085243,2.5085543 l -0.1466114,0.099589 -3.921e-4,7.0198121 c -3.92e-4,6.7912266 -0.00142,7.0221046 -0.032342,7.0901846 -0.038333,0.0844 -0.1349174,0.168073 -0.2252432,0.195135 -0.097123,0.0291 -0.8924887,0.02556 -0.960784,-0.0043 z M 4.4373436,12.716063 C 4.2495943,12.658952 4.0830255,12.448031 4.030713,12.201158 3.9938784,12.027325 3.9933603,3.8105555 4.0301903,3.6368753 4.0755058,3.4230858 4.1697878,3.2812388 4.3715252,3.1233409 5.3165223,2.3836996 6.3178698,1.8281179 7.1314687,1.5920287 7.5769935,1.4627473 7.8698032,1.4201238 8.3098088,1.4205014 c 0.489238,3.92e-4 0.8716048,0.059099 1.2965409,0.198971 0.4326523,0.1424116 1.0852773,0.4785873 1.8454873,0.9506357 0.373219,0.2317482 0.431206,0.2606372 0.620161,0.3089634 0.378486,0.096801 0.849505,0.00191 1.427559,-0.2876026 0.334418,-0.1674874 0.481383,-0.2591206 0.80027,-0.4989694 0.380455,-0.286156 0.773889,-0.5542429 0.917678,-0.6253073 0.105568,-0.052175 0.136868,-0.060373 0.191991,-0.050288 0.203687,0.037265 0.387579,0.2559054 0.435924,0.5183002 0.0255,0.1384181 0.02682,8.6933686 0.0014,8.8276256 -0.04196,0.221249 -0.146314,0.34086 -0.54429,0.62386 -1.367655,0.972536 -2.434688,1.331821 -3.326394,1.120042 -0.379668,-0.09017 -0.901986,-0.356434 -1.323138,-0.6745 C 10.371145,11.619406 10.208111,11.5187 9.9025808,11.368732 9.1628643,11.005644 8.5121596,10.936609 7.6874652,11.133724 7.3017711,11.225912 6.8336043,11.405483 6.3827127,11.63418 5.8966468,11.880716 5.6917131,12.0075 4.8136497,12.604888 4.6413416,12.722118 4.5479619,12.749707 4.4373579,12.716063 Z"
|
||||
id="path836" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
28
static/images/note.svg
Normal file
28
static/images/note.svg
Normal file
@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="64"
|
||||
height="64"
|
||||
viewBox="0 0 16.933333 16.933334"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<g
|
||||
id="layer1">
|
||||
<path
|
||||
style="fill:#ffffff;stroke:#000000;stroke-width:0.027706"
|
||||
d="M 4.5551369,13.73784 C 4.4712159,13.713848 4.2942016,13.631736 4.1617718,13.555369 3.9569689,13.437269 3.8864653,13.375379 3.6899365,13.141189 3.4947882,12.908643 3.4405963,12.821296 3.3412101,12.579103 3.1831538,12.193936 3.1341667,11.913692 3.1494268,11.481958 3.1792799,10.637362 3.5697022,9.9484817 4.2213186,9.5906614 4.5074657,9.4335284 4.6608076,9.3951632 5.0001725,9.3957953 c 0.3135708,5.704e-4 0.5150999,0.048388 0.7563296,0.1794085 0.3300738,0.1792741 0.6561763,0.5253422 0.8331234,0.8841312 l 0.1088097,0.220629 0.027707,-0.189904 C 6.9470115,8.9761896 6.6960377,6.9399561 6.0763141,5.2177993 5.9357403,4.8271574 5.6389438,4.1214756 5.5058847,3.8615112 5.4508509,3.7539882 5.4112872,3.6595299 5.4179658,3.6516022 5.4567654,3.6055488 6.6007883,3.373365 7.2253505,3.2847856 8.1011111,3.1605811 8.324241,3.1472014 9.5268205,3.1467839 c 1.3500215,-4.564e-4 1.7915365,0.034262 2.9118045,0.2290499 0.55905,0.097205 0.525804,0.083369 0.591628,0.2462207 0.101089,0.2501087 0.329321,1.1059901 0.448665,1.6825274 0.117824,0.5691893 0.214134,1.2008098 0.273853,1.795992 0.04477,0.4461473 0.04513,2.154298 5.77e-4,2.5807942 -0.109067,1.0434349 -0.269326,1.8555249 -0.512069,2.5948459 -0.234881,0.715378 -0.62845,1.181088 -1.191885,1.410356 -0.277845,0.113057 -0.722659,0.133043 -1.014105,0.04557 -1.3979076,-0.419587 -1.8940712,-2.441281 -0.900268,-3.668278 0.536302,-0.6621431 1.349345,-0.8627349 2.060124,-0.508268 0.350035,0.1745629 0.723984,0.571198 0.892645,0.946793 0.04324,0.09629 0.08236,0.175083 0.08692,0.175083 0.01834,0 0.06685,-0.49438 0.09118,-0.9293692 C 13.317443,8.8268273 13.202847,7.6012054 12.980897,6.7000545 12.921884,6.4604495 12.940482,6.4769175 12.667531,6.422634 11.506786,6.1917919 10.005801,6.0828451 8.8237205,6.1436358 8.1898052,6.1762364 7.3817978,6.2496667 7.2414036,6.2874331 c -0.055361,0.014892 -0.05885,0.026539 -0.04285,0.1430145 0.2622878,1.9092854 0.1127805,4.0985924 -0.3905402,5.7188654 -0.2725808,0.877484 -0.7067678,1.380715 -1.3604851,1.576831 -0.2413187,0.0724 -0.6608065,0.07789 -0.8923609,0.01169 z"
|
||||
id="path836" />
|
||||
<ellipse
|
||||
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:1.00874;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.938225;stop-color:#000000"
|
||||
id="path958"
|
||||
cx="8.4408598"
|
||||
cy="8.4666681"
|
||||
rx="7.9622936"
|
||||
ry="7.9622946" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@ -33,15 +33,14 @@ body {
|
||||
bottom: 0;
|
||||
left: 1em;
|
||||
right: 1em;
|
||||
padding-right: 1em; /* Leave space for scroll bar */
|
||||
}
|
||||
|
||||
.song_search_bar {
|
||||
position: relative;
|
||||
padding: 1em 1em .5em;
|
||||
max-width: 35em;
|
||||
max-width: 38em;
|
||||
width: 80%;
|
||||
margin: auto;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.song_search_field {
|
||||
@ -169,20 +168,27 @@ body {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.duet_icon {
|
||||
background-image: url("/images/duet.svg");
|
||||
.gizmo {
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
.duet_icon {
|
||||
background-image: url("/images/duet.svg");
|
||||
}
|
||||
|
||||
.video_icon {
|
||||
background-image: url("/images/video.svg");
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
.flag_icon {
|
||||
background-image: url("/images/flag.svg");
|
||||
}
|
||||
|
||||
.note_icon {
|
||||
background-image: url("/images/note.svg");
|
||||
}
|
||||
|
||||
.hidden {
|
||||
|
||||
Reference in New Issue
Block a user