diff --git a/assets/js/loader.js b/assets/js/loader.js
deleted file mode 100644
index 4cdb194..0000000
--- a/assets/js/loader.js
+++ /dev/null
@@ -1,82 +0,0 @@
-window.playerModal = document.getElementById("player");
-window.playerAudio = document.getElementById("player-el");
-window.playerTitle = document.getElementById("player-title");
-window.versionModal = document.getElementById("versions");
-window.versionList = document.getElementById("versions-list");
-window.versionTitle = document.getElementById("versions-title");
-window.originalTitle = document.title;
-
-window.onload = async () => {
- window.files = await (await fetch("https://cdn.floo.fi/watercolor/records/directory.json")).json();
- window.filesProcessed = [];
-
- for (let _file of Object.values(files)) {
- let file = {
- versions: _file
- };
- file.year = Math.max(...file.versions.map(i => i.year));
- file.edition = file.versions[0].edition.filter(i => !i.startsWith("v"));
- file.artist = file.versions[0].artist;
- file.track = file.versions[0].track;
- file.original = file.versions[0].original;
- file.ai = file.versions[0].ai;
- filesProcessed.push(file);
- }
-
- document.getElementById("js-data-list").innerHTML = Object.values(window.filesProcessed)
- .map((i, j) => [i, j])
- .sort((a, b) => a[0].artist.localeCompare(b[0].artist))
- .sort((a, b) => b[0].year - a[0].year)
- .sort((a, b) => (b[0].ai ? -1 : 1) - (a[0].ai ? -1 : 1))
- .map(i => {
- let j = i[1];
- i = i[0];
- return `
-
- ${!i.ai && !i.original ? `` : ""}${i.track}
- ${i.edition.length > 0 ? i.edition.map(e => `
-
- ${e}
- `).join("") : ""}
- ${i.ai ? `
-
- AI generated
- ` : ""}
- ${i.original && !i.ai ? `
-
- Original
- ` : ""}
- ${i.versions.length > 1 ? `
-
- ${i.versions.length} versions
- ` : ""}
-
- `;
- }).join("");
-
- registerClicks();
-
- // noinspection JSUnresolvedReference
- window.fuse = new Fuse(Object.values(window.filesProcessed), {
- keys: [
- {name: 'artist', weight: 0.9},
- {name: 'track', weight: 1},
- {name: 'edition', weight: 0.8},
- {name: 'versions.edition', weight: 0.8},
- {name: 'versions.year', weight: 0.5},
- {name: 'versions.file',weight: 0.5}
- ]
- });
-
- window.processHash();
-
- document.getElementById("count").innerText = Object.keys(window.filesProcessed).length + " productions";
- completeLoad();
- document.getElementById("app").style.display = "";
- document.getElementById("search").value = "";
- document.getElementById("search").focus();
-};
diff --git a/assets/js/scroll.js b/assets/js/scroll.js
deleted file mode 100644
index d355fa3..0000000
--- a/assets/js/scroll.js
+++ /dev/null
@@ -1,11 +0,0 @@
-window.onscroll = () => {
- updateScroll();
-}
-
-function updateScroll() {
- if (window.scrollY === 0) {
- document.getElementById("navbar").classList.add("fella-nav-no-border");
- } else {
- document.getElementById("navbar").classList.remove("fella-nav-no-border");
- }
-}
diff --git a/assets/js/util.js b/assets/js/util.js
deleted file mode 100644
index 4e95a25..0000000
--- a/assets/js/util.js
+++ /dev/null
@@ -1,122 +0,0 @@
-function modalHide() {
- document.title = window.originalTitle;
- window.playerAudio.pause();
-}
-
-function crc32(input) {
- let a, o, c, n, t;
-
- for (o = [], c = 0; c < 256; c++) {
- a = c;
-
- for (let f = 0; f < 8; f++) {
- a = 1 & a ? 3988292384 ^ a >>> 1 : a >>> 1;
- }
-
- o[c] = a;
- }
-
- for (n = -1, t = 0; t < input.length; t++) {
- n = n >>> 8 ^ o[255 & (n ^ input.charCodeAt(t))];
- }
-
- return (-1 ^ n) >>> 0;
-}
-
-window.onhashchange = window.processHash = () => {
- modalHide();
- document.getElementById('player').classList.remove('show');
- document.getElementById('versions').classList.remove('show');
-
- let hash = location.hash.substring(2);
- if (location.hash !== "" && hash !== "") {
- let version;
- for (let record of window.filesProcessed) {
- for (let entry of Object.entries(record.versions)) {
- let currentVersion = entry[1];
- if (currentVersion.id === hash.split("/")[0] && entry[0] === hash.split("/")[1]) {
- version = currentVersion;
- }
- }
- }
- if (!version) {
- location.hash = "";
- return;
- }
- window.playerTitle.innerText = document.title = version.artist + " - " + version.track +
- (version.edition.length > 0 ? " (" + version.edition.join(", ") + ")" : "") + " [" + version.year + "]";
- window.player.initialize(window.playerAudio, "https://cdn.floo.fi/watercolor/records/" + version['cdnId'] + "/stream_dash.mpd", true);
- window.playerAudio.play();
- window.playerModal.classList.add("show");
- }
-}
-
-function registerClicks(base = "js-data-list-item-") {
- Object.entries(window.filesProcessed).map((i, j) => {
- if (document.getElementById(base + j)) document.getElementById(base + j)
- .onclick = () => {
- if (i[1].versions.length < 2) {
- let version = i[1].versions[0];
- location.hash = "#/" + version.id + "/0";
- } else {
- window.versionTitle.innerText = i[1].track + (i[1].edition.length > 0 ? " (" + i[1].edition.join(", ") + ")" : "")
- window.versionList.innerHTML = i[1].versions.map((i, j) => [i, j])
- .sort((a, b) => b[0].file.localeCompare(a[0].file))
- .sort((a, b) => a[0].edition.length - b[0].edition.length)
- .sort((a, b) => b[0].year - a[0].year)
- .map(i => {
- j = i[1];
- i = i[0];
- return `
-
- ${i.year}
- ${i.track}
- ${i.edition.length > 0 ? i.edition.map(e => `
-
- ${e}
- `).join("") : ""}
-
- `
- }).join("");
- window.versionModal.classList.add("show");
-
- i[1].versions.map((version, j) => {
- document.getElementById("versions-item-" + j).onclick = () => {
- location.hash = "#/" + version.id + "/" + j;
- }
- });
- }
- };
- });
-}
-
-function search() {
- let query = document.getElementById("search").value.trim();
-
- if (query === "") {
- document.getElementById("js-data-list").style.display = "";
- document.getElementById("js-data-results").style.display = "none";
- document.getElementById("js-data-results").innerHTML = "";
- return;
- }
-
- let results = fuse.search(query).map(i => {
- i = document.getElementById("js-data-list-item-" + i['refIndex']).cloneNode(true);
- i.id = i.id.replace("-list-", "-results-");
- return i;
- });
-
- document.getElementById("js-data-list").style.display = "none";
- document.getElementById("js-data-results").style.display = "";
- document.getElementById("js-data-results").innerHTML = "";
-
- for (let item of results) {
- document.getElementById("js-data-results").insertAdjacentElement("beforeend", item);
- }
-
- registerClicks("js-data-results-item-");
-}
diff --git a/build.sh b/build.sh
new file mode 100644
index 0000000..6d1a7dd
--- /dev/null
+++ b/build.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+cd ./engine
+rm -rf ../assets/engine
+wasm-pack build -t web -d ../assets/engine --no-pack --no-typescript --release
+rm ../assets/engine/.gitignore
+terser ../assets/engine/engine.js > ../assets/engine/engine.min.js
+mv -f ../assets/engine/engine.min.js ../assets/engine/engine.js
\ No newline at end of file
diff --git a/engine/Cargo.toml b/engine/Cargo.toml
index 167b6ad..3dabfc4 100644
--- a/engine/Cargo.toml
+++ b/engine/Cargo.toml
@@ -10,7 +10,10 @@ crate-type = ["cdylib", "rlib"]
wasm-bindgen = "0.2.84"
wasm-bindgen-futures = "0.4.49"
console_error_panic_hook = "0.1.7"
-web-sys = { version = "0.3.76", features = ["Document", "Element", "HtmlElement", "Node", "Window", "Headers", "Request", "RequestInit", "RequestMode", "Response", "DomTokenList", "HtmlAudioElement", "Location", "HtmlInputElement"] }
+web-sys = { version = "0.3.76", features = [
+ "Document", "Element", "HtmlElement", "Node", "Window", "Headers", "Request", "RequestInit", "RequestMode",
+ "Response", "DomTokenList", "HtmlAudioElement", "Location", "HtmlInputElement"
+] }
serde = { version = "1.0.216", features = ["derive"] }
serde_json = "1.0.134"
futures = "0.3.31"
@@ -18,11 +21,15 @@ crc = "3.2.1"
[profile.release]
opt-level = "s"
+codegen-units = 1
+lto = "fat"
+panic = "abort"
+strip = "symbols"
[lints.rust]
rust_2024_compatibility = "warn"
[lints.clippy]
-pedantic = "warn"
+pedantic = { level = "warn", priority = -1 }
missing_panics_doc = "allow"
missing_errors_doc = "allow"
\ No newline at end of file
diff --git a/engine/src/lib.rs b/engine/src/lib.rs
index ac0b3bb..3cba55c 100644
--- a/engine/src/lib.rs
+++ b/engine/src/lib.rs
@@ -1,36 +1,25 @@
mod utils;
-use std::cell::{OnceCell, RefCell};
-use std::sync::{Arc, Mutex};
use std::collections::HashMap;
-use std::fs::create_dir_all;
-use std::rc::Rc;
use serde::{Deserialize, Serialize};
-use wasm_bindgen::closure::WasmClosureFnOnce;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::JsFuture;
-use web_sys::{Document, Element, HtmlAudioElement, HtmlElement, HtmlInputElement, Location, Request, RequestInit, RequestMode, Response, Window};
-use crate::utils::{log, initialize_dash, set_panic_hook};
+use web_sys::{Document, Element, HtmlAudioElement, HtmlElement, HtmlInputElement, Location, Request, RequestInit, RequestMode, Response};
+use crate::utils::{initialize_dash, set_panic_hook, eval, fella_complete_load};
-thread_local! {
- static APPLICATION_STATE: RefCell = unreachable!()
-}
+static mut APPLICATION_STATE: Option = None;
-#[wasm_bindgen]
#[derive(Debug, Clone)]
pub struct State {
- window: Window,
location: Location,
document: Document,
- body: HtmlElement,
- directory: Directory,
songs: Vec,
old_title: String,
version: VersionModal,
- player: PlayerModal
+ player: PlayerModal,
+ search: HtmlInputElement
}
-#[wasm_bindgen]
#[derive(Debug, Clone)]
pub struct VersionModal {
modal: Element,
@@ -38,7 +27,6 @@ pub struct VersionModal {
title: Element
}
-#[wasm_bindgen]
#[derive(Debug, Clone)]
pub struct PlayerModal {
modal: Element,
@@ -75,16 +63,19 @@ pub struct DirectoryEntry {
#[derive(Debug, Clone)]
struct Directory(HashMap>);
-pub fn modal_hide(state: &State) {
- state.location.set_hash("").unwrap();
+#[wasm_bindgen]
+pub fn modal_hide() {
+ let state = get_state();
state.document.set_title(&state.old_title);
let _ = state.player.audio.pause();
state.player.modal.class_list().remove_1("show").unwrap();
}
-#[wasm_bindgen]
-pub fn modal_hide_global() {
- APPLICATION_STATE.with_borrow(modal_hide);
+#[allow(static_mut_refs)]
+fn get_state<'a>() -> &'a State {
+ unsafe {
+ APPLICATION_STATE.as_ref().unwrap()
+ }
}
async fn get_directory() -> Directory {
@@ -103,20 +94,17 @@ async fn get_directory() -> Directory {
let json = JsFuture::from(response.text().unwrap()).await
.unwrap().as_string().unwrap();
- Directory(serde_json::from_str(&json).unwrap(),)
+ Directory(serde_json::from_str(&json).unwrap())
}
#[wasm_bindgen]
-pub fn process_hash_global() {
- APPLICATION_STATE.with_borrow(process_hash);
-}
-
-pub fn process_hash(state: &State) {
- modal_hide(state);
+pub fn process_hash() {
+ let state = get_state();
+ let hash = state.location.hash().unwrap();
+ modal_hide();
state.player.modal.class_list().remove_1("show").unwrap();
state.version.modal.class_list().remove_1("show").unwrap();
- let hash = state.location.hash().unwrap();
let parts: Vec<&str> = hash.split("#/").collect();
if parts.len() > 1 {
@@ -129,7 +117,7 @@ pub fn process_hash(state: &State) {
})
.find(Option::is_some);
- if let Some(Some((index, version))) = version {
+ if let Some(Some((_, version))) = version {
let mut title = format!("{} - {}", version.artist, version.track);
if !version.edition.is_empty() {
title.push_str(&format!(" ({})", version.edition.join(", ")));
@@ -148,50 +136,73 @@ pub fn process_hash(state: &State) {
}
}
-#[wasm_bindgen]
+#[wasm_bindgen(start)]
pub async fn start() {
set_panic_hook();
- println!("Hello from Rust!");
let window = web_sys::window().expect("No global `window` exists");
let document = window.document().expect("Should have a document on window");
- let body = document.body().expect("Document should have a body");
-
- println!("Fetching directory...");
let directory: Directory = get_directory().await;
- println!("Got {} directory entries", directory.0.len());
document.get_element_by_id("count")
.unwrap()
.set_text_content(Some(&format!("{} productions", directory.0.len())));
let songs: Vec = (&directory).into();
- let state = State {
- window: window.clone(),
- location: window.location(),
- document: document.clone(),
- body: body.clone(),
- directory: directory.clone(),
- songs: songs.clone(),
- old_title: document.title(),
- version: VersionModal {
- modal: document.get_element_by_id("versions").unwrap(),
- list: document.get_element_by_id("versions-list").unwrap(),
- title: document.get_element_by_id("versions-title").unwrap()
- },
- player: PlayerModal {
- modal: document.get_element_by_id("player").unwrap(),
- audio: document.get_element_by_id("player-el").unwrap().dyn_into().unwrap(),
- title: document.get_element_by_id("player-title").unwrap()
+ unsafe {
+ APPLICATION_STATE = Some(State {
+ location: window.location(),
+ document: document.clone(),
+ songs: songs.clone(),
+ old_title: document.title(),
+ version: VersionModal {
+ modal: document.get_element_by_id("versions").unwrap(),
+ list: document.get_element_by_id("versions-list").unwrap(),
+ title: document.get_element_by_id("versions-title").unwrap()
+ },
+ player: PlayerModal {
+ modal: document.get_element_by_id("player").unwrap(),
+ audio: document.get_element_by_id("player-el").unwrap().dyn_into().unwrap(),
+ title: document.get_element_by_id("player-title").unwrap()
+ },
+ search: document.get_element_by_id("search").unwrap().dyn_into().unwrap()
+ });
+ }
+
+ populate_list(&songs, "js-data-list");
+
+ document.get_element_by_id("search")
+ .unwrap().dyn_into::()
+ .unwrap().set_value("");
+ document.get_element_by_id("search")
+ .unwrap().dyn_into::()
+ .unwrap().focus().unwrap();
+
+ process_hash();
+ eval("window.addEventListener('hashchange', () => wasm.process_hash());");
+ eval("document.getElementById('player-modal-close').addEventListener('click', () => { wasm.modal_hide(); location.hash = ''; });");
+ eval("document.getElementById('app').style.display = '';"); // TODO: There has to be a better way to do this
+ fella_complete_load();
+}
+
+fn register_clicks(base: &str) {
+ let state = get_state();
+ for index in 0..state.songs.len() {
+ let id = &format!("{base}{index}");
+ if let Some(el) = state.document.get_element_by_id(id) {
+ eval(&format!("document.getElementById('{}').addEventListener('click', () => {{ wasm.select_song({index}); }});", el.id()));
}
- };
+ }
+}
- println!("{:#?}", state);
- println!("Filling HTML...");
- let container = document.get_element_by_id("js-data-list").unwrap();
+fn populate_list(list: &[Song], id: &str) {
+ let state = get_state();
+ let document = &state.document;
+ let container = document.get_element_by_id(id).unwrap();
+ container.set_inner_html("");
- let mut songs_enumeration = songs
+ let mut songs_enumeration = list
.iter()
.enumerate()
.collect::>();
@@ -201,32 +212,49 @@ pub async fn start() {
songs_enumeration.sort_by(|a, b| b.1.year.partial_cmp(&a.1.year).unwrap());
songs_enumeration.sort_by(|a, b| a.1.ai.partial_cmp(&b.1.ai).unwrap());
- for (id, element) in songs_enumeration {
- container.append_child(&element.html(&state, id)).unwrap();
+ for (eid, element) in songs_enumeration {
+ container.append_child(&element.html(eid, id)).unwrap();
}
- document.get_element_by_id("search")
- .unwrap().dyn_into::()
- .unwrap().set_value("");
- document.get_element_by_id("search")
- .unwrap().dyn_into::()
- .unwrap().focus().unwrap();
+ register_clicks(&format!("{id}-item-"));
+}
- process_hash(&state);
- APPLICATION_STATE.set(state);
+#[wasm_bindgen]
+pub fn select_song(index: usize) {
+ let state = get_state();
+ let song = &state.songs[index];
+
+ if song.versions.len() < 2 {
+ let version = &song.versions[0];
+ state.location.set_hash(&format!("#/{}/0", version.id)).unwrap();
+ } else {
+ let mut title = song.track.clone();
+ if !song.edition.is_empty() {
+ title.push_str(&format!(" ({})", song.edition.join(", ")));
+ }
+ state.version.title.set_text_content(Some(&title));
+ state.version.list.set_inner_html("");
+
+ let versions = song.versions.clone();
+ let mut versions: Vec<(usize, &DirectoryEntry)> = versions
+ .iter()
+ .enumerate()
+ .collect();
+ versions.sort_by(|(_, va), (_, vb)| va.file.partial_cmp(&vb.file).unwrap());
+ versions.sort_by(|(_, va), (_, vb)| va.edition.len().partial_cmp(&vb.edition.len()).unwrap());
+ versions.sort_by(|(_, va), (_, vb)| vb.year.partial_cmp(&va.year).unwrap());
+
+ for (id, entry) in versions {
+ state.version.list.append_child(&entry.html(id)).unwrap();
+ }
- let callback = Closure::::new(process_hash_global);
- let callback = callback.as_ref().unchecked_ref();
- window.add_event_listener_with_callback("hashchange", callback).unwrap();
+ for (index, version) in song.versions.iter().enumerate() {
+ eval(&format!("document.getElementById('versions-item-{index}').addEventListener('click', () => {{ location.hash = \"#/{}/{index}\"; }});",
+ version.id));
+ }
- let callback = &modal_hide_global
- .into_js_function()
- .dyn_into()
- .unwrap();
- document.get_element_by_id("player-modal-close")
- .unwrap()
- .add_event_listener_with_callback("click", callback)
- .unwrap();
+ state.version.modal.class_list().add_1("show").unwrap();
+ }
}
fn hash_text_color(text: &str) -> (u16, u16, u16) {
@@ -240,6 +268,30 @@ fn hash_text_color(text: &str) -> (u16, u16, u16) {
)
}
+fn get_search_results(query: &str) -> Vec {
+ let state = get_state();
+ let query = query.to_lowercase();
+ state.songs.clone().into_iter()
+ .filter(|x| x.track.to_lowercase().contains(&query) || x.artist.to_lowercase().contains(&query))
+ .collect()
+}
+
+#[wasm_bindgen]
+pub fn search() {
+ let state = get_state();
+ let query = state.search.value();
+
+ if query.is_empty() {
+ eval("document.getElementById('js-data-list').style.display = '';");
+ eval("document.getElementById('js-data-results').style.display = 'none';");
+ } else {
+ let results = get_search_results(&query);
+ populate_list(&results, "js-data-results");
+ eval("document.getElementById('js-data-list').style.display = 'none';");
+ eval("document.getElementById('js-data-results').style.display = '';");
+ }
+}
+
impl From<&Directory> for Vec {
fn from(directory: &Directory) -> Vec {
directory.0.values()
@@ -264,11 +316,12 @@ impl From<&Directory> for Vec {
}
impl Song {
- fn html(&self, state: &State, id: usize) -> HtmlElement {
+ fn html(&self, id: usize, prefix: &str) -> HtmlElement {
+ let state = get_state();
let document = &state.document;
let element = document.create_element("a").unwrap();
- element.set_id(&format!("js-data-list-item-{id}"));
+ element.set_id(&format!("{prefix}-item-{id}"));
element.class_list().add_3("fella-list-item", "fella-list-link", "fella-list-item-padded")
.unwrap();
@@ -322,7 +375,45 @@ impl Song {
}
let element: HtmlElement = element.dyn_into().unwrap();
+ element
+ }
+}
+impl DirectoryEntry {
+ fn html(&self, id: usize) -> HtmlElement {
+ let state = get_state();
+ let document = &state.document;
+ let element = document.create_element("a").unwrap();
+
+ element.class_list().add_3("fella-list-item", "fella-list-link", "fella-list-item-padded")
+ .unwrap();
+ element.set_id(&format!("versions-item-{id}"));
+
+ let year = document.create_element("span").unwrap();
+ year.set_text_content(Some(&self.year.to_string()));
+ year.class_list().add_1("fella-badge-notice").unwrap();
+ let hash = hash_text_color(&self.year.to_string());
+ year.set_attribute("style",
+ &format!("--fella-badge-notice-rgb: {},{},{} !important;", hash.0, hash.1, hash.2)
+ ).unwrap();
+ element.append_with_node_1(&year).unwrap();
+
+ let track = document.create_element("span").unwrap();
+ track.set_text_content(Some(&self.track));
+ element.append_with_node_1(&track).unwrap();
+
+ for ed in &self.edition {
+ let edition = document.create_element("span").unwrap();
+ edition.class_list().add_1("fella-badge-notice").unwrap();
+ edition.set_text_content(Some(ed));
+ let hash = hash_text_color(ed);
+ edition.set_attribute("style",
+ &format!("--fella-badge-notice-rgb: {},{},{} !important;", hash.0, hash.1, hash.2)
+ ).unwrap();
+ element.append_with_node_1(&edition).unwrap();
+ }
+
+ let element: HtmlElement = element.dyn_into().unwrap();
element
}
}
\ No newline at end of file
diff --git a/engine/src/utils.rs b/engine/src/utils.rs
index fd389d2..dc40880 100644
--- a/engine/src/utils.rs
+++ b/engine/src/utils.rs
@@ -6,20 +6,23 @@ pub fn set_panic_hook() {
#[wasm_bindgen]
extern {
- #[wasm_bindgen(js_namespace = console)]
+ /*#[wasm_bindgen(js_namespace = console)]
pub fn log(message: &str);
#[wasm_bindgen(js_namespace = console)]
- pub fn error(message: &str);
-
- /*#[wasm_bindgen]
- pub fn set_title(title: &str);*/
+ pub fn error(message: &str);*/
#[wasm_bindgen]
pub fn initialize_dash(url: &str);
+
+ #[wasm_bindgen]
+ pub fn eval(code: &str);
+
+ #[wasm_bindgen(js_name = completeLoad)]
+ pub fn fella_complete_load();
}
-#[macro_export]
+/*#[macro_export]
macro_rules! println {
($($t:tt)*) => (log(&format_args!($($t)*).to_string()))
}
@@ -27,4 +30,4 @@ macro_rules! println {
#[macro_export]
macro_rules! eprintln {
($($t:tt)*) => (error(&format_args!($($t)*).to_string()))
-}
\ No newline at end of file
+}*/
\ No newline at end of file
diff --git a/index.html b/index.html
index 61d3ff8..1551c8a 100644
--- a/index.html
+++ b/index.html
@@ -7,24 +7,14 @@
Floofi Music Library
-
-
@@ -91,7 +81,7 @@
- Loading...
+
@@ -105,8 +95,8 @@
target="_blank">Sign up to get notifications when new releases are published.
+ autofocus placeholder="Search for some cover or production..." id="search" onkeydown="wasm.search();"
+ onkeyup="wasm.search();" onchange="wasm.search();">
@@ -156,19 +146,11 @@ Select a version
-