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.artist} - ` : ""}${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
-